From 483cc454030e0b52f22322fe2a6a655b28d4fa78 Mon Sep 17 00:00:00 2001 From: mgiota Date: Thu, 19 May 2022 20:10:24 +0200 Subject: [PATCH 001/120] [Actionable Observability] update alerts table rule details link to point to o11y rule detail page (#132479) * update alerts table rule details link to point to o11y rule detail page * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * o11y alert flyout should also link to o11y rule details page * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * add data-test-subj to rule details page title and add move path definition * fix failing tests by checking existance of Observability in breadcrumb * use alerts and rules link from the paths file * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * update link in alert flyout to use paths * update rule details link in the rules page Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/observability/public/config/paths.ts | 7 ++++++- .../components/alerts_flyout/alerts_flyout.tsx | 3 +-- .../alerts_table_t_grid/alerts_table_t_grid.tsx | 3 +-- .../public/pages/rule_details/config.ts | 3 --- .../public/pages/rule_details/index.tsx | 16 ++++++---------- .../public/pages/rules/components/name.tsx | 3 ++- .../apps/observability/alerts/index.ts | 4 +++- 7 files changed, 19 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/observability/public/config/paths.ts b/x-pack/plugins/observability/public/config/paths.ts index 57bbc95fef40b..7f6599ef3c483 100644 --- a/x-pack/plugins/observability/public/config/paths.ts +++ b/x-pack/plugins/observability/public/config/paths.ts @@ -5,9 +5,14 @@ * 2.0. */ +export const ALERT_PAGE_LINK = '/app/observability/alerts'; +export const RULES_PAGE_LINK = `${ALERT_PAGE_LINK}/rules`; + export const paths = { observability: { - alerts: '/app/observability/alerts', + alerts: ALERT_PAGE_LINK, + rules: RULES_PAGE_LINK, + ruleDetails: (ruleId: string) => `${RULES_PAGE_LINK}/${encodeURI(ruleId)}`, }, management: { rules: '/app/management/insightsAndAlerting/triggersActions/rules', diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.tsx index d0957f0224b53..5a1b88ff1a420 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout.tsx @@ -77,8 +77,7 @@ export function AlertsFlyout({ } const ruleId = alertData.fields['kibana.alert.rule.uuid'] ?? null; - const linkToRule = ruleId && prepend ? prepend(paths.management.ruleDetails(ruleId)) : null; - + const linkToRule = ruleId && prepend ? prepend(paths.observability.ruleDetails(ruleId)) : null; const overviewListItems = [ { title: translations.alertsFlyout.statusLabel, diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx index 621a43eedfc25..c9d2d67e11bdc 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx @@ -170,8 +170,7 @@ function ObservabilityActions({ const casePermissions = useGetUserCasesPermissions(); const ruleId = alert.fields['kibana.alert.rule.uuid'] ?? null; - const linkToRule = ruleId ? http.basePath.prepend(paths.management.ruleDetails(ruleId)) : null; - + const linkToRule = ruleId ? http.basePath.prepend(paths.observability.ruleDetails(ruleId)) : null; const caseAttachments: CaseAttachments = useMemo(() => { return ecsData?._id ? [ diff --git a/x-pack/plugins/observability/public/pages/rule_details/config.ts b/x-pack/plugins/observability/public/pages/rule_details/config.ts index e73849f47e7b3..8822c68a85a0b 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/config.ts +++ b/x-pack/plugins/observability/public/pages/rule_details/config.ts @@ -18,6 +18,3 @@ export function hasAllPrivilege(rule: InitialRule, ruleType?: RuleType): boolean export const hasExecuteActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.execute; - -export const RULES_PAGE_LINK = '/app/observability/alerts/rules'; -export const ALERT_PAGE_LINK = '/app/observability/alerts'; diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx index 9cce5bfb99c92..e5d6cccab60a8 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/index.tsx @@ -56,12 +56,8 @@ import { PageTitle, ItemTitleRuleSummary, ItemValueRuleSummary, Actions } from ' import { useKibana } from '../../utils/kibana_react'; import { useFetchLast24hAlerts } from '../../hooks/use_fetch_last24h_alerts'; import { formatInterval } from './utils'; -import { - hasExecuteActionsCapability, - hasAllPrivilege, - RULES_PAGE_LINK, - ALERT_PAGE_LINK, -} from './config'; +import { hasExecuteActionsCapability, hasAllPrivilege } from './config'; +import { paths } from '../../config/paths'; export function RuleDetailsPage() { const { @@ -125,10 +121,10 @@ export function RuleDetailsPage() { text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { defaultMessage: 'Alerts', }), - href: http.basePath.prepend(ALERT_PAGE_LINK), + href: http.basePath.prepend(paths.observability.alerts), }, { - href: http.basePath.prepend(RULES_PAGE_LINK), + href: http.basePath.prepend(paths.observability.rules), text: RULES_BREADCRUMB_TEXT, }, { @@ -476,11 +472,11 @@ export function RuleDetailsPage() { { setRuleToDelete([]); - navigateToUrl(http.basePath.prepend(RULES_PAGE_LINK)); + navigateToUrl(http.basePath.prepend(paths.observability.rules)); }} onErrors={async () => { setRuleToDelete([]); - navigateToUrl(http.basePath.prepend(RULES_PAGE_LINK)); + navigateToUrl(http.basePath.prepend(paths.observability.rules)); }} onCancel={() => {}} apiDeleteCall={deleteRules} diff --git a/x-pack/plugins/observability/public/pages/rules/components/name.tsx b/x-pack/plugins/observability/public/pages/rules/components/name.tsx index 15cb44412d880..96418758df0a5 100644 --- a/x-pack/plugins/observability/public/pages/rules/components/name.tsx +++ b/x-pack/plugins/observability/public/pages/rules/components/name.tsx @@ -9,10 +9,11 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; import { RuleNameProps } from '../types'; import { useKibana } from '../../../utils/kibana_react'; +import { paths } from '../../../config/paths'; export function Name({ name, rule }: RuleNameProps) { const { http } = useKibana().services; - const detailsLink = http.basePath.prepend(`/app/observability/alerts/rules/${rule.id}`); + const detailsLink = http.basePath.prepend(paths.observability.ruleDetails(rule.id)); const link = ( diff --git a/x-pack/test/observability_functional/apps/observability/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/alerts/index.ts index e1fd795d55ffb..5afdb0b00c774 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/index.ts @@ -223,7 +223,9 @@ export default ({ getService }: FtrProviderContext) => { const actionsButton = await observability.alerts.common.getActionsButtonByIndex(0); await actionsButton.click(); await observability.alerts.common.viewRuleDetailsButtonClick(); - expect(await find.existsByCssSelector('[title="Rules and Connectors"]')).to.eql(true); + expect( + await (await find.byCssSelector('[data-test-subj="breadcrumb first"]')).getVisibleText() + ).to.eql('Observability'); }); }); From 956fbc76d96c1a98f13e983c15000c227341a489 Mon Sep 17 00:00:00 2001 From: mgiota Date: Thu, 19 May 2022 20:11:04 +0200 Subject: [PATCH 002/120] [Actionable Observability] render human readable rule type name and notify when fields in o11y rule details page (#132404) * render rule type name * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * human readable text for notify field * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * create getNotifyText function * increase bundle size for triggers_actions_ui plugin (temp) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-optimizer/limits.yml | 2 +- .../public/pages/rule_details/index.tsx | 16 +++++++++++----- .../sections/rule_form/rule_notify_when.tsx | 2 +- .../plugins/triggers_actions_ui/public/index.ts | 3 +-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 504ba4906ffd5..b9012d30b0f18 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -58,7 +58,7 @@ pageLoadAssetSize: telemetry: 51957 telemetryManagementSection: 38586 transform: 41007 - triggersActionsUi: 107800 #This is temporary. Check https://github.com/elastic/kibana/pull/130710#issuecomment-1119843458 & https://github.com/elastic/kibana/issues/130728 + triggersActionsUi: 119000 #This is temporary. Check https://github.com/elastic/kibana/pull/130710#issuecomment-1119843458 & https://github.com/elastic/kibana/issues/130728 upgradeAssistant: 81241 urlForwarding: 32579 usageCollection: 39762 diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx index e5d6cccab60a8..31b9a888ec266 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/index.tsx @@ -34,6 +34,7 @@ import { deleteRules, useLoadRuleTypes, RuleType, + NOTIFY_WHEN_OPTIONS, RuleEventLogListProps, } from '@kbn/triggers-actions-ui-plugin/public'; // TODO: use a Delete modal from triggersActionUI when it's sharable @@ -75,7 +76,7 @@ export function RuleDetailsPage() { const { ruleId } = useParams(); const { ObservabilityPageTemplate } = usePluginContext(); const { isRuleLoading, rule, errorRule, reloadRule } = useFetchRule({ ruleId, http }); - const { ruleTypes } = useLoadRuleTypes({ + const { ruleTypes, ruleTypeIndex } = useLoadRuleTypes({ filteredSolutions: OBSERVABILITY_SOLUTIONS, }); @@ -109,8 +110,9 @@ export function RuleDetailsPage() { useEffect(() => { if (ruleTypes.length && rule) { const matchedRuleType = ruleTypes.find((type) => type.id === rule.ruleTypeId); + setRuleType(matchedRuleType); + if (rule.consumer === ALERTS_FEATURE_ID && matchedRuleType && matchedRuleType.producer) { - setRuleType(matchedRuleType); setFeatures(matchedRuleType.producer); } else setFeatures(rule.consumer); } @@ -217,6 +219,9 @@ export function RuleDetailsPage() { /> ); + const getNotifyText = () => + NOTIFY_WHEN_OPTIONS.find((option) => option.value === rule?.notifyWhen)?.inputDisplay || + rule.notifyWhen; return ( - + @@ -438,8 +445,7 @@ export function RuleDetailsPage() { defaultMessage: 'Notify', })} - - + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.tsx index 4c23aa0dda40d..992c4df4e5798 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.tsx @@ -28,7 +28,7 @@ import { RuleNotifyWhenType } from '../../../types'; const DEFAULT_NOTIFY_WHEN_VALUE: RuleNotifyWhenType = 'onActionGroupChange'; -const NOTIFY_WHEN_OPTIONS: Array> = [ +export const NOTIFY_WHEN_OPTIONS: Array> = [ { value: 'onActionGroupChange', inputDisplay: i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 001f63bc6cc6f..9c08dfe597ecf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -89,9 +89,8 @@ export { loadRuleAggregations, loadRuleTags } from './application/lib/rule_api/a export { useLoadRuleTypes } from './application/hooks/use_load_rule_types'; export { loadRule } from './application/lib/rule_api/get_rule'; export { loadAllActions } from './application/lib/action_connector_api'; - export { loadActionTypes } from './application/lib/action_connector_api/connector_types'; - +export { NOTIFY_WHEN_OPTIONS } from './application/sections/rule_form/rule_notify_when'; export type { TIME_UNITS } from './application/constants'; export { getTimeUnitLabel } from './common/lib/get_time_unit_label'; export type { TriggersAndActionsUiServices } from './application/app'; From 4b262a52fd7b48ad7b5729d540a99b8318a2e5f2 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 19 May 2022 15:04:50 -0400 Subject: [PATCH 003/120] Fix test (#132546) --- .../apps/triggers_actions_ui/alerts_table.ts | 103 ++++++++---------- 1 file changed, 48 insertions(+), 55 deletions(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_table.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_table.ts index 56026093c88dd..27989942d3e95 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_table.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_table.ts @@ -87,48 +87,41 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); }); - // This keeps failing in CI because the next button is not clickable - // Revisit this once we change the UI around based on feedback - /* - fail: Actions and Triggers app Alerts table should open a flyout and paginate through the flyout - │ Error: retry.try timeout: ElementClickInterceptedError: element click intercepted: Element ... is not clickable at point (1564, 795). Other element would receive the click:
...
- */ - // it('should open a flyout and paginate through the flyout', async () => { - // await PageObjects.common.navigateToUrlWithBrowserHistory('triggersActions', '/alerts'); - // await waitTableIsLoaded(); - // await testSubjects.click('expandColumnCellOpenFlyoutButton-0'); - // await waitFlyoutOpen(); - // await waitFlyoutIsLoaded(); - - // expect(await testSubjects.getVisibleText('alertsFlyoutName')).to.be( - // 'APM Failed Transaction Rate (one)' - // ); - // expect(await testSubjects.getVisibleText('alertsFlyoutReason')).to.be( - // 'Failed transactions rate is greater than 5.0% (current value is 31%) for elastic-co-frontend' - // ); - - // await testSubjects.click('pagination-button-next'); - - // expect(await testSubjects.getVisibleText('alertsFlyoutName')).to.be( - // 'APM Failed Transaction Rate (one)' - // ); - // expect(await testSubjects.getVisibleText('alertsFlyoutReason')).to.be( - // 'Failed transactions rate is greater than 5.0% (current value is 35%) for opbeans-python' - // ); - - // await testSubjects.click('pagination-button-previous'); - // await testSubjects.click('pagination-button-previous'); - - // await waitTableIsLoaded(); - - // const rows = await getRows(); - // expect(rows[0].status).to.be('close'); - // expect(rows[0].lastUpdated).to.be('2021-10-19T14:55:14.503Z'); - // expect(rows[0].duration).to.be('252002000'); - // expect(rows[0].reason).to.be( - // 'CPU usage is greater than a threshold of 40 (current value is 56.7%) for gke-edge-oblt-default-pool-350b44de-c3dd' - // ); - // }); + it('should open a flyout and paginate through the flyout', async () => { + await PageObjects.common.navigateToUrlWithBrowserHistory('triggersActions', '/alerts'); + await waitTableIsLoaded(); + await testSubjects.click('expandColumnCellOpenFlyoutButton-0'); + await waitFlyoutOpen(); + await waitFlyoutIsLoaded(); + + expect(await testSubjects.getVisibleText('alertsFlyoutName')).to.be( + 'APM Failed Transaction Rate (one)' + ); + expect(await testSubjects.getVisibleText('alertsFlyoutReason')).to.be( + 'Failed transactions rate is greater than 5.0% (current value is 31%) for elastic-co-frontend' + ); + + await testSubjects.click('alertsFlyoutPagination > pagination-button-next'); + + expect(await testSubjects.getVisibleText('alertsFlyoutName')).to.be( + 'APM Failed Transaction Rate (one)' + ); + expect(await testSubjects.getVisibleText('alertsFlyoutReason')).to.be( + 'Failed transactions rate is greater than 5.0% (current value is 35%) for opbeans-python' + ); + + await testSubjects.click('alertsFlyoutPagination > pagination-button-previous'); + + await waitTableIsLoaded(); + + const rows = await getRows(); + expect(rows[0].status).to.be('active'); + expect(rows[0].lastUpdated).to.be('2021-10-19T15:20:38.749Z'); + expect(rows[0].duration).to.be('1197194000'); + expect(rows[0].reason).to.be( + 'Failed transactions rate is greater than 5.0% (current value is 31%) for elastic-co-frontend' + ); + }); async function waitTableIsLoaded() { return await retry.try(async () => { @@ -137,19 +130,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); } - // async function waitFlyoutOpen() { - // return await retry.try(async () => { - // const exists = await testSubjects.exists('alertsFlyout'); - // if (!exists) throw new Error('Still loading...'); - // }); - // } - - // async function waitFlyoutIsLoaded() { - // return await retry.try(async () => { - // const exists = await testSubjects.exists('alertsFlyoutLoading'); - // if (exists) throw new Error('Still loading...'); - // }); - // } + async function waitFlyoutOpen() { + return await retry.try(async () => { + const exists = await testSubjects.exists('alertsFlyout'); + if (!exists) throw new Error('Still loading...'); + }); + } + + async function waitFlyoutIsLoaded() { + return await retry.try(async () => { + const exists = await testSubjects.exists('alertsFlyoutLoading'); + if (exists) throw new Error('Still loading...'); + }); + } async function getRows() { const euiDataGridRows = await find.allByCssSelector('.euiDataGridRow'); From ee8158002035e3e9e8de5200ca0c6b128a76b423 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 May 2022 14:16:49 -0500 Subject: [PATCH 004/120] skip failing test suite (#132288) --- test/functional/apps/discover/_chart_hidden.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_chart_hidden.ts b/test/functional/apps/discover/_chart_hidden.ts index a9179fd234905..44fa42e568a0b 100644 --- a/test/functional/apps/discover/_chart_hidden.ts +++ b/test/functional/apps/discover/_chart_hidden.ts @@ -19,7 +19,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - describe('discover show/hide chart test', function () { + // Failing: See https://github.com/elastic/kibana/issues/132288 + describe.skip('discover show/hide chart test', function () { before(async function () { log.debug('load kibana index with default index pattern'); From f96ff560ed38ddf9e3027cb1cea5d4da1a0ccdec Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 19 May 2022 12:28:00 -0700 Subject: [PATCH 005/120] [Fleet] Reduce bundle size limit (#132488) --- packages/kbn-optimizer/limits.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index b9012d30b0f18..8856f7f0aaabb 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -27,7 +27,7 @@ pageLoadAssetSize: indexLifecycleManagement: 107090 indexManagement: 140608 infra: 184320 - fleet: 250000 + fleet: 95000 ingestPipelines: 58003 inputControlVis: 172675 inspector: 148711 From 9814c8515dcb1c767f10b79df0b2bee0dd6e6039 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 May 2022 15:41:38 -0500 Subject: [PATCH 006/120] skip failing test suite (#132553) --- test/functional/apps/discover/_context_encoded_url_param.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_context_encoded_url_param.ts b/test/functional/apps/discover/_context_encoded_url_param.ts index fdbee7a637f46..95540c929130c 100644 --- a/test/functional/apps/discover/_context_encoded_url_param.ts +++ b/test/functional/apps/discover/_context_encoded_url_param.ts @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const es = getService('es'); - describe('encoded URL params in context page', () => { + // Failing: See https://github.com/elastic/kibana/issues/132553 + describe.skip('encoded URL params in context page', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'context_encoded_param']); await PageObjects.common.navigateToApp('settings'); From 42eec11a8d30d63c4d82de2d3a0ecd0272a1a9a4 Mon Sep 17 00:00:00 2001 From: Tre Date: Thu, 19 May 2022 22:12:22 +0100 Subject: [PATCH 007/120] Rebalance dashboard group 1 (#132193) Split a group of the files to group 6. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../functional/apps/dashboard/group1/index.ts | 13 -- .../apps/dashboard/group6/config.ts | 18 ++ .../group6/create_and_add_embeddables.ts | 169 ++++++++++++++++++ .../dashboard_back_button.ts | 0 .../dashboard_error_handling.ts | 0 .../{group1 => group6}/dashboard_options.ts | 0 .../{group1 => group6}/dashboard_query_bar.ts | 0 .../data_shared_attributes.ts | 0 .../{group1 => group6}/embed_mode.ts | 0 .../apps/dashboard/group6/empty_dashboard.ts | 67 +++++++ .../functional/apps/dashboard/group6/index.ts | 46 +++++ .../{group1 => group6}/legacy_urls.ts | 0 .../saved_search_embeddable.ts | 0 .../dashboard/{group1 => group6}/share.ts | 0 14 files changed, 300 insertions(+), 13 deletions(-) create mode 100644 test/functional/apps/dashboard/group6/config.ts create mode 100644 test/functional/apps/dashboard/group6/create_and_add_embeddables.ts rename test/functional/apps/dashboard/{group1 => group6}/dashboard_back_button.ts (100%) rename test/functional/apps/dashboard/{group1 => group6}/dashboard_error_handling.ts (100%) rename test/functional/apps/dashboard/{group1 => group6}/dashboard_options.ts (100%) rename test/functional/apps/dashboard/{group1 => group6}/dashboard_query_bar.ts (100%) rename test/functional/apps/dashboard/{group1 => group6}/data_shared_attributes.ts (100%) rename test/functional/apps/dashboard/{group1 => group6}/embed_mode.ts (100%) create mode 100644 test/functional/apps/dashboard/group6/empty_dashboard.ts create mode 100644 test/functional/apps/dashboard/group6/index.ts rename test/functional/apps/dashboard/{group1 => group6}/legacy_urls.ts (100%) rename test/functional/apps/dashboard/{group1 => group6}/saved_search_embeddable.ts (100%) rename test/functional/apps/dashboard/{group1 => group6}/share.ts (100%) diff --git a/test/functional/apps/dashboard/group1/index.ts b/test/functional/apps/dashboard/group1/index.ts index 597102433ef45..736dfd6f577f8 100644 --- a/test/functional/apps/dashboard/group1/index.ts +++ b/test/functional/apps/dashboard/group1/index.ts @@ -37,18 +37,5 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_unsaved_state')); loadTestFile(require.resolve('./dashboard_unsaved_listing')); loadTestFile(require.resolve('./edit_visualizations')); - loadTestFile(require.resolve('./dashboard_options')); - loadTestFile(require.resolve('./data_shared_attributes')); - loadTestFile(require.resolve('./share')); - loadTestFile(require.resolve('./embed_mode')); - loadTestFile(require.resolve('./dashboard_back_button')); - loadTestFile(require.resolve('./dashboard_error_handling')); - loadTestFile(require.resolve('./legacy_urls')); - loadTestFile(require.resolve('./saved_search_embeddable')); - - // Note: This one must be last because it unloads some data for one of its tests! - // No, this isn't ideal, but loading/unloading takes so much time and these are all bunched - // to improve efficiency... - loadTestFile(require.resolve('./dashboard_query_bar')); }); } diff --git a/test/functional/apps/dashboard/group6/config.ts b/test/functional/apps/dashboard/group6/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/dashboard/group6/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard/group6/create_and_add_embeddables.ts b/test/functional/apps/dashboard/group6/create_and_add_embeddables.ts new file mode 100644 index 0000000000000..c96e596a88ecf --- /dev/null +++ b/test/functional/apps/dashboard/group6/create_and_add_embeddables.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { VisualizeConstants } from '@kbn/visualizations-plugin/common/constants'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '@kbn/visualizations-plugin/common/constants'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); + const browser = getService('browser'); + const kibanaServer = getService('kibanaServer'); + const dashboardAddPanel = getService('dashboardAddPanel'); + + describe('create and add embeddables', () => { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/dashboard/current/kibana' + ); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + }); + + it('ensure toolbar popover closes on add', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.switchToEditMode(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAddNewEmbeddableLink('LOG_STREAM_EMBEDDABLE'); + await dashboardAddPanel.expectEditorMenuClosed(); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + }); + + describe('add new visualization link', () => { + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.loadSavedDashboard('few panels'); + }); + + it('adds new visualization via the top nav link', async () => { + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + await PageObjects.dashboard.switchToEditMode(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); + await PageObjects.visualize.clickAreaChart(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.visualize.saveVisualizationExpectSuccess( + 'visualization from top nav add new panel', + { redirectToOrigin: true } + ); + await retry.try(async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(originalPanelCount + 1); + }); + await PageObjects.dashboard.waitForRenderComplete(); + }); + + it('adds a new visualization', async () => { + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); + await PageObjects.visualize.clickAreaChart(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.visualize.saveVisualizationExpectSuccess( + 'visualization from add new link', + { redirectToOrigin: true } + ); + + await retry.try(async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(originalPanelCount + 1); + }); + await PageObjects.dashboard.waitForRenderComplete(); + }); + + it('adds a new timelion visualization', async () => { + // adding this case, as the timelion agg-based viz doesn't need the `clickNewSearch()` step + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); + await PageObjects.visualize.clickTimelion(); + await PageObjects.visualize.saveVisualizationExpectSuccess( + 'timelion visualization from add new link', + { redirectToOrigin: true } + ); + + await retry.try(async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(originalPanelCount + 1); + }); + await PageObjects.dashboard.waitForRenderComplete(); + }); + + it('adds a markdown visualization via the quick button', async () => { + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + await dashboardAddPanel.clickMarkdownQuickButton(); + await PageObjects.visualize.saveVisualizationExpectSuccess( + 'visualization from markdown quick button', + { redirectToOrigin: true } + ); + + await retry.try(async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(originalPanelCount + 1); + }); + await PageObjects.dashboard.waitForRenderComplete(); + }); + + it('saves the listing page instead of the visualization to the app link', async () => { + await PageObjects.header.clickVisualize(true); + const currentUrl = await browser.getCurrentUrl(); + expect(currentUrl).not.to.contain(VisualizeConstants.EDIT_PATH); + }); + + after(async () => { + await PageObjects.header.clickDashboard(); + }); + }); + + describe('visualize:enableLabs advanced setting', () => { + const LAB_VIS_NAME = 'Rendering Test: input control'; + + it('should display lab visualizations in add panel', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + const exists = await dashboardAddPanel.panelAddLinkExists(LAB_VIS_NAME); + await dashboardAddPanel.closeAddPanel(); + expect(exists).to.be(true); + }); + + describe('is false', () => { + before(async () => { + await PageObjects.header.clickStackManagement(); + await PageObjects.settings.clickKibanaSettings(); + await PageObjects.settings.toggleAdvancedSettingCheckbox(VISUALIZE_ENABLE_LABS_SETTING); + }); + + it('should not display lab visualizations in add panel', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + + const exists = await dashboardAddPanel.panelAddLinkExists(LAB_VIS_NAME); + await dashboardAddPanel.closeAddPanel(); + expect(exists).to.be(false); + }); + + after(async () => { + await PageObjects.header.clickStackManagement(); + await PageObjects.settings.clickKibanaSettings(); + await PageObjects.settings.clearAdvancedSettings(VISUALIZE_ENABLE_LABS_SETTING); + await PageObjects.header.clickDashboard(); + }); + }); + }); + }); +} diff --git a/test/functional/apps/dashboard/group1/dashboard_back_button.ts b/test/functional/apps/dashboard/group6/dashboard_back_button.ts similarity index 100% rename from test/functional/apps/dashboard/group1/dashboard_back_button.ts rename to test/functional/apps/dashboard/group6/dashboard_back_button.ts diff --git a/test/functional/apps/dashboard/group1/dashboard_error_handling.ts b/test/functional/apps/dashboard/group6/dashboard_error_handling.ts similarity index 100% rename from test/functional/apps/dashboard/group1/dashboard_error_handling.ts rename to test/functional/apps/dashboard/group6/dashboard_error_handling.ts diff --git a/test/functional/apps/dashboard/group1/dashboard_options.ts b/test/functional/apps/dashboard/group6/dashboard_options.ts similarity index 100% rename from test/functional/apps/dashboard/group1/dashboard_options.ts rename to test/functional/apps/dashboard/group6/dashboard_options.ts diff --git a/test/functional/apps/dashboard/group1/dashboard_query_bar.ts b/test/functional/apps/dashboard/group6/dashboard_query_bar.ts similarity index 100% rename from test/functional/apps/dashboard/group1/dashboard_query_bar.ts rename to test/functional/apps/dashboard/group6/dashboard_query_bar.ts diff --git a/test/functional/apps/dashboard/group1/data_shared_attributes.ts b/test/functional/apps/dashboard/group6/data_shared_attributes.ts similarity index 100% rename from test/functional/apps/dashboard/group1/data_shared_attributes.ts rename to test/functional/apps/dashboard/group6/data_shared_attributes.ts diff --git a/test/functional/apps/dashboard/group1/embed_mode.ts b/test/functional/apps/dashboard/group6/embed_mode.ts similarity index 100% rename from test/functional/apps/dashboard/group1/embed_mode.ts rename to test/functional/apps/dashboard/group6/embed_mode.ts diff --git a/test/functional/apps/dashboard/group6/empty_dashboard.ts b/test/functional/apps/dashboard/group6/empty_dashboard.ts new file mode 100644 index 0000000000000..e559c0ef81f60 --- /dev/null +++ b/test/functional/apps/dashboard/group6/empty_dashboard.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardVisualizations = getService('dashboardVisualizations'); + const dashboardExpect = getService('dashboardExpect'); + const PageObjects = getPageObjects(['common', 'dashboard']); + + describe('empty dashboard', () => { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/dashboard/current/kibana' + ); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + after(async () => { + await dashboardAddPanel.closeAddPanel(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await kibanaServer.savedObjects.cleanStandardList(); + }); + + it('should display empty widget', async () => { + const emptyWidgetExists = await testSubjects.exists('emptyDashboardWidget'); + expect(emptyWidgetExists).to.be(true); + }); + + it('should open add panel when add button is clicked', async () => { + await dashboardAddPanel.clickOpenAddPanel(); + const isAddPanelOpen = await dashboardAddPanel.isAddPanelOpen(); + expect(isAddPanelOpen).to.be(true); + await testSubjects.click('euiFlyoutCloseButton'); + }); + + it('should add new visualization from dashboard', async () => { + await dashboardVisualizations.createAndAddMarkdown({ + name: 'Dashboard Test Markdown', + markdown: 'Markdown text', + }); + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.markdownWithValuesExists(['Markdown text']); + }); + + it('should open editor menu when editor button is clicked', async () => { + await dashboardAddPanel.clickEditorMenuButton(); + await testSubjects.existOrFail('dashboardEditorContextMenu'); + }); + }); +} diff --git a/test/functional/apps/dashboard/group6/index.ts b/test/functional/apps/dashboard/group6/index.ts new file mode 100644 index 0000000000000..f78f7e2d549b8 --- /dev/null +++ b/test/functional/apps/dashboard/group6/index.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + async function loadCurrentData() { + await browser.setWindowSize(1300, 900); + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data'); + } + + async function unloadCurrentData() { + await esArchiver.unload('test/functional/fixtures/es_archiver/dashboard/current/data'); + } + + describe('dashboard app - group 1', function () { + before(loadCurrentData); + after(unloadCurrentData); + + // This has to be first since the other tests create some embeddables as side affects and our counting assumes + // a fresh index. + loadTestFile(require.resolve('./empty_dashboard')); + loadTestFile(require.resolve('./dashboard_options')); + loadTestFile(require.resolve('./data_shared_attributes')); + loadTestFile(require.resolve('./share')); + loadTestFile(require.resolve('./embed_mode')); + loadTestFile(require.resolve('./dashboard_back_button')); + loadTestFile(require.resolve('./dashboard_error_handling')); + loadTestFile(require.resolve('./legacy_urls')); + loadTestFile(require.resolve('./saved_search_embeddable')); + + // Note: This one must be last because it unloads some data for one of its tests! + // No, this isn't ideal, but loading/unloading takes so much time and these are all bunched + // to improve efficiency... + loadTestFile(require.resolve('./dashboard_query_bar')); + }); +} diff --git a/test/functional/apps/dashboard/group1/legacy_urls.ts b/test/functional/apps/dashboard/group6/legacy_urls.ts similarity index 100% rename from test/functional/apps/dashboard/group1/legacy_urls.ts rename to test/functional/apps/dashboard/group6/legacy_urls.ts diff --git a/test/functional/apps/dashboard/group1/saved_search_embeddable.ts b/test/functional/apps/dashboard/group6/saved_search_embeddable.ts similarity index 100% rename from test/functional/apps/dashboard/group1/saved_search_embeddable.ts rename to test/functional/apps/dashboard/group6/saved_search_embeddable.ts diff --git a/test/functional/apps/dashboard/group1/share.ts b/test/functional/apps/dashboard/group6/share.ts similarity index 100% rename from test/functional/apps/dashboard/group1/share.ts rename to test/functional/apps/dashboard/group6/share.ts From b2008488ba0efeff58347e0e998692c3b7701cc0 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Thu, 19 May 2022 16:17:14 -0500 Subject: [PATCH 008/120] [Shared UX] Move No Data Views to package (#131996) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 + packages/BUILD.bazel | 2 + packages/kbn-shared-ux-components/BUILD.bazel | 2 + .../src/empty_state/index.ts | 1 - .../empty_state/kibana_no_data_page.test.tsx | 8 +- .../src/empty_state/kibana_no_data_page.tsx | 33 +- .../no_data_views/no_data_views.stories.tsx | 49 -- .../kbn-shared-ux-components/src/index.ts | 41 -- .../prompt/no_data_views/BUILD.bazel | 142 +++++ .../prompt/no_data_views/README.mdx} | 8 +- .../prompt/no_data_views/jest.config.js} | 7 +- .../prompt/no_data_views/package.json | 8 + .../documentation_link.test.tsx.snap | 4 +- .../src/data_view_illustration.tsx | 552 ++++++++++++++++++ .../src}/documentation_link.test.tsx | 0 .../no_data_views/src}/documentation_link.tsx | 4 +- .../prompt/no_data_views/src/index.tsx | 47 ++ .../src}/no_data_views.component.test.tsx | 12 +- .../src}/no_data_views.component.tsx | 27 +- .../src/no_data_views.stories.tsx | 68 +++ .../no_data_views/src}/no_data_views.test.tsx | 30 +- .../no_data_views/src}/no_data_views.tsx | 20 +- .../prompt/no_data_views/src/services.tsx | 115 ++++ .../prompt/no_data_views/tsconfig.json | 20 + .../empty_prompts/empty_prompts.tsx | 4 +- .../translations/translations/fr-FR.json | 4 +- .../translations/translations/ja-JP.json | 4 +- .../translations/translations/zh-CN.json | 4 +- yarn.lock | 10 + 29 files changed, 1067 insertions(+), 161 deletions(-) delete mode 100644 packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.stories.tsx create mode 100644 packages/shared-ux/prompt/no_data_views/BUILD.bazel rename packages/{kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.mdx => shared-ux/prompt/no_data_views/README.mdx} (74%) rename packages/{kbn-shared-ux-components/src/empty_state/no_data_views/index.tsx => shared-ux/prompt/no_data_views/jest.config.js} (72%) create mode 100644 packages/shared-ux/prompt/no_data_views/package.json rename packages/{kbn-shared-ux-components/src/empty_state/no_data_views => shared-ux/prompt/no_data_views/src}/__snapshots__/documentation_link.test.tsx.snap (82%) create mode 100644 packages/shared-ux/prompt/no_data_views/src/data_view_illustration.tsx rename packages/{kbn-shared-ux-components/src/empty_state/no_data_views => shared-ux/prompt/no_data_views/src}/documentation_link.test.tsx (100%) rename packages/{kbn-shared-ux-components/src/empty_state/no_data_views => shared-ux/prompt/no_data_views/src}/documentation_link.tsx (88%) create mode 100644 packages/shared-ux/prompt/no_data_views/src/index.tsx rename packages/{kbn-shared-ux-components/src/empty_state/no_data_views => shared-ux/prompt/no_data_views/src}/no_data_views.component.test.tsx (79%) rename packages/{kbn-shared-ux-components/src/empty_state/no_data_views => shared-ux/prompt/no_data_views/src}/no_data_views.component.tsx (77%) create mode 100644 packages/shared-ux/prompt/no_data_views/src/no_data_views.stories.tsx rename packages/{kbn-shared-ux-components/src/empty_state/no_data_views => shared-ux/prompt/no_data_views/src}/no_data_views.test.tsx (53%) rename packages/{kbn-shared-ux-components/src/empty_state/no_data_views => shared-ux/prompt/no_data_views/src}/no_data_views.tsx (72%) create mode 100644 packages/shared-ux/prompt/no_data_views/src/services.tsx create mode 100644 packages/shared-ux/prompt/no_data_views/tsconfig.json diff --git a/package.json b/package.json index 6330d68c742b1..72f4acfc18354 100644 --- a/package.json +++ b/package.json @@ -184,6 +184,7 @@ "@kbn/shared-ux-components": "link:bazel-bin/packages/kbn-shared-ux-components", "@kbn/shared-ux-link-redirect-app": "link:bazel-bin/packages/shared-ux/link/redirect_app", "@kbn/shared-ux-page-analytics-no-data": "link:bazel-bin/packages/shared-ux/page/analytics_no_data", + "@kbn/shared-ux-prompt-no-data-views": "link:bazel-bin/packages/shared-ux/prompt/no_data_views", "@kbn/shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services", "@kbn/shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook", "@kbn/shared-ux-utility": "link:bazel-bin/packages/kbn-shared-ux-utility", @@ -682,6 +683,7 @@ "@types/kbn__shared-ux-components": "link:bazel-bin/packages/kbn-shared-ux-components/npm_module_types", "@types/kbn__shared-ux-link-redirect-app": "link:bazel-bin/packages/shared-ux/link/redirect_app/npm_module_types", "@types/kbn__shared-ux-page-analytics-no-data": "link:bazel-bin/packages/shared-ux/page/analytics_no_data/npm_module_types", + "@types/kbn__shared-ux-prompt-no-data-views": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/npm_module_types", "@types/kbn__shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services/npm_module_types", "@types/kbn__shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook/npm_module_types", "@types/kbn__shared-ux-utility": "link:bazel-bin/packages/kbn-shared-ux-utility/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 234a69cb4bdf7..51db32d5d89f7 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -116,6 +116,7 @@ filegroup( "//packages/shared-ux/button/exit_full_screen:build", "//packages/shared-ux/link/redirect_app:build", "//packages/shared-ux/page/analytics_no_data:build", + "//packages/shared-ux/prompt/no_data_views:build", ], ) @@ -215,6 +216,7 @@ filegroup( "//packages/shared-ux/button/exit_full_screen:build_types", "//packages/shared-ux/link/redirect_app:build_types", "//packages/shared-ux/page/analytics_no_data:build_types", + "//packages/shared-ux/prompt/no_data_views:build_types", ], ) diff --git a/packages/kbn-shared-ux-components/BUILD.bazel b/packages/kbn-shared-ux-components/BUILD.bazel index b1420f5376041..1a4a7100ded72 100644 --- a/packages/kbn-shared-ux-components/BUILD.bazel +++ b/packages/kbn-shared-ux-components/BUILD.bazel @@ -44,6 +44,7 @@ RUNTIME_DEPS = [ "//packages/kbn-i18n", "//packages/shared-ux/avatar/solution", "//packages/shared-ux/link/redirect_app", + "//packages/shared-ux/prompt/no_data_views", "//packages/kbn-shared-ux-services", "//packages/kbn-shared-ux-storybook", "//packages/kbn-shared-ux-utility", @@ -72,6 +73,7 @@ TYPES_DEPS = [ "//packages/kbn-i18n:npm_module_types", "//packages/shared-ux/avatar/solution:npm_module_types", "//packages/shared-ux/link/redirect_app:npm_module_types", + "//packages/shared-ux/prompt/no_data_views:npm_module_types", "//packages/kbn-shared-ux-services:npm_module_types", "//packages/kbn-shared-ux-storybook:npm_module_types", "//packages/kbn-shared-ux-utility:npm_module_types", diff --git a/packages/kbn-shared-ux-components/src/empty_state/index.ts b/packages/kbn-shared-ux-components/src/empty_state/index.ts index 68defa5269344..9883d595633a7 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/index.ts +++ b/packages/kbn-shared-ux-components/src/empty_state/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export { NoDataViews, NoDataViewsComponent } from './no_data_views'; export { KibanaNoDataPage } from './kibana_no_data_page'; diff --git a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.test.tsx b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.test.tsx index 4f565e55ef52c..3b117f54369a0 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.test.tsx +++ b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.test.tsx @@ -12,10 +12,10 @@ import { act } from 'react-dom/test-utils'; import { EuiLoadingElastic } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { SharedUxServicesProvider, mockServicesFactory } from '@kbn/shared-ux-services'; +import { NoDataViewsPrompt } from '@kbn/shared-ux-prompt-no-data-views'; import { KibanaNoDataPage } from './kibana_no_data_page'; import { NoDataConfigPage } from '../page_template'; -import { NoDataViews } from './no_data_views'; describe('Kibana No Data Page', () => { const noDataConfig = { @@ -52,7 +52,7 @@ describe('Kibana No Data Page', () => { component.update(); expect(component.find(NoDataConfigPage).length).toBe(1); - expect(component.find(NoDataViews).length).toBe(0); + expect(component.find(NoDataViewsPrompt).length).toBe(0); }); test('renders NoDataViews', async () => { @@ -66,7 +66,7 @@ describe('Kibana No Data Page', () => { await act(() => new Promise(setImmediate)); component.update(); - expect(component.find(NoDataViews).length).toBe(1); + expect(component.find(NoDataViewsPrompt).length).toBe(1); expect(component.find(NoDataConfigPage).length).toBe(0); }); @@ -90,7 +90,7 @@ describe('Kibana No Data Page', () => { component.update(); expect(component.find(EuiLoadingElastic).length).toBe(1); - expect(component.find(NoDataViews).length).toBe(0); + expect(component.find(NoDataViewsPrompt).length).toBe(0); expect(component.find(NoDataConfigPage).length).toBe(0); }); }); diff --git a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.tsx b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.tsx index 89ba915c07cfd..5d0f84e0bd41b 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.tsx +++ b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.tsx @@ -6,10 +6,14 @@ * Side Public License, v 1. */ import React, { useEffect, useState } from 'react'; +import { useData, useDocLinks, useEditors, usePermissions } from '@kbn/shared-ux-services'; +import { + NoDataViewsPrompt, + NoDataViewsPromptProvider, + NoDataViewsPromptServices, +} from '@kbn/shared-ux-prompt-no-data-views'; import { EuiLoadingElastic } from '@elastic/eui'; -import { useData } from '@kbn/shared-ux-services'; import { NoDataConfigPage, NoDataPageProps } from '../page_template'; -import { NoDataViews } from './no_data_views'; export interface Props { onDataViewCreated: (dataView: unknown) => void; @@ -17,6 +21,11 @@ export interface Props { } export const KibanaNoDataPage = ({ onDataViewCreated, noDataConfig }: Props) => { + // These hooks are temporary, until this component is moved to a package. + const { canCreateNewDataView } = usePermissions(); + const { dataViewsDocLink } = useDocLinks(); + const { openDataViewEditor } = useEditors(); + const { hasESData, hasUserDataView } = useData(); const [isLoading, setIsLoading] = useState(true); const [dataExists, setDataExists] = useState(false); @@ -43,8 +52,26 @@ export const KibanaNoDataPage = ({ onDataViewCreated, noDataConfig }: Props) => return ; } + /* + TODO: clintandrewhall - the use and population of `NoDataViewPromptProvider` here is temporary, + until `KibanaNoDataPage` is moved to a package of its own. + + Once `KibanaNoDataPage` is moved to a package, `NoDataViewsPromptProvider` will be *combined* + with `KibanaNoDataPageProvider`, creating a single Provider that manages contextual dependencies + throughout the React tree from the top-level of composition and consumption. + */ if (!hasUserDataViews) { - return ; + const services: NoDataViewsPromptServices = { + canCreateNewDataView, + dataViewsDocLink, + openDataViewEditor, + }; + + return ( + + + + ); } return null; diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.stories.tsx b/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.stories.tsx deleted file mode 100644 index bee7c87d2841b..0000000000000 --- a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.stories.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import React from 'react'; -import { action } from '@storybook/addon-actions'; - -import { servicesFactory } from '@kbn/shared-ux-storybook'; - -import { NoDataViews as NoDataViewsComponent, Props } from './no_data_views.component'; -import { NoDataViews } from './no_data_views'; - -import mdx from './no_data_views.mdx'; - -const services = servicesFactory({}); - -export default { - title: 'No Data/No Data Views', - description: 'A component to display when there are no user-created data views available.', - parameters: { - docs: { - page: mdx, - }, - }, -}; - -export const ConnectedComponent = () => { - return ; -}; - -type Params = Pick; - -export const PureComponent = (params: Params) => { - return ; -}; - -PureComponent.argTypes = { - canCreateNewDataView: { - control: 'boolean', - defaultValue: true, - }, - dataViewsDocLink: { - options: [services.docLinks.dataViewsDocLink, undefined], - control: { type: 'radio' }, - }, -}; diff --git a/packages/kbn-shared-ux-components/src/index.ts b/packages/kbn-shared-ux-components/src/index.ts index 77586e8592b6a..fb4676e9f4e55 100644 --- a/packages/kbn-shared-ux-components/src/index.ts +++ b/packages/kbn-shared-ux-components/src/index.ts @@ -90,44 +90,3 @@ export const KibanaPageTemplateSolutionNavLazy = React.lazy(() => default: KibanaPageTemplateSolutionNav, })) ); - -/** - * A `KibanaPageTemplateSolutionNav` component that is wrapped by the `withSuspense` HOC. This component can - * be used directly by consumers and will load the `KibanaPageTemplateSolutionNavLazy` component lazily with - * a predefined fallback and error boundary. - */ -export const KibanaPageTemplateSolutionNav = withSuspense(KibanaPageTemplateSolutionNavLazy); - -/** - * The Lazily-loaded `NoDataViews` component. Consumers should use `React.Suspennse` or the - * `withSuspense` HOC to load this component. - */ -export const NoDataViewsLazy = React.lazy(() => - import('./empty_state/no_data_views').then(({ NoDataViews }) => ({ - default: NoDataViews, - })) -); - -/** - * A `NoDataViews` component that is wrapped by the `withSuspense` HOC. This component can - * be used directly by consumers and will load the `LazyNoDataViews` component lazily with - * a predefined fallback and error boundary. - */ -export const NoDataViews = withSuspense(NoDataViewsLazy); - -/** - * A pure `NoDataViews` component, with no services hooks. Consumers should use `React.Suspennse` or the - * `withSuspense` HOC to load this component. - */ -export const NoDataViewsComponentLazy = React.lazy(() => - import('./empty_state/no_data_views').then(({ NoDataViewsComponent }) => ({ - default: NoDataViewsComponent, - })) -); - -/** - * A pure `NoDataViews` component, with no services hooks. The component is wrapped by the `withSuspense` HOC. - * This component can be used directly by consumers and will load the `LazyNoDataViewsComponent` lazily with - * a predefined fallback and error boundary. - */ -export const NoDataViewsComponent = withSuspense(NoDataViewsComponentLazy); diff --git a/packages/shared-ux/prompt/no_data_views/BUILD.bazel b/packages/shared-ux/prompt/no_data_views/BUILD.bazel new file mode 100644 index 0000000000000..91fae6aeddea9 --- /dev/null +++ b/packages/shared-ux/prompt/no_data_views/BUILD.bazel @@ -0,0 +1,142 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "no_data_views" +PKG_REQUIRE_NAME = "@kbn/shared-ux-prompt-no-data-views" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.mdx", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//@elastic/eui", + "@npm//@emotion/css", + "@npm//@emotion/react", + "@npm//@storybook/addon-actions", + "@npm//enzyme", + "@npm//react", + "//packages/kbn-i18n-react", + "//packages/kbn-i18n", + "//packages/kbn-shared-ux-utility", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@elastic/eui", + "@npm//@emotion/css", + "@npm//@emotion/react", + "@npm//@storybook/addon-actions", + "@npm//@types/enzyme", + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/react", + "//packages/kbn-ambient-ui-types", + "//packages/kbn-i18n-react:npm_module_types", + "//packages/kbn-i18n:npm_module_types", + "//packages/kbn-shared-ux-utility:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.mdx b/packages/shared-ux/prompt/no_data_views/README.mdx similarity index 74% rename from packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.mdx rename to packages/shared-ux/prompt/no_data_views/README.mdx index ef8812c565a9f..730470c72f170 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.mdx +++ b/packages/shared-ux/prompt/no_data_views/README.mdx @@ -1,7 +1,7 @@ -**id:** sharedUX/Components/NoDataViewsPage -**slug:** /shared-ux/components/no-data-views-page -**title:** No Data Views Page -**summary:** A page to be displayed when there is data in Elasticsearch, but no data views +**id:** sharedUX/Components/NoDataViewsPrompt +**slug:** /shared-ux/components/no-data-views +**title:** No Data Views +**summary:** A prompt to be displayed when there is data in Elasticsearch, but no data views **tags:** ['shared-ux', 'component'] **date:** 2022-02-09 diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/index.tsx b/packages/shared-ux/prompt/no_data_views/jest.config.js similarity index 72% rename from packages/kbn-shared-ux-components/src/empty_state/no_data_views/index.tsx rename to packages/shared-ux/prompt/no_data_views/jest.config.js index 6719fffa36740..a89d3ff222089 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/index.tsx +++ b/packages/shared-ux/prompt/no_data_views/jest.config.js @@ -6,5 +6,8 @@ * Side Public License, v 1. */ -export { NoDataViews } from './no_data_views'; -export { NoDataViews as NoDataViewsComponent } from './no_data_views.component'; +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/shared-ux/prompt/no_data_views'], +}; diff --git a/packages/shared-ux/prompt/no_data_views/package.json b/packages/shared-ux/prompt/no_data_views/package.json new file mode 100644 index 0000000000000..79070e1242994 --- /dev/null +++ b/packages/shared-ux/prompt/no_data_views/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/shared-ux-prompt-no-data-views", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/__snapshots__/documentation_link.test.tsx.snap b/packages/shared-ux/prompt/no_data_views/src/__snapshots__/documentation_link.test.tsx.snap similarity index 82% rename from packages/kbn-shared-ux-components/src/empty_state/no_data_views/__snapshots__/documentation_link.test.tsx.snap rename to packages/shared-ux/prompt/no_data_views/src/__snapshots__/documentation_link.test.tsx.snap index e84b997d8df87..0f7160c7b06e8 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/__snapshots__/documentation_link.test.tsx.snap +++ b/packages/shared-ux/prompt/no_data_views/src/__snapshots__/documentation_link.test.tsx.snap @@ -10,7 +10,7 @@ exports[` is rendered correctly 1`] = ` > @@ -26,7 +26,7 @@ exports[` is rendered correctly 1`] = ` > diff --git a/packages/shared-ux/prompt/no_data_views/src/data_view_illustration.tsx b/packages/shared-ux/prompt/no_data_views/src/data_view_illustration.tsx new file mode 100644 index 0000000000000..8a889a9267dee --- /dev/null +++ b/packages/shared-ux/prompt/no_data_views/src/data_view_illustration.tsx @@ -0,0 +1,552 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; + +export const DataViewIllustration = () => { + const { euiTheme } = useEuiTheme(); + const { colors } = euiTheme; + + const dataViewIllustrationVerticalStripes = css` + fill: ${colors.fullShade}; + `; + + const dataViewIllustrationDots = css` + fill: ${colors.lightShade}; + `; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/documentation_link.test.tsx b/packages/shared-ux/prompt/no_data_views/src/documentation_link.test.tsx similarity index 100% rename from packages/kbn-shared-ux-components/src/empty_state/no_data_views/documentation_link.test.tsx rename to packages/shared-ux/prompt/no_data_views/src/documentation_link.test.tsx diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/documentation_link.tsx b/packages/shared-ux/prompt/no_data_views/src/documentation_link.tsx similarity index 88% rename from packages/kbn-shared-ux-components/src/empty_state/no_data_views/documentation_link.tsx rename to packages/shared-ux/prompt/no_data_views/src/documentation_link.tsx index 3b3e742ea74ce..2b40f30acc779 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/documentation_link.tsx +++ b/packages/shared-ux/prompt/no_data_views/src/documentation_link.tsx @@ -20,7 +20,7 @@ export function DocumentationLink({ href }: Props) {
@@ -29,7 +29,7 @@ export function DocumentationLink({ href }: Props) {
diff --git a/packages/shared-ux/prompt/no_data_views/src/index.tsx b/packages/shared-ux/prompt/no_data_views/src/index.tsx new file mode 100644 index 0000000000000..23c2ed068f2af --- /dev/null +++ b/packages/shared-ux/prompt/no_data_views/src/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { withSuspense } from '@kbn/shared-ux-utility'; + +export { NoDataViewsPromptKibanaProvider, NoDataViewsPromptProvider } from './services'; +export type { NoDataViewsPromptKibanaServices, NoDataViewsPromptServices } from './services'; + +/** + * The Lazily-loaded `NoDataViewsPrompt` component. Consumers should use `React.Suspennse` or the + * `withSuspense` HOC to load this component. + */ +export const NoDataViewsPromptLazy = React.lazy(() => + import('./no_data_views').then(({ NoDataViewsPrompt }) => ({ + default: NoDataViewsPrompt, + })) +); + +/** + * A `NoDataViewsPrompt` component that is wrapped by the `withSuspense` HOC. This component can + * be used directly by consumers and will load the `NoDataViewsPromptLazy` component lazily with + * a predefined fallback and error boundary. + */ +export const NoDataViewsPrompt = withSuspense(NoDataViewsPromptLazy); + +/** + * A pure `NoDataViewsPrompt` component, with no services hooks. Consumers should use `React.Suspennse` or the + * `withSuspense` HOC to load this component. + */ +export const NoDataViewsPromptComponentLazy = React.lazy(() => + import('./no_data_views.component').then(({ NoDataViewsPrompt: Component }) => ({ + default: Component, + })) +); + +/** + * A pure `NoDataViewsPrompt` component, with no services hooks. The component is wrapped by the `withSuspense` HOC. + * This component can be used directly by consumers and will load the `NoDataViewsComponentLazy` lazily with + * a predefined fallback and error boundary. + */ +export const NoDataViewsPromptComponent = withSuspense(NoDataViewsPromptComponentLazy); diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.test.tsx b/packages/shared-ux/prompt/no_data_views/src/no_data_views.component.test.tsx similarity index 79% rename from packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.test.tsx rename to packages/shared-ux/prompt/no_data_views/src/no_data_views.component.test.tsx index 87dd68e202bc2..d0de72797cc2f 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.test.tsx +++ b/packages/shared-ux/prompt/no_data_views/src/no_data_views.component.test.tsx @@ -9,13 +9,13 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; -import { NoDataViews } from './no_data_views.component'; +import { NoDataViewsPrompt } from './no_data_views.component'; import { DocumentationLink } from './documentation_link'; -describe('', () => { +describe('', () => { test('is rendered correctly', () => { const component = mountWithIntl( - ', () => { }); test('does not render button if canCreateNewDataViews is false', () => { - const component = mountWithIntl(); + const component = mountWithIntl(); expect(component.find(EuiButton).length).toBe(0); }); test('does not documentation link if linkToDocumentation is not provided', () => { const component = mountWithIntl( - + ); expect(component.find(DocumentationLink).length).toBe(0); @@ -43,7 +43,7 @@ describe('', () => { test('onClickCreate', () => { const onClickCreate = jest.fn(); const component = mountWithIntl( - + ); component.find('button').simulate('click'); diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.tsx b/packages/shared-ux/prompt/no_data_views/src/no_data_views.component.tsx similarity index 77% rename from packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.tsx rename to packages/shared-ux/prompt/no_data_views/src/no_data_views.component.tsx index 3131b6ab2a73c..f53a187265703 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.tsx +++ b/packages/shared-ux/prompt/no_data_views/src/no_data_views.component.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, EuiEmptyPrompt, EuiEmptyPromptProps } from '@elastic/eui'; -import { DataViewIllustration } from '../assets'; +import { DataViewIllustration } from './data_view_illustration'; import { DocumentationLink } from './documentation_link'; export interface Props { @@ -23,7 +23,7 @@ export interface Props { emptyPromptColor?: EuiEmptyPromptProps['color']; } -const createDataViewText = i18n.translate('sharedUXComponents.noDataViewsPrompt.addDataViewText', { +const createDataViewText = i18n.translate('sharedUXPackages.noDataViewsPrompt.addDataViewText', { defaultMessage: 'Create data view', }); @@ -33,13 +33,13 @@ const MAX_WIDTH = 830; /** * A presentational component that is shown in cases when there are no data views created yet. */ -export const NoDataViews = ({ +export const NoDataViewsPrompt = ({ onClickCreate, canCreateNewDataView, dataViewsDocLink, emptyPromptColor = 'plain', }: Props) => { - const createNewButton = canCreateNewDataView && ( + const actions = canCreateNewDataView && (
) : (

@@ -74,19 +74,22 @@ export const NoDataViews = ({ const body = canCreateNewDataView ? (

) : (

); + const icon = ; + const footer = dataViewsDocLink ? : undefined; + return ( } - title={title} - body={body} - actions={createNewButton} - footer={dataViewsDocLink && } + {...{ actions, icon, title, body, footer }} /> ); }; diff --git a/packages/shared-ux/prompt/no_data_views/src/no_data_views.stories.tsx b/packages/shared-ux/prompt/no_data_views/src/no_data_views.stories.tsx new file mode 100644 index 0000000000000..c9e983c5f01b2 --- /dev/null +++ b/packages/shared-ux/prompt/no_data_views/src/no_data_views.stories.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import { NoDataViewsPrompt as NoDataViewsPromptComponent, Props } from './no_data_views.component'; +import { NoDataViewsPrompt } from './no_data_views'; +import { NoDataViewsPromptProvider, NoDataViewsPromptServices } from './services'; + +import mdx from '../README.mdx'; + +export default { + title: 'No Data/No Data Views', + description: 'A component to display when there are no user-created data views available.', + parameters: { + docs: { + page: mdx, + }, + }, +}; + +type ConnectedParams = Pick; + +const openDataViewEditor: NoDataViewsPromptServices['openDataViewEditor'] = (options) => { + action('openDataViewEditor')(options); + return () => {}; +}; + +export const ConnectedComponent = (params: ConnectedParams) => { + return ( + + + + ); +}; + +ConnectedComponent.argTypes = { + canCreateNewDataView: { + control: 'boolean', + defaultValue: true, + }, + dataViewsDocLink: { + options: ['some/link', undefined], + control: { type: 'radio' }, + }, +}; + +type PureParams = Pick; + +export const PureComponent = (params: PureParams) => { + return ; +}; + +PureComponent.argTypes = { + canCreateNewDataView: { + control: 'boolean', + defaultValue: true, + }, + dataViewsDocLink: { + options: ['some/link', undefined], + control: { type: 'radio' }, + }, +}; diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.test.tsx b/packages/shared-ux/prompt/no_data_views/src/no_data_views.test.tsx similarity index 53% rename from packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.test.tsx rename to packages/shared-ux/prompt/no_data_views/src/no_data_views.test.tsx index bb067544013c8..041e71d87e2ae 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.test.tsx +++ b/packages/shared-ux/prompt/no_data_views/src/no_data_views.test.tsx @@ -12,21 +12,23 @@ import { ReactWrapper } from 'enzyme'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { EuiButton } from '@elastic/eui'; -import { - SharedUxServicesProvider, - SharedUxServices, - mockServicesFactory, -} from '@kbn/shared-ux-services'; -import { NoDataViews } from './no_data_views'; - -describe('', () => { - let services: SharedUxServices; +import { NoDataViewsPrompt } from './no_data_views'; +import { NoDataViewsPromptServices, NoDataViewsPromptProvider } from './services'; + +const getServices = (canCreateNewDataView: boolean = true) => ({ + canCreateNewDataView, + openDataViewEditor: jest.fn(), + dataViewsDocLink: 'some/link', +}); + +describe('', () => { + let services: NoDataViewsPromptServices; let mount: (element: JSX.Element) => ReactWrapper; beforeEach(() => { - services = mockServicesFactory(); + services = getServices(); mount = (element: JSX.Element) => - mountWithIntl({element}); + mountWithIntl({element}); }); afterEach(() => { @@ -34,13 +36,13 @@ describe('', () => { }); test('on dataView created', () => { - const component = mount(); + const component = mount(); - expect(services.editors.openDataViewEditor).not.toHaveBeenCalled(); + expect(services.openDataViewEditor).not.toHaveBeenCalled(); component.find(EuiButton).simulate('click'); component.unmount(); - expect(services.editors.openDataViewEditor).toHaveBeenCalled(); + expect(services.openDataViewEditor).toHaveBeenCalled(); }); }); diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.tsx b/packages/shared-ux/prompt/no_data_views/src/no_data_views.tsx similarity index 72% rename from packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.tsx rename to packages/shared-ux/prompt/no_data_views/src/no_data_views.tsx index 8d0e6d93275e1..da618674810ce 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.tsx +++ b/packages/shared-ux/prompt/no_data_views/src/no_data_views.tsx @@ -8,20 +8,18 @@ import React, { useCallback, useEffect, useRef } from 'react'; -import { useEditors, usePermissions, useDocLinks } from '@kbn/shared-ux-services'; -import type { SharedUxEditorsService } from '@kbn/shared-ux-services'; - -import { NoDataViews as NoDataViewsComponent } from './no_data_views.component'; +import { NoDataViewsPrompt as NoDataViewsPromptComponent } from './no_data_views.component'; +import { useServices, NoDataViewsPromptServices } from './services'; // TODO: https://github.com/elastic/kibana/issues/127695 export interface Props { onDataViewCreated: (dataView: unknown) => void; } -type CloseDataViewEditorFn = ReturnType; +type CloseDataViewEditorFn = ReturnType; /** - * A service-enabled component that provides Kibana-specific functionality to the `NoDataViews` + * A service-enabled component that provides Kibana-specific functionality to the `NoDataViewsPrompt` * component. * * Use of this component requires both the `EuiTheme` context as well as either a configured Shared UX @@ -29,10 +27,8 @@ type CloseDataViewEditorFn = ReturnType { - const { canCreateNewDataView } = usePermissions(); - const { openDataViewEditor } = useEditors(); - const { dataViewsDocLink } = useDocLinks(); +export const NoDataViewsPrompt = ({ onDataViewCreated }: Props) => { + const { canCreateNewDataView, openDataViewEditor, dataViewsDocLink } = useServices(); const closeDataViewEditor = useRef(); useEffect(() => { @@ -69,5 +65,7 @@ export const NoDataViews = ({ onDataViewCreated }: Props) => { } }, [canCreateNewDataView, openDataViewEditor, setDataViewEditorRef, onDataViewCreated]); - return ; + return ( + + ); }; diff --git a/packages/shared-ux/prompt/no_data_views/src/services.tsx b/packages/shared-ux/prompt/no_data_views/src/services.tsx new file mode 100644 index 0000000000000..58d21d1845b56 --- /dev/null +++ b/packages/shared-ux/prompt/no_data_views/src/services.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC, useContext } from 'react'; + +/** + * TODO: `DataView` is a class exported by `src/plugins/data_views/public`. Since this service + * is contained in this package-- and packages can only depend on other packages and never on + * plugins-- we have to set this to `unknown`. If and when `DataView` is exported from a + * stateless package, we can remove this. + * + * @see: https://github.com/elastic/kibana/issues/127695 + */ +type DataView = unknown; + +/** + * A subset of the `DataViewEditorOptions` interface relevant to our service and components. + * + * @see: src/plugins/data_view_editor/public/types.ts + */ +interface DataViewEditorOptions { + /** Handler to be invoked when the Data View Editor completes a save operation. */ + onSave: (dataView: DataView) => void; + /** If set to false, will skip empty prompt in data view editor. */ + showEmptyPrompt?: boolean; +} + +/** + * Abstract external services for this component. + */ +export interface NoDataViewsPromptServices { + /** True if the user has permission to create a new Data View, false otherwise. */ + canCreateNewDataView: boolean; + /** A method to open the Data View Editor flow. */ + openDataViewEditor: (options: DataViewEditorOptions) => () => void; + /** A link to information about Data Views in Kibana */ + dataViewsDocLink: string; +} + +const NoDataViewsPromptContext = React.createContext(null); + +/** + * Abstract external service Provider. + */ +export const NoDataViewsPromptProvider: FC = ({ + children, + ...services +}) => { + return ( + + {children} + + ); +}; + +/** + * Kibana-specific service types. + */ +export interface NoDataViewsPromptKibanaServices { + coreStart: { + docLinks: { + links: { + indexPatterns: { + introduction: string; + }; + }; + }; + }; + dataViewEditor: { + userPermissions: { + editDataView: () => boolean; + }; + openEditor: (options: DataViewEditorOptions) => () => void; + }; +} + +/** + * Kibana-specific Provider that maps to known dependency types. + */ +export const NoDataViewsPromptKibanaProvider: FC = ({ + children, + ...services +}) => { + return ( + + {children} + + ); +}; + +/** + * React hook for accessing pre-wired services. + */ +export function useServices() { + const context = useContext(NoDataViewsPromptContext); + + if (!context) { + throw new Error( + 'NoDataViewsPromptContext is missing. Ensure your component or React root is wrapped with NoDataViewsPromptProvider.' + ); + } + + return context; +} diff --git a/packages/shared-ux/prompt/no_data_views/tsconfig.json b/packages/shared-ux/prompt/no_data_views/tsconfig.json new file mode 100644 index 0000000000000..45842fa3da472 --- /dev/null +++ b/packages/shared-ux/prompt/no_data_views/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node", + "react", + "@emotion/react/types/css-prop", + "@kbn/ambient-ui-types", + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx b/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx index ecfdd9e5c1c92..690bfa1f7acb8 100644 --- a/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx +++ b/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx @@ -9,9 +9,9 @@ import React, { useState, FC, useEffect } from 'react'; import useAsync from 'react-use/lib/useAsync'; -import { NoDataViewsComponent } from '@kbn/shared-ux-components'; import { EuiFlyoutBody } from '@elastic/eui'; import { DEFAULT_ASSETS_TO_IGNORE } from '@kbn/data-plugin/common'; +import { NoDataViewsPromptComponent } from '@kbn/shared-ux-prompt-no-data-views'; import { useKibana } from '../../shared_imports'; import { MatchedItem, DataViewEditorContext } from '../../types'; @@ -105,7 +105,7 @@ export const EmptyPrompts: FC = ({ return ( <> - setGoToForm(true)} canCreateNewDataView={application.capabilities.indexPatterns.save as boolean} dataViewsDocLink={docLinks.links.indexPatterns.introduction} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 70d3a81a2f808..f211cc9fede8e 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -5367,8 +5367,8 @@ "sharedUXComponents.noDataPage.elasticAgentCard.noPermission.description": "Cette intégration n'est pas encore activée. Votre administrateur possède les autorisations requises pour l’activer.", "sharedUXComponents.noDataPage.elasticAgentCard.noPermission.title": "Contactez votre administrateur", "sharedUXComponents.noDataPage.elasticAgentCard.title": "Ajouter Elastic Agent", - "sharedUXComponents.noDataViews.learnMore": "Envie d'en savoir plus ?", - "sharedUXComponents.noDataViews.readDocumentation": "Lisez les documents", + "sharedUXPackages.noDataViewsPrompt.learnMore": "Envie d'en savoir plus ?", + "sharedUXPackages.noDataViewsPrompt.readDocumentation": "Lisez les documents", "sharedUXComponents.pageTemplate.noDataCard.description": "Continuer sans collecter de données", "sharedUXComponents.toolbar.buttons.addFromLibrary.libraryButtonLabel": "Ajouter depuis la bibliothèque", "telemetry.callout.appliesSettingTitle": "Les modifications apportées à ce paramètre s'appliquent dans {allOfKibanaText} et sont enregistrées automatiquement.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a20feeeccdb1b..eec41bfb71c81 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5469,8 +5469,8 @@ "sharedUXComponents.noDataPage.elasticAgentCard.noPermission.description": "この統合はまだ有効ではありません。管理者にはオンにするために必要なアクセス権があります。", "sharedUXComponents.noDataPage.elasticAgentCard.noPermission.title": "管理者にお問い合わせください", "sharedUXComponents.noDataPage.elasticAgentCard.title": "Elasticエージェントの追加", - "sharedUXComponents.noDataViews.learnMore": "詳細について", - "sharedUXComponents.noDataViews.readDocumentation": "ドキュメントを読む", + "sharedUXPackages.noDataViewsPrompt.learnMore": "詳細について", + "sharedUXPackages.noDataViewsPrompt.readDocumentation": "ドキュメントを読む", "sharedUXComponents.pageTemplate.noDataCard.description": "データを収集せずに続行", "sharedUXComponents.toolbar.buttons.addFromLibrary.libraryButtonLabel": "ライブラリから追加", "telemetry.callout.appliesSettingTitle": "この設定に加えた変更は {allOfKibanaText} に適用され、自動的に保存されます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a2c33d9a1fae7..2d7566bdd8c87 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5480,8 +5480,8 @@ "sharedUXComponents.noDataPage.elasticAgentCard.noPermission.description": "尚未启用此集成。您的管理员具有打开它所需的权限。", "sharedUXComponents.noDataPage.elasticAgentCard.noPermission.title": "请联系您的管理员", "sharedUXComponents.noDataPage.elasticAgentCard.title": "添加 Elastic 代理", - "sharedUXComponents.noDataViews.learnMore": "希望了解详情?", - "sharedUXComponents.noDataViews.readDocumentation": "阅读文档", + "sharedUXPackages.noDataViewsPrompt.learnMore": "希望了解详情?", + "sharedUXPackages.noDataViewsPrompt.readDocumentation": "阅读文档", "sharedUXComponents.pageTemplate.noDataCard.description": "继续,而不收集数据", "sharedUXComponents.toolbar.buttons.addFromLibrary.libraryButtonLabel": "从库中添加", "telemetry.callout.appliesSettingTitle": "对此设置的更改将应用到{allOfKibanaText} 且会自动保存。", diff --git a/yarn.lock b/yarn.lock index 5225ebe505cbe..3668e805f67cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3216,6 +3216,11 @@ version "0.0.0" uid "" + +"@kbn/shared-ux-prompt-no-data-views@link:bazel-bin/packages/shared-ux/prompt/no_data_views": + version "0.0.0" + uid "" + "@kbn/shared-ux-services@link:bazel-bin/packages/kbn-shared-ux-services": version "0.0.0" uid "" @@ -6421,6 +6426,11 @@ version "0.0.0" uid "" + +"@types/kbn__shared-ux-prompt-no-data-views@link:bazel-bin/packages/shared-ux/prompt/no_data_views/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__shared-ux-services@link:bazel-bin/packages/kbn-shared-ux-services/npm_module_types": version "0.0.0" uid "" From 0c43f86470bb4cc52969e434103e654a854c2c57 Mon Sep 17 00:00:00 2001 From: nastasha-solomon <79124755+nastasha-solomon@users.noreply.github.com> Date: Thu, 19 May 2022 17:29:48 -0400 Subject: [PATCH 009/120] [DOCS] Remove note that pre-configured connectors are not supported on cases (#132186) --- docs/management/connectors/pre-configured-connectors.asciidoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/management/connectors/pre-configured-connectors.asciidoc b/docs/management/connectors/pre-configured-connectors.asciidoc index 27d1d80ea7305..7498784ef389e 100644 --- a/docs/management/connectors/pre-configured-connectors.asciidoc +++ b/docs/management/connectors/pre-configured-connectors.asciidoc @@ -12,8 +12,6 @@ action are predefined, including the connector name and ID. - Appear in all spaces because they are not saved objects. - Cannot be edited or deleted. -NOTE: Preconfigured connectors cannot be used with cases. - [float] [[preconfigured-connector-example]] ==== Preconfigured connectors example From efd30bc0077f98db0b162911c23fc703a1ad7880 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 May 2022 15:22:51 -0700 Subject: [PATCH 010/120] Update ftr (#132558) Co-authored-by: Renovate Bot --- package.json | 6 +++--- yarn.lock | 33 +++++++++++++++++++++------------ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 72f4acfc18354..9b01ec9decdcb 100644 --- a/package.json +++ b/package.json @@ -757,7 +757,7 @@ "@types/redux-logger": "^3.0.8", "@types/resolve": "^1.20.1", "@types/seedrandom": ">=2.0.0 <4.0.0", - "@types/selenium-webdriver": "^4.0.19", + "@types/selenium-webdriver": "^4.1.0", "@types/semver": "^7", "@types/set-value": "^2.0.0", "@types/sinon": "^7.0.13", @@ -812,7 +812,7 @@ "callsites": "^3.1.0", "chai": "3.5.0", "chance": "1.0.18", - "chromedriver": "^100.0.0", + "chromedriver": "^101.0.0", "clean-webpack-plugin": "^3.0.0", "cmd-shim": "^2.1.0", "compression-webpack-plugin": "^4.0.0", @@ -933,7 +933,7 @@ "resolve": "^1.22.0", "rxjs-marbles": "^5.0.6", "sass-loader": "^10.2.0", - "selenium-webdriver": "^4.1.1", + "selenium-webdriver": "^4.1.2", "shelljs": "^0.8.4", "simple-git": "1.116.0", "sinon": "^7.4.2", diff --git a/yarn.lock b/yarn.lock index 3668e805f67cb..88a23a226d0e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7112,10 +7112,12 @@ resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f" integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA== -"@types/selenium-webdriver@^4.0.19": - version "4.0.19" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.19.tgz#25699713552a63ee70215effdfd2e5d6dda19f8e" - integrity sha512-Irrh+iKc6Cxj6DwTupi4zgWhSBm1nK+JElOklIUiBVE6rcLYDtT1mwm9oFkHie485BQXNmZRoayjwxhowdInnA== +"@types/selenium-webdriver@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.0.tgz#b23ba7e7f4f59069529c57f0cbb7f5fba74affe7" + integrity sha512-ehqwZemosqiWVe+W0f5GqcLH7NgtjMBmcknmeaPG6YZHc7EZ69XbD7VVNZcT/L8lyMIL/KG99MsGcvDuFWo3Yw== + dependencies: + "@types/ws" "*" "@types/semver@^7": version "7.3.4" @@ -7387,6 +7389,13 @@ resolved "https://registry.yarnpkg.com/@types/write-pkg/-/write-pkg-3.1.0.tgz#f58767f4fb9a6a3ad8e95d3e9cd1f2d026ceab26" integrity sha512-JRGsPEPCrYqTXU0Cr+Yu7esPBE2yvH7ucOHr+JuBy0F59kglPvO5gkmtyEvf3P6dASSkScvy/XQ6SC1QEBFDuA== +"@types/ws@*": + version "8.5.3" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" + integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w== + dependencies: + "@types/node" "*" + "@types/xml-crypto@^1.4.2": version "1.4.2" resolved "https://registry.yarnpkg.com/@types/xml-crypto/-/xml-crypto-1.4.2.tgz#5ea7ef970f525ae8fe1e2ce0b3d40da1e3b279ae" @@ -10255,10 +10264,10 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^100.0.0: - version "100.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-100.0.0.tgz#1b4bf5c89cea12c79f53bc94d8f5bb5aa79ed7be" - integrity sha512-oLfB0IgFEGY9qYpFQO/BNSXbPw7bgfJUN5VX8Okps9W2qNT4IqKh5hDwKWtpUIQNI6K3ToWe2/J5NdpurTY02g== +chromedriver@^101.0.0: + version "101.0.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-101.0.0.tgz#ad19003008dd5df1770a1ad96059a9c5fe78e365" + integrity sha512-LkkWxy6KM/0YdJS8qBeg5vfkTZTRamhBfOttb4oic4echDgWvCU1E8QcBbUBOHqZpSrYMyi7WMKmKMhXFUaZ+w== dependencies: "@testim/chrome-version" "^1.1.2" axios "^0.24.0" @@ -25515,10 +25524,10 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -selenium-webdriver@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.1.1.tgz#da083177d811f36614950e809e2982570f67d02e" - integrity sha512-Fr9e9LC6zvD6/j7NO8M1M/NVxFX67abHcxDJoP5w2KN/Xb1SyYLjMVPGgD14U2TOiKe4XKHf42OmFw9g2JgCBQ== +selenium-webdriver@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.1.2.tgz#d463b4335632d2ea41a9e988e435a55dc41f5314" + integrity sha512-e4Ap8vQvhipgBB8Ry9zBiKGkU6kHKyNnWiavGGLKkrdW81Zv7NVMtFOL/j3yX0G8QScM7XIXijKssNd4EUxSOw== dependencies: jszip "^3.6.0" tmp "^0.2.1" From 1ea3fc6d32486656d8ed5e2f5e637e61baf24245 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Thu, 19 May 2022 18:00:14 -0500 Subject: [PATCH 011/120] [Security Solution] improve endpoint metadata tests (#125883) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../data_loaders/index_fleet_agent.ts | 2 +- .../services/endpoint.ts | 68 +++++++++++++++---- .../apis/endpoint_authz.ts | 9 --- .../apis/metadata.ts | 49 ++++++------- 4 files changed, 80 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts index b051eff37edc7..8719db5036b83 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts @@ -23,7 +23,7 @@ import { wrapErrorAndRejectPromise } from './utils'; const defaultFleetAgentGenerator = new FleetAgentGenerator(); export interface IndexedFleetAgentResponse { - agents: Agent[]; + agents: Array; fleetAgentsIndex: string; } diff --git a/x-pack/test/security_solution_endpoint/services/endpoint.ts b/x-pack/test/security_solution_endpoint/services/endpoint.ts index 27dcd67c6d684..d526c59ee6864 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint.ts @@ -11,6 +11,7 @@ import { metadataCurrentIndexPattern, metadataTransformPrefix, METADATA_UNITED_INDEX, + METADATA_UNITED_TRANSFORM, } from '@kbn/security-solution-plugin/common/endpoint/constants'; import { deleteIndexedHostsAndAlerts, @@ -77,6 +78,27 @@ export class EndpointTestResources extends FtrService { await this.transform.api.updateTransform(transform.id, { frequency }).catch(catchAndWrapError); } + private async stopTransform(transformId: string) { + const stopRequest = { + transform_id: `${transformId}*`, + force: true, + wait_for_completion: true, + allow_no_match: true, + }; + return this.esClient.transform.stopTransform(stopRequest); + } + + private async startTransform(transformId: string) { + const transformsResponse = await this.esClient.transform.getTransform({ + transform_id: `${transformId}*`, + }); + return Promise.all( + transformsResponse.transforms.map((transform) => { + return this.esClient.transform.startTransform({ transform_id: transform.id }); + }) + ); + } + /** * Loads endpoint host/alert/event data into elasticsearch * @param [options] @@ -86,6 +108,8 @@ export class EndpointTestResources extends FtrService { * @param [options.enableFleetIntegration=true] When set to `true`, Fleet data will also be loaded (ex. Integration Policies, Agent Policies, "fake" Agents) * @param [options.generatorSeed='seed`] The seed to be used by the data generator. Important in order to ensure the same data is generated on very run. * @param [options.waitUntilTransformed=true] If set to `true`, the data loading process will wait until the endpoint hosts metadata is processed by the transform + * @param [options.waitTimeout=60000] If waitUntilTransformed=true, number of ms to wait until timeout + * @param [options.customIndexFn] If provided, will use this function to generate and index data instead */ async loadEndpointData( options: Partial<{ @@ -95,6 +119,8 @@ export class EndpointTestResources extends FtrService { enableFleetIntegration: boolean; generatorSeed: string; waitUntilTransformed: boolean; + waitTimeout: number; + customIndexFn: () => Promise; }> = {} ): Promise { const { @@ -104,25 +130,39 @@ export class EndpointTestResources extends FtrService { enableFleetIntegration = true, generatorSeed = 'seed', waitUntilTransformed = true, + waitTimeout = 60000, + customIndexFn, } = options; + if (waitUntilTransformed) { + // need this before indexing docs so that the united transform doesn't + // create a checkpoint with a timestamp after the doc timestamps + await this.stopTransform(METADATA_UNITED_TRANSFORM); + } + // load data into the system - const indexedData = await indexHostsAndAlerts( - this.esClient as Client, - this.kbnClient, - generatorSeed, - numHosts, - numHostDocs, - 'metrics-endpoint.metadata-default', - 'metrics-endpoint.policy-default', - 'logs-endpoint.events.process-default', - 'logs-endpoint.alerts-default', - alertsPerHost, - enableFleetIntegration - ); + const indexedData = customIndexFn + ? await customIndexFn() + : await indexHostsAndAlerts( + this.esClient as Client, + this.kbnClient, + generatorSeed, + numHosts, + numHostDocs, + 'metrics-endpoint.metadata-default', + 'metrics-endpoint.policy-default', + 'logs-endpoint.events.process-default', + 'logs-endpoint.alerts-default', + alertsPerHost, + enableFleetIntegration + ); if (waitUntilTransformed) { - await this.waitForEndpoints(indexedData.hosts.map((host) => host.agent.id)); + const metadataIds = Array.from(new Set(indexedData.hosts.map((host) => host.agent.id))); + await this.waitForEndpoints(metadataIds, waitTimeout); + await this.startTransform(METADATA_UNITED_TRANSFORM); + const agentIds = Array.from(new Set(indexedData.agents.map((agent) => agent.agent!.id))); + await this.waitForUnitedEndpoints(agentIds, waitTimeout); } return indexedData; diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts index f560103c6c862..1a009aaef07ec 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; import { wrapErrorAndRejectPromise } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/utils'; import { FtrProviderContext } from '../ftr_provider_context'; import { @@ -15,23 +14,15 @@ import { } from '../../common/services/security_solution'; export default function ({ getService }: FtrProviderContext) { - const endpointTestResources = getService('endpointTestResources'); const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('When attempting to call an endpoint api with no authz', () => { - let loadedData: IndexedHostsAndAlertsResponse; - before(async () => { // create role/user await createUserAndRole(getService, ROLES.t1_analyst); - loadedData = await endpointTestResources.loadEndpointData(); }); after(async () => { - if (loadedData) { - await endpointTestResources.unloadEndpointData(loadedData); - } - // delete role/user await deleteUserAndRole(getService, ROLES.t1_analyst); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 9b023e6992385..047b21827c5c3 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -19,6 +19,8 @@ import { import { AGENTS_INDEX } from '@kbn/fleet-plugin/common'; import { indexFleetEndpointPolicy } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { TRANSFORM_STATES } from '@kbn/security-solution-plugin/common/constants'; +import type { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; + import { generateAgentDocs, generateMetadataDocs } from './metadata.fixtures'; import { deleteAllDocsFromMetadataCurrentIndex, @@ -47,38 +49,37 @@ export default function ({ getService }: FtrProviderContext) { const numberOfHostsInFixture = 2; before(async () => { - await stopTransform(getService, `${METADATA_UNITED_TRANSFORM}*`); await deleteAllDocsFromFleetAgents(getService); await deleteAllDocsFromMetadataDatastream(getService); await deleteAllDocsFromMetadataCurrentIndex(getService); await deleteAllDocsFromIndex(getService, METADATA_UNITED_INDEX); - // generate an endpoint policy and attach id to agents since - // metadata list api filters down to endpoint policies only - const policy = await indexFleetEndpointPolicy( - getService('kibanaServer'), - `Default ${uuid.v4()}`, - '1.1.1' - ); - const policyId = policy.integrationPolicies[0].policy_id; - const currentTime = new Date().getTime(); + const customIndexFn = async (): Promise => { + // generate an endpoint policy and attach id to agents since + // metadata list api filters down to endpoint policies only + const policy = await indexFleetEndpointPolicy( + getService('kibanaServer'), + `Default ${uuid.v4()}`, + '1.1.1' + ); + const policyId = policy.integrationPolicies[0].policy_id; + const currentTime = new Date().getTime(); - const agentDocs = generateAgentDocs(currentTime, policyId); + const agentDocs = generateAgentDocs(currentTime, policyId); + const metadataDocs = generateMetadataDocs(currentTime); - await Promise.all([ - bulkIndex(getService, AGENTS_INDEX, agentDocs), - bulkIndex(getService, METADATA_DATASTREAM, generateMetadataDocs(currentTime)), - ]); + await Promise.all([ + bulkIndex(getService, AGENTS_INDEX, agentDocs), + bulkIndex(getService, METADATA_DATASTREAM, metadataDocs), + ]); - await endpointTestResources.waitForEndpoints( - agentDocs.map((doc) => doc.agent.id), - 60000 - ); - await startTransform(getService, METADATA_UNITED_TRANSFORM); - await endpointTestResources.waitForUnitedEndpoints( - agentDocs.map((doc) => doc.agent.id), - 60000 - ); + return { + agents: agentDocs, + hosts: metadataDocs, + } as unknown as IndexedHostsAndAlertsResponse; + }; + + await endpointTestResources.loadEndpointData({ customIndexFn }); }); after(async () => { From cadd7b33b84d403c4dca2b2fb7c99aa78f505d17 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Thu, 19 May 2022 18:16:59 -0500 Subject: [PATCH 012/120] Adds example for how to change a field format (#132541) --- docs/api/data-views/update-fields.asciidoc | 50 +++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/api/data-views/update-fields.asciidoc b/docs/api/data-views/update-fields.asciidoc index 3ec4b7c84694a..c43daff187528 100644 --- a/docs/api/data-views/update-fields.asciidoc +++ b/docs/api/data-views/update-fields.asciidoc @@ -60,6 +60,53 @@ $ curl -X POST api/data_views/data-view/my-view/fields -------------------------------------------------- // KIBANA +Change a simple field format: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/data_views/data-view/my-view/fields +{ + "fields": { + "foo": { + "format": { + "id": "bytes" + } + } + } +} +-------------------------------------------------- +// KIBANA + +Change a complex field format: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/data_views/data-view/my-view/fields +{ + "fields": { + "foo": { + "format": { + "id": "static_lookup", + "params": { + "lookupEntries": [ + { + "key": "1", + "value": "100" + }, + { + "key": "2", + "value": "200" + } + ], + "unknownKeyValue": "5000" + } + } + } + } +} +-------------------------------------------------- +// KIBANA + Update multiple metadata fields in one request: [source,sh] @@ -80,6 +127,7 @@ $ curl -X POST api/data_views/data-view/my-view/fields // KIBANA Use `null` value to delete metadata: + [source,sh] -------------------------------------------------- $ curl -X POST api/data_views/data-view/my-pattern/fields @@ -93,8 +141,8 @@ $ curl -X POST api/data_views/data-view/my-pattern/fields -------------------------------------------------- // KIBANA - The endpoint returns the updated data view object: + [source,sh] -------------------------------------------------- { From 04f47dda7453fe8c02d8b7137d805f7d406e25a6 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 19 May 2022 20:19:00 -0400 Subject: [PATCH 013/120] Fix upgrade available overflow (#132555) --- .../fleet/sections/agents/agent_list_page/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index f12a99c6e37f9..223ff395eb444 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -400,12 +400,12 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }, { field: 'local_metadata.elastic.agent.version', - width: '120px', + width: '135px', name: i18n.translate('xpack.fleet.agentList.versionTitle', { defaultMessage: 'Version', }), render: (version: string, agent: Agent) => ( - + {safeMetadata(version)} From 419d4e2e5942c378045667e8675a20b9db0e19fc Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 20 May 2022 02:30:01 +0100 Subject: [PATCH 014/120] docs(NA): adds @kbn/test-subj-selector into ops devdocs (#132505) --- dev_docs/operations/operations_landing.mdx | 1 + nav-kibana-dev.docnav.json | 3 ++- packages/kbn-test-subj-selector/BUILD.bazel | 1 - packages/kbn-test-subj-selector/README.md | 3 --- packages/kbn-test-subj-selector/README.mdx | 10 ++++++++++ 5 files changed, 13 insertions(+), 5 deletions(-) delete mode 100755 packages/kbn-test-subj-selector/README.md create mode 100755 packages/kbn-test-subj-selector/README.mdx diff --git a/dev_docs/operations/operations_landing.mdx b/dev_docs/operations/operations_landing.mdx index cda44a96fe4dd..8a54ee0a90a43 100644 --- a/dev_docs/operations/operations_landing.mdx +++ b/dev_docs/operations/operations_landing.mdx @@ -45,5 +45,6 @@ layout: landing { pageId: "kibDevDocsOpsExpect" }, { pageId: "kibDevDocsOpsAmbientStorybookTypes" }, { pageId: "kibDevDocsOpsAmbientUiTypes"}, + { pageId: "kibDevDocsOpsTestSubjSelector"}, ]} /> \ No newline at end of file diff --git a/nav-kibana-dev.docnav.json b/nav-kibana-dev.docnav.json index 4704430ba94b6..4bd2349cb18d3 100644 --- a/nav-kibana-dev.docnav.json +++ b/nav-kibana-dev.docnav.json @@ -200,7 +200,8 @@ { "id": "kibDevDocsOpsJestSerializers" }, { "id": "kibDevDocsOpsExpect" }, { "id": "kibDevDocsOpsAmbientStorybookTypes" }, - { "id": "kibDevDocsOpsAmbientUiTypes" } + { "id": "kibDevDocsOpsAmbientUiTypes" }, + { "id": "kibDevDocsOpsTestSubjSelector"} ] } ] diff --git a/packages/kbn-test-subj-selector/BUILD.bazel b/packages/kbn-test-subj-selector/BUILD.bazel index f494b558ad5a6..cc3334650a5d9 100644 --- a/packages/kbn-test-subj-selector/BUILD.bazel +++ b/packages/kbn-test-subj-selector/BUILD.bazel @@ -18,7 +18,6 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "package.json", - "README.md", ] RUNTIME_DEPS = [] diff --git a/packages/kbn-test-subj-selector/README.md b/packages/kbn-test-subj-selector/README.md deleted file mode 100755 index 463d6c808e298..0000000000000 --- a/packages/kbn-test-subj-selector/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# test-subj-selector - -Convert a string from test subject syntax to css selectors. diff --git a/packages/kbn-test-subj-selector/README.mdx b/packages/kbn-test-subj-selector/README.mdx new file mode 100755 index 0000000000000..c924d15937129 --- /dev/null +++ b/packages/kbn-test-subj-selector/README.mdx @@ -0,0 +1,10 @@ +--- +id: kibDevDocsOpsTestSubjSelector +slug: /kibana-dev-docs/ops/test-subj-selector +title: "@kbn/test-subj-selector" +description: An utility package to quickly get css selectors from strings +date: 2022-05-19 +tags: ['kibana', 'dev', 'contributor', 'operations', 'test', 'subj', 'selector'] +--- + +Converts a string from a test subject syntax into a css selectors composed by `data-test-subj`. From 963b91d86b49327cf59397da31597f63f4f8fef7 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 20 May 2022 03:28:05 +0100 Subject: [PATCH 015/120] docs(NA): adds @kbn/babel-plugin-synthentic-packages into ops devdocs (#132512) * docs(NA): adds @kbn/babel-plugin-synthentic-packages into ops devdocs * chore(NA): update packages/kbn-babel-plugin-synthetic-packages/README.mdx Co-authored-by: Jonathan Budzenski Co-authored-by: Jonathan Budzenski --- dev_docs/operations/operations_landing.mdx | 1 + nav-kibana-dev.docnav.json | 3 ++- .../kbn-babel-plugin-synthetic-packages/README.mdx | 13 +++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 packages/kbn-babel-plugin-synthetic-packages/README.mdx diff --git a/dev_docs/operations/operations_landing.mdx b/dev_docs/operations/operations_landing.mdx index 8a54ee0a90a43..27bec68ac9014 100644 --- a/dev_docs/operations/operations_landing.mdx +++ b/dev_docs/operations/operations_landing.mdx @@ -25,6 +25,7 @@ layout: landing { pageId: "kibDevDocsOpsOptimizer" }, { pageId: "kibDevDocsOpsBabelPreset" }, { pageId: "kibDevDocsOpsTypeSummarizer" }, + { pageId: "kibDevDocsOpsBabelPluginSyntheticPackages"}, ]} /> diff --git a/nav-kibana-dev.docnav.json b/nav-kibana-dev.docnav.json index 4bd2349cb18d3..d182492c3da14 100644 --- a/nav-kibana-dev.docnav.json +++ b/nav-kibana-dev.docnav.json @@ -181,7 +181,8 @@ "items": [ { "id": "kibDevDocsOpsOptimizer" }, { "id": "kibDevDocsOpsBabelPreset" }, - { "id": "kibDevDocsOpsTypeSummarizer" } + { "id": "kibDevDocsOpsTypeSummarizer" }, + { "id": "kibDevDocsOpsBabelPluginSyntheticPackages"} ] }, { diff --git a/packages/kbn-babel-plugin-synthetic-packages/README.mdx b/packages/kbn-babel-plugin-synthetic-packages/README.mdx new file mode 100644 index 0000000000000..6f11e9cf2d6b9 --- /dev/null +++ b/packages/kbn-babel-plugin-synthetic-packages/README.mdx @@ -0,0 +1,13 @@ +--- +id: kibDevDocsOpsBabelPluginSyntheticPackages +slug: /kibana-dev-docs/ops/babel-plugin-synthetic-packages +title: "@kbn/babel-plugin-synthetic-packages" +description: A babel plugin that transforms our @kbn/{NAME} imports into paths +date: 2022-05-19 +tags: ['kibana', 'dev', 'contributor', 'operations', 'babel', 'plugin', 'synthetic', 'packages'] +--- + +When developing inside the Kibana repository importing a package from any other package is just easy as importing `@kbn/{package-name}`. +However not every package is a node_module yet and while that is something we are working on to accomplish we need a way to dealing with it for +now. Using this babel plugin is our transitory solution. It allows us to import from module ids and then transform it automatically back into +paths on the transpiled code without friction for our engineering teams. \ No newline at end of file From 753fd99d64d52a5bf836a05a4c3f077406720406 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 19 May 2022 23:56:33 -0500 Subject: [PATCH 016/120] add internal/search test for correct handling of 403 error (#132046) --- .../api_integration/apis/search/search.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/x-pack/test/api_integration/apis/search/search.ts b/x-pack/test/api_integration/apis/search/search.ts index e459616304843..e7dfbb52ec701 100644 --- a/x-pack/test/api_integration/apis/search/search.ts +++ b/x-pack/test/api_integration/apis/search/search.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import type { Context } from 'mocha'; +import { parse as parseCookie } from 'tough-cookie'; import { FtrProviderContext } from '../../ftr_provider_context'; import { verifyErrorResponse } from '../../../../../test/api_integration/apis/search/verify_error'; @@ -16,6 +17,8 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('es'); const log = getService('log'); const retry = getService('retry'); + const security = getService('security'); + const supertestNoAuth = getService('supertestWithoutAuth'); const shardDelayAgg = (delay: string) => ({ aggs: { @@ -266,6 +269,48 @@ export default function ({ getService }: FtrProviderContext) { verifyErrorResponse(resp.body, 400, 'parsing_exception', true); }); + + it('should return 403 for lack of privledges', async () => { + const username = 'no_access'; + const password = 't0pS3cr3t'; + + await security.user.create(username, { + password, + roles: ['test_shakespeare_reader'], + }); + + const loginResponse = await supertestNoAuth + .post('/internal/security/login') + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { username, password }, + }) + .expect(200); + + const sessionCookie = parseCookie(loginResponse.headers['set-cookie'][0]); + + await supertestNoAuth + .post(`/internal/search/ese`) + .set('kbn-xsrf', 'foo') + .set('Cookie', sessionCookie!.cookieString()) + .send({ + params: { + index: 'log*', + body: { + query: { + match_all: {}, + }, + }, + wait_for_completion_timeout: '10s', + }, + }) + .expect(403); + + await security.testUser.restoreDefaults(); + }); }); describe('rollup', () => { From 6bdef369052fc5040c5aaa5893a1b96f7e90550d Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Fri, 20 May 2022 10:23:19 +0300 Subject: [PATCH 017/120] Use warn instead of warning (#132516) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../saved_object_migrations.test.ts | 32 +++++++++---------- .../migrations/saved_object_migrations.ts | 4 +-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index d43d4c4cb2a38..53765ed69cdac 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -181,7 +181,7 @@ describe('Lens migrations', () => { }); describe('7.8.0 auto timestamp', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', @@ -533,7 +533,7 @@ describe('Lens migrations', () => { }); describe('7.11.0 remove suggested priority', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', @@ -618,7 +618,7 @@ describe('Lens migrations', () => { }); describe('7.12.0 restructure datatable state', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', id: 'mock-saved-object-id', @@ -691,7 +691,7 @@ describe('Lens migrations', () => { }); describe('7.13.0 rename operations for Formula', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', id: 'mocked-saved-object-id', @@ -869,7 +869,7 @@ describe('Lens migrations', () => { }); describe('7.14.0 remove time zone from date histogram', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', id: 'mocked-saved-object-id', @@ -961,7 +961,7 @@ describe('Lens migrations', () => { }); describe('7.15.0 add layer type information', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', id: 'mocked-saved-object-id', @@ -1143,7 +1143,7 @@ describe('Lens migrations', () => { }); describe('7.16.0 move reversed default palette to custom palette', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', id: 'mocked-saved-object-id', @@ -1417,7 +1417,7 @@ describe('Lens migrations', () => { }); describe('8.1.0 update filter reference schema', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', id: 'mocked-saved-object-id', @@ -1523,7 +1523,7 @@ describe('Lens migrations', () => { }); describe('8.1.0 rename records field', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', id: 'mocked-saved-object-id', @@ -1709,7 +1709,7 @@ describe('Lens migrations', () => { }); describe('8.1.0 add parentFormat to terms operation', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', id: 'mocked-saved-object-id', @@ -1785,7 +1785,7 @@ describe('Lens migrations', () => { describe('8.2.0', () => { describe('last_value columns', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', id: 'mocked-saved-object-id', @@ -1877,7 +1877,7 @@ describe('Lens migrations', () => { }); describe('rename fitRowToContent to new detailed rowHeight and rowHeightLines', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; function getExample(fitToContent: boolean) { return { type: 'lens', @@ -1996,7 +1996,7 @@ describe('Lens migrations', () => { }); describe('8.2.0 include empty rows for date histogram columns', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', id: 'mocked-saved-object-id', @@ -2067,7 +2067,7 @@ describe('Lens migrations', () => { }); describe('8.3.0 old metric visualization defaults', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', id: 'mocked-saved-object-id', @@ -2117,7 +2117,7 @@ describe('Lens migrations', () => { }); describe('8.3.0 - convert legend sizes to strings', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const migrate = migrations['8.3.0']; const autoLegendSize = 'auto'; @@ -2185,7 +2185,7 @@ describe('Lens migrations', () => { }); describe('8.3.0 valueLabels in XY', () => { - const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', id: 'mocked-saved-object-id', diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index 3870bab9fad65..e6daa2cb99439 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -198,7 +198,7 @@ const removeLensAutoDate: SavedObjectMigrationFn Date: Fri, 20 May 2022 11:18:17 +0300 Subject: [PATCH 018/120] [XY] Usable reference lines for `xyVis`. (#132192) * ReferenceLineLayer -> referenceLine. * Added the referenceLine and splitted the logic at ReferenceLineAnnotations. * Fixed formatters of referenceLines * Added referenceLines keys. * Added test for the referenceLine fn. * Added some tests for reference_lines. * Unified the two different approaches of referenceLines. * Fixed types at tests and limits. --- packages/kbn-optimizer/limits.yml | 2 +- .../expression_xy/common/constants.ts | 3 +- .../common_reference_line_layer_args.ts | 25 - .../extended_reference_line_layer.ts | 50 -- .../common/expression_functions/index.ts | 2 +- .../expression_functions/layered_xy_vis.ts | 9 +- .../reference_line.test.ts | 140 ++++ .../expression_functions/reference_line.ts | 114 +++ .../reference_line_layer.ts | 29 +- .../expression_functions/xy_vis.test.ts | 17 +- .../common/expression_functions/xy_vis.ts | 8 +- .../common/expression_functions/xy_vis_fn.ts | 8 +- .../common/helpers/layers.test.ts | 2 +- .../expression_xy/common/i18n/index.tsx | 14 +- .../expression_xy/common/index.ts | 1 - .../common/types/expression_functions.ts | 65 +- .../common/utils/log_datatables.ts | 13 +- .../public/components/annotations.tsx | 2 +- .../components/reference_lines.test.tsx | 369 ---------- .../public/components/reference_lines.tsx | 268 ------- .../components/reference_lines/index.ts | 10 + .../reference_lines/reference_line.tsx | 56 ++ .../reference_line_annotations.tsx | 137 ++++ .../reference_lines/reference_line_layer.tsx | 92 +++ .../reference_lines.scss | 0 .../reference_lines/reference_lines.test.tsx | 683 ++++++++++++++++++ .../reference_lines/reference_lines.tsx | 79 ++ .../components/reference_lines/utils.tsx | 143 ++++ .../public/components/xy_chart.tsx | 28 +- .../expression_xy/public/helpers/layers.ts | 6 +- .../expression_xy/public/helpers/state.ts | 8 +- .../public/helpers/visualization.ts | 28 +- .../expression_xy/public/plugin.ts | 4 +- .../expression_xy/server/plugin.ts | 6 +- .../public/xy_visualization/to_expression.ts | 2 +- 35 files changed, 1615 insertions(+), 808 deletions(-) delete mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts delete mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.ts delete mode 100644 src/plugins/chart_expressions/expression_xy/public/components/reference_lines.test.tsx delete mode 100644 src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/reference_lines/index.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_annotations.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_layer.tsx rename src/plugins/chart_expressions/expression_xy/public/components/{ => reference_lines}/reference_lines.scss (100%) create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.test.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 8856f7f0aaabb..97e9f23784f60 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -128,5 +128,5 @@ pageLoadAssetSize: eventAnnotation: 19334 screenshotting: 22870 synthetics: 40958 - expressionXY: 31000 + expressionXY: 33000 kibanaUsageCollection: 16463 diff --git a/src/plugins/chart_expressions/expression_xy/common/constants.ts b/src/plugins/chart_expressions/expression_xy/common/constants.ts index 68ac2963c9646..fc2e41700b94f 100644 --- a/src/plugins/chart_expressions/expression_xy/common/constants.ts +++ b/src/plugins/chart_expressions/expression_xy/common/constants.ts @@ -9,6 +9,7 @@ export const XY_VIS = 'xyVis'; export const LAYERED_XY_VIS = 'layeredXyVis'; export const Y_CONFIG = 'yConfig'; +export const REFERENCE_LINE_Y_CONFIG = 'referenceLineYConfig'; export const EXTENDED_Y_CONFIG = 'extendedYConfig'; export const DATA_LAYER = 'dataLayer'; export const EXTENDED_DATA_LAYER = 'extendedDataLayer'; @@ -19,8 +20,8 @@ export const ANNOTATION_LAYER = 'annotationLayer'; export const EXTENDED_ANNOTATION_LAYER = 'extendedAnnotationLayer'; export const TICK_LABELS_CONFIG = 'tickLabelsConfig'; export const AXIS_EXTENT_CONFIG = 'axisExtentConfig'; +export const REFERENCE_LINE = 'referenceLine'; export const REFERENCE_LINE_LAYER = 'referenceLineLayer'; -export const EXTENDED_REFERENCE_LINE_LAYER = 'extendedReferenceLineLayer'; export const LABELS_ORIENTATION_CONFIG = 'labelsOrientationConfig'; export const AXIS_TITLES_VISIBILITY_CONFIG = 'axisTitlesVisibilityConfig'; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts deleted file mode 100644 index d85f5ae2b2f77..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EXTENDED_Y_CONFIG } from '../constants'; -import { strings } from '../i18n'; -import { ReferenceLineLayerFn, ExtendedReferenceLineLayerFn } from '../types'; - -type CommonReferenceLineLayerFn = ReferenceLineLayerFn | ExtendedReferenceLineLayerFn; - -export const commonReferenceLineLayerArgs: Omit = { - yConfig: { - types: [EXTENDED_Y_CONFIG], - help: strings.getRLYConfigHelp(), - multi: true, - }, - columnToLabel: { - types: ['string'], - help: strings.getColumnToLabelHelp(), - }, -}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts deleted file mode 100644 index 41b264cf53a4d..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { validateAccessor } from '@kbn/visualizations-plugin/common/utils'; -import { LayerTypes, EXTENDED_REFERENCE_LINE_LAYER } from '../constants'; -import { ExtendedReferenceLineLayerFn } from '../types'; -import { strings } from '../i18n'; -import { commonReferenceLineLayerArgs } from './common_reference_line_layer_args'; - -export const extendedReferenceLineLayerFunction: ExtendedReferenceLineLayerFn = { - name: EXTENDED_REFERENCE_LINE_LAYER, - aliases: [], - type: EXTENDED_REFERENCE_LINE_LAYER, - help: strings.getRLHelp(), - inputTypes: ['datatable'], - args: { - ...commonReferenceLineLayerArgs, - accessors: { - types: ['string'], - help: strings.getRLAccessorsHelp(), - multi: true, - }, - table: { - types: ['datatable'], - help: strings.getTableHelp(), - }, - layerId: { - types: ['string'], - help: strings.getLayerIdHelp(), - }, - }, - fn(input, args) { - const table = args.table ?? input; - const accessors = args.accessors ?? []; - accessors.forEach((accessor) => validateAccessor(accessor, table.columns)); - - return { - type: EXTENDED_REFERENCE_LINE_LAYER, - ...args, - accessors: args.accessors ?? [], - layerType: LayerTypes.REFERENCELINE, - table, - }; - }, -}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts index 30a76217b5c0e..dc82220db6e23 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts @@ -18,6 +18,6 @@ export * from './grid_lines_config'; export * from './axis_extent_config'; export * from './tick_labels_config'; export * from './labels_orientation_config'; +export * from './reference_line'; export * from './reference_line_layer'; -export * from './extended_reference_line_layer'; export * from './axis_titles_visibility_config'; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts index 695bd16613715..f419891e079ea 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts @@ -6,10 +6,11 @@ * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; import { LayeredXyVisFn } from '../types'; import { EXTENDED_DATA_LAYER, - EXTENDED_REFERENCE_LINE_LAYER, + REFERENCE_LINE_LAYER, LAYERED_XY_VIS, EXTENDED_ANNOTATION_LAYER, } from '../constants'; @@ -24,8 +25,10 @@ export const layeredXyVisFunction: LayeredXyVisFn = { args: { ...commonXYArgs, layers: { - types: [EXTENDED_DATA_LAYER, EXTENDED_REFERENCE_LINE_LAYER, EXTENDED_ANNOTATION_LAYER], - help: strings.getLayersHelp(), + types: [EXTENDED_DATA_LAYER, REFERENCE_LINE_LAYER, EXTENDED_ANNOTATION_LAYER], + help: i18n.translate('expressionXY.layeredXyVis.layers.help', { + defaultMessage: 'Layers of visual series', + }), multi: true, }, }, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts new file mode 100644 index 0000000000000..b96f39923fab2 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks'; +import { ReferenceLineArgs, ReferenceLineConfigResult } from '../types'; +import { referenceLineFunction } from './reference_line'; + +describe('referenceLine', () => { + test('produces the correct arguments for minimum arguments', async () => { + const args: ReferenceLineArgs = { + value: 100, + }; + + const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); + + const expectedResult: ReferenceLineConfigResult = { + type: 'referenceLine', + layerType: 'referenceLine', + lineLength: 0, + yConfig: [ + { + type: 'referenceLineYConfig', + ...args, + textVisibility: false, + }, + ], + }; + expect(result).toEqual(expectedResult); + }); + + test('produces the correct arguments for maximum arguments', async () => { + const args: ReferenceLineArgs = { + name: 'some value', + value: 100, + icon: 'alert', + iconPosition: 'below', + axisMode: 'bottom', + lineStyle: 'solid', + lineWidth: 10, + color: '#fff', + fill: 'below', + textVisibility: true, + }; + + const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); + + const expectedResult: ReferenceLineConfigResult = { + type: 'referenceLine', + layerType: 'referenceLine', + lineLength: 0, + yConfig: [ + { + type: 'referenceLineYConfig', + ...args, + }, + ], + }; + expect(result).toEqual(expectedResult); + }); + + test('adds text visibility if name is provided ', async () => { + const args: ReferenceLineArgs = { + name: 'some name', + value: 100, + }; + + const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); + + const expectedResult: ReferenceLineConfigResult = { + type: 'referenceLine', + layerType: 'referenceLine', + lineLength: 0, + yConfig: [ + { + type: 'referenceLineYConfig', + ...args, + textVisibility: true, + }, + ], + }; + expect(result).toEqual(expectedResult); + }); + + test('hides text if textVisibility is true and no text is provided', async () => { + const args: ReferenceLineArgs = { + value: 100, + textVisibility: true, + }; + + const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); + + const expectedResult: ReferenceLineConfigResult = { + type: 'referenceLine', + layerType: 'referenceLine', + lineLength: 0, + yConfig: [ + { + type: 'referenceLineYConfig', + ...args, + textVisibility: false, + }, + ], + }; + expect(result).toEqual(expectedResult); + }); + + test('applies text visibility if name is provided', async () => { + const checktextVisibility = (textVisibility: boolean = false) => { + const args: ReferenceLineArgs = { + value: 100, + name: 'some text', + textVisibility, + }; + + const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); + + const expectedResult: ReferenceLineConfigResult = { + type: 'referenceLine', + layerType: 'referenceLine', + lineLength: 0, + yConfig: [ + { + type: 'referenceLineYConfig', + ...args, + textVisibility, + }, + ], + }; + expect(result).toEqual(expectedResult); + }; + + checktextVisibility(); + checktextVisibility(true); + }); +}); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.ts new file mode 100644 index 0000000000000..c294d6ca5aaec --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { + AvailableReferenceLineIcons, + FillStyles, + IconPositions, + LayerTypes, + LineStyles, + REFERENCE_LINE, + REFERENCE_LINE_Y_CONFIG, + YAxisModes, +} from '../constants'; +import { ReferenceLineFn } from '../types'; +import { strings } from '../i18n'; + +export const referenceLineFunction: ReferenceLineFn = { + name: REFERENCE_LINE, + aliases: [], + type: REFERENCE_LINE, + help: strings.getRLHelp(), + inputTypes: ['datatable', 'null'], + args: { + name: { + types: ['string'], + help: strings.getReferenceLineNameHelp(), + }, + value: { + types: ['number'], + help: strings.getReferenceLineValueHelp(), + required: true, + }, + axisMode: { + types: ['string'], + options: [...Object.values(YAxisModes)], + help: strings.getAxisModeHelp(), + default: YAxisModes.AUTO, + strict: true, + }, + color: { + types: ['string'], + help: strings.getColorHelp(), + }, + lineStyle: { + types: ['string'], + options: [...Object.values(LineStyles)], + help: i18n.translate('expressionXY.yConfig.lineStyle.help', { + defaultMessage: 'The style of the reference line', + }), + default: LineStyles.SOLID, + strict: true, + }, + lineWidth: { + types: ['number'], + help: i18n.translate('expressionXY.yConfig.lineWidth.help', { + defaultMessage: 'The width of the reference line', + }), + default: 1, + }, + icon: { + types: ['string'], + help: i18n.translate('expressionXY.yConfig.icon.help', { + defaultMessage: 'An optional icon used for reference lines', + }), + options: [...Object.values(AvailableReferenceLineIcons)], + strict: true, + }, + iconPosition: { + types: ['string'], + options: [...Object.values(IconPositions)], + help: i18n.translate('expressionXY.yConfig.iconPosition.help', { + defaultMessage: 'The placement of the icon for the reference line', + }), + default: IconPositions.AUTO, + strict: true, + }, + textVisibility: { + types: ['boolean'], + help: i18n.translate('expressionXY.yConfig.textVisibility.help', { + defaultMessage: 'Visibility of the label on the reference line', + }), + }, + fill: { + types: ['string'], + options: [...Object.values(FillStyles)], + help: i18n.translate('expressionXY.yConfig.fill.help', { + defaultMessage: 'Fill', + }), + default: FillStyles.NONE, + strict: true, + }, + }, + fn(table, args) { + const textVisibility = + args.name !== undefined && args.textVisibility === undefined + ? true + : args.name === undefined + ? false + : args.textVisibility; + + return { + type: REFERENCE_LINE, + layerType: LayerTypes.REFERENCELINE, + lineLength: table?.rows.length ?? 0, + yConfig: [{ ...args, textVisibility, type: REFERENCE_LINE_Y_CONFIG }], + }; + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts index 04c06f92d616f..6b51edd2d209e 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts @@ -7,10 +7,9 @@ */ import { validateAccessor } from '@kbn/visualizations-plugin/common/utils'; -import { LayerTypes, REFERENCE_LINE_LAYER } from '../constants'; +import { LayerTypes, REFERENCE_LINE_LAYER, EXTENDED_Y_CONFIG } from '../constants'; import { ReferenceLineLayerFn } from '../types'; import { strings } from '../i18n'; -import { commonReferenceLineLayerArgs } from './common_reference_line_layer_args'; export const referenceLineLayerFunction: ReferenceLineLayerFn = { name: REFERENCE_LINE_LAYER, @@ -19,14 +18,31 @@ export const referenceLineLayerFunction: ReferenceLineLayerFn = { help: strings.getRLHelp(), inputTypes: ['datatable'], args: { - ...commonReferenceLineLayerArgs, accessors: { - types: ['string', 'vis_dimension'], + types: ['string'], help: strings.getRLAccessorsHelp(), multi: true, }, + yConfig: { + types: [EXTENDED_Y_CONFIG], + help: strings.getRLYConfigHelp(), + multi: true, + }, + columnToLabel: { + types: ['string'], + help: strings.getColumnToLabelHelp(), + }, + table: { + types: ['datatable'], + help: strings.getTableHelp(), + }, + layerId: { + types: ['string'], + help: strings.getLayerIdHelp(), + }, }, - fn(table, args) { + fn(input, args) { + const table = args.table ?? input; const accessors = args.accessors ?? []; accessors.forEach((accessor) => validateAccessor(accessor, table.columns)); @@ -34,8 +50,7 @@ export const referenceLineLayerFunction: ReferenceLineLayerFn = { type: REFERENCE_LINE_LAYER, ...args, layerType: LayerTypes.REFERENCELINE, - accessors, - table, + table: args.table ?? input, }; }, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts index 8ec1961416638..73d4444217d90 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts @@ -30,11 +30,12 @@ describe('xyVis', () => { } ), } as Datatable; + const { layers, ...rest } = args; const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer; const result = await xyVisFunction.fn( newData, - { ...rest, ...restLayerArgs, referenceLineLayers: [], annotationLayers: [] }, + { ...rest, ...restLayerArgs, referenceLines: [], annotationLayers: [] }, createMockExecutionContext() ); @@ -60,7 +61,7 @@ describe('xyVis', () => { ...rest, ...{ ...sampleLayer, markSizeAccessor: 'b' }, markSizeRatio: 0, - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], }, createMockExecutionContext() @@ -74,7 +75,7 @@ describe('xyVis', () => { ...rest, ...{ ...sampleLayer, markSizeAccessor: 'b' }, markSizeRatio: 101, - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], }, createMockExecutionContext() @@ -92,7 +93,7 @@ describe('xyVis', () => { ...rest, ...restLayerArgs, minTimeBarInterval: '1q', - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], }, createMockExecutionContext() @@ -111,7 +112,7 @@ describe('xyVis', () => { ...rest, ...restLayerArgs, minTimeBarInterval: '1h', - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], }, createMockExecutionContext() @@ -131,7 +132,7 @@ describe('xyVis', () => { { ...rest, ...restLayerArgs, - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], splitRowAccessor, }, @@ -152,7 +153,7 @@ describe('xyVis', () => { { ...rest, ...restLayerArgs, - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], splitColumnAccessor, }, @@ -172,7 +173,7 @@ describe('xyVis', () => { { ...rest, ...restLayerArgs, - referenceLineLayers: [], + referenceLines: [], annotationLayers: [], markSizeRatio: 5, }, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts index 37baf028178cc..7d2783cf6f1cd 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts @@ -7,7 +7,7 @@ */ import { XyVisFn } from '../types'; -import { XY_VIS, REFERENCE_LINE_LAYER, ANNOTATION_LAYER } from '../constants'; +import { XY_VIS, REFERENCE_LINE, ANNOTATION_LAYER } from '../constants'; import { strings } from '../i18n'; import { commonXYArgs } from './common_xy_args'; import { commonDataLayerArgs } from './common_data_layer_args'; @@ -33,9 +33,9 @@ export const xyVisFunction: XyVisFn = { help: strings.getAccessorsHelp(), multi: true, }, - referenceLineLayers: { - types: [REFERENCE_LINE_LAYER], - help: strings.getReferenceLineLayerHelp(), + referenceLines: { + types: [REFERENCE_LINE], + help: strings.getReferenceLinesHelp(), multi: true, }, annotationLayers: { diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index e879f33b76548..3de2dd35831e4 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -13,7 +13,7 @@ import { } from '@kbn/visualizations-plugin/common/utils'; import type { Datatable } from '@kbn/expressions-plugin/common'; import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; -import { LayerTypes, XY_VIS_RENDERER, DATA_LAYER } from '../constants'; +import { LayerTypes, XY_VIS_RENDERER, DATA_LAYER, REFERENCE_LINE } from '../constants'; import { appendLayerIds, getAccessors, normalizeTable } from '../helpers'; import { DataLayerConfigResult, XYLayerConfig, XyVisFn, XYArgs } from '../types'; import { getLayerDimensions } from '../utils'; @@ -53,7 +53,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { validateAccessor(args.splitColumnAccessor, data.columns); const { - referenceLineLayers = [], + referenceLines = [], annotationLayers = [], // data_layer args seriesType, @@ -81,7 +81,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { const layers: XYLayerConfig[] = [ ...appendLayerIds(dataLayers, 'dataLayers'), - ...appendLayerIds(referenceLineLayers, 'referenceLineLayers'), + ...appendLayerIds(referenceLines, 'referenceLines'), ...appendLayerIds(annotationLayers, 'annotationLayers'), ]; @@ -90,7 +90,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { handlers.inspectorAdapters.tables.allowCsvExport = true; const layerDimensions = layers.reduce((dimensions, layer) => { - if (layer.layerType === LayerTypes.ANNOTATIONS) { + if (layer.layerType === LayerTypes.ANNOTATIONS || layer.type === REFERENCE_LINE) { return dimensions; } diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts index a3eea973fbf91..895abdb7a60df 100644 --- a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts @@ -63,7 +63,7 @@ describe('#getDataLayers', () => { palette: { type: 'system_palette', name: 'system' }, }, { - type: 'extendedReferenceLineLayer', + type: 'referenceLineLayer', layerType: 'referenceLine', accessors: ['y'], table: { rows: [], columns: [], type: 'datatable' }, diff --git a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx index f3425ec2db625..ba26bb973f64f 100644 --- a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx @@ -93,9 +93,9 @@ export const strings = { i18n.translate('expressionXY.xyVis.dataLayer.help', { defaultMessage: 'Data layer of visual series', }), - getReferenceLineLayerHelp: () => - i18n.translate('expressionXY.xyVis.referenceLineLayer.help', { - defaultMessage: 'Reference line layer', + getReferenceLinesHelp: () => + i18n.translate('expressionXY.xyVis.referenceLines.help', { + defaultMessage: 'Reference line', }), getAnnotationLayerHelp: () => i18n.translate('expressionXY.xyVis.annotationLayer.help', { @@ -237,4 +237,12 @@ export const strings = { i18n.translate('expressionXY.annotationLayer.annotations.help', { defaultMessage: 'Annotations', }), + getReferenceLineNameHelp: () => + i18n.translate('expressionXY.referenceLine.name.help', { + defaultMessage: 'Reference line name', + }), + getReferenceLineValueHelp: () => + i18n.translate('expressionXY.referenceLine.Value.help', { + defaultMessage: 'Reference line value', + }), }; diff --git a/src/plugins/chart_expressions/expression_xy/common/index.ts b/src/plugins/chart_expressions/expression_xy/common/index.ts index 7211a7a7db1b7..005f6c2867c18 100755 --- a/src/plugins/chart_expressions/expression_xy/common/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/index.ts @@ -58,6 +58,5 @@ export type { ReferenceLineLayerConfigResult, CommonXYReferenceLineLayerConfig, AxisTitlesVisibilityConfigResult, - ExtendedReferenceLineLayerConfigResult, CommonXYReferenceLineLayerConfigResult, } from './types'; diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index 0e10f680811ec..0a7b93c495c29 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -26,7 +26,7 @@ import { XYCurveTypes, YAxisModes, YScaleTypes, - REFERENCE_LINE_LAYER, + REFERENCE_LINE, Y_CONFIG, AXIS_TITLES_VISIBILITY_CONFIG, LABELS_ORIENTATION_CONFIG, @@ -36,7 +36,7 @@ import { DATA_LAYER, AXIS_EXTENT_CONFIG, EXTENDED_DATA_LAYER, - EXTENDED_REFERENCE_LINE_LAYER, + REFERENCE_LINE_LAYER, ANNOTATION_LAYER, EndValues, EXTENDED_Y_CONFIG, @@ -44,6 +44,7 @@ import { XY_VIS, LAYERED_XY_VIS, EXTENDED_ANNOTATION_LAYER, + REFERENCE_LINE_Y_CONFIG, } from '../constants'; import { XYRender } from './expression_renderers'; @@ -194,7 +195,7 @@ export interface XYArgs extends DataLayerArgs { endValue?: EndValue; emphasizeFitting?: boolean; valueLabels: ValueLabelMode; - referenceLineLayers: ReferenceLineLayerConfigResult[]; + referenceLines: ReferenceLineConfigResult[]; annotationLayers: AnnotationLayerConfigResult[]; fittingFunction?: FittingFunction; axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult; @@ -287,13 +288,12 @@ export type ExtendedAnnotationLayerConfigResult = ExtendedAnnotationLayerArgs & layerType: typeof LayerTypes.ANNOTATIONS; }; -export interface ReferenceLineLayerArgs { - accessors: Array; - columnToLabel?: string; - yConfig?: ExtendedYConfigResult[]; +export interface ReferenceLineArgs extends Omit { + name?: string; + value: number; } -export interface ExtendedReferenceLineLayerArgs { +export interface ReferenceLineLayerArgs { layerId?: string; accessors: string[]; columnToLabel?: string; @@ -301,26 +301,31 @@ export interface ExtendedReferenceLineLayerArgs { table?: Datatable; } -export type XYLayerArgs = DataLayerArgs | ReferenceLineLayerArgs | AnnotationLayerArgs; -export type XYLayerConfig = DataLayerConfig | ReferenceLineLayerConfig | AnnotationLayerConfig; +export type XYLayerArgs = DataLayerArgs | ReferenceLineArgs | AnnotationLayerArgs; +export type XYLayerConfig = DataLayerConfig | ReferenceLineConfig | AnnotationLayerConfig; export type XYExtendedLayerConfig = | ExtendedDataLayerConfig - | ExtendedReferenceLineLayerConfig + | ReferenceLineLayerConfig | ExtendedAnnotationLayerConfig; export type XYExtendedLayerConfigResult = | ExtendedDataLayerConfigResult - | ExtendedReferenceLineLayerConfigResult + | ReferenceLineLayerConfigResult | ExtendedAnnotationLayerConfigResult; -export type ReferenceLineLayerConfigResult = ReferenceLineLayerArgs & { - type: typeof REFERENCE_LINE_LAYER; +export interface ReferenceLineYConfig extends ReferenceLineArgs { + type: typeof REFERENCE_LINE_Y_CONFIG; +} + +export interface ReferenceLineConfigResult { + type: typeof REFERENCE_LINE; layerType: typeof LayerTypes.REFERENCELINE; - table: Datatable; -}; + lineLength: number; + yConfig: [ReferenceLineYConfig]; +} -export type ExtendedReferenceLineLayerConfigResult = ExtendedReferenceLineLayerArgs & { - type: typeof EXTENDED_REFERENCE_LINE_LAYER; +export type ReferenceLineLayerConfigResult = ReferenceLineLayerArgs & { + type: typeof REFERENCE_LINE_LAYER; layerType: typeof LayerTypes.REFERENCELINE; table: Datatable; }; @@ -337,11 +342,11 @@ export interface WithLayerId { } export type DataLayerConfig = DataLayerConfigResult & WithLayerId; -export type ReferenceLineLayerConfig = ReferenceLineLayerConfigResult & WithLayerId; +export type ReferenceLineConfig = ReferenceLineConfigResult & WithLayerId; export type AnnotationLayerConfig = AnnotationLayerConfigResult & WithLayerId; export type ExtendedDataLayerConfig = ExtendedDataLayerConfigResult & WithLayerId; -export type ExtendedReferenceLineLayerConfig = ExtendedReferenceLineLayerConfigResult & WithLayerId; +export type ReferenceLineLayerConfig = ReferenceLineLayerConfigResult & WithLayerId; export type ExtendedAnnotationLayerConfig = ExtendedAnnotationLayerConfigResult & WithLayerId; export type ExtendedDataLayerConfigResult = Omit & { @@ -370,13 +375,11 @@ export type TickLabelsConfigResult = AxesSettingsConfig & { type: typeof TICK_LA export type CommonXYLayerConfig = XYLayerConfig | XYExtendedLayerConfig; export type CommonXYDataLayerConfigResult = DataLayerConfigResult | ExtendedDataLayerConfigResult; export type CommonXYReferenceLineLayerConfigResult = - | ReferenceLineLayerConfigResult - | ExtendedReferenceLineLayerConfigResult; + | ReferenceLineConfigResult + | ReferenceLineLayerConfigResult; export type CommonXYDataLayerConfig = DataLayerConfig | ExtendedDataLayerConfig; -export type CommonXYReferenceLineLayerConfig = - | ReferenceLineLayerConfig - | ExtendedReferenceLineLayerConfig; +export type CommonXYReferenceLineLayerConfig = ReferenceLineConfig | ReferenceLineLayerConfig; export type CommonXYAnnotationLayerConfig = AnnotationLayerConfig | ExtendedAnnotationLayerConfig; @@ -400,18 +403,18 @@ export type ExtendedDataLayerFn = ExpressionFunctionDefinition< Promise >; +export type ReferenceLineFn = ExpressionFunctionDefinition< + typeof REFERENCE_LINE, + Datatable | null, + ReferenceLineArgs, + ReferenceLineConfigResult +>; export type ReferenceLineLayerFn = ExpressionFunctionDefinition< typeof REFERENCE_LINE_LAYER, Datatable, ReferenceLineLayerArgs, ReferenceLineLayerConfigResult >; -export type ExtendedReferenceLineLayerFn = ExpressionFunctionDefinition< - typeof EXTENDED_REFERENCE_LINE_LAYER, - Datatable, - ExtendedReferenceLineLayerArgs, - ExtendedReferenceLineLayerConfigResult ->; export type YConfigFn = ExpressionFunctionDefinition; export type ExtendedYConfigFn = ExpressionFunctionDefinition< diff --git a/src/plugins/chart_expressions/expression_xy/common/utils/log_datatables.ts b/src/plugins/chart_expressions/expression_xy/common/utils/log_datatables.ts index 79a3cbd2eef19..44026b30ed493 100644 --- a/src/plugins/chart_expressions/expression_xy/common/utils/log_datatables.ts +++ b/src/plugins/chart_expressions/expression_xy/common/utils/log_datatables.ts @@ -8,13 +8,9 @@ import { ExecutionContext } from '@kbn/expressions-plugin'; import { Dimension, prepareLogTable } from '@kbn/visualizations-plugin/common/utils'; -import { LayerTypes } from '../constants'; +import { LayerTypes, REFERENCE_LINE } from '../constants'; import { strings } from '../i18n'; -import { - CommonXYDataLayerConfig, - CommonXYLayerConfig, - CommonXYReferenceLineLayerConfig, -} from '../types'; +import { CommonXYDataLayerConfig, CommonXYLayerConfig, ReferenceLineLayerConfig } from '../types'; export const logDatatables = (layers: CommonXYLayerConfig[], handlers: ExecutionContext) => { if (!handlers?.inspectorAdapters?.tables) { @@ -25,16 +21,17 @@ export const logDatatables = (layers: CommonXYLayerConfig[], handlers: Execution handlers.inspectorAdapters.tables.allowCsvExport = true; layers.forEach((layer) => { - if (layer.layerType === LayerTypes.ANNOTATIONS) { + if (layer.layerType === LayerTypes.ANNOTATIONS || layer.type === REFERENCE_LINE) { return; } + const logTable = prepareLogTable(layer.table, getLayerDimensions(layer), true); handlers.inspectorAdapters.tables.logDatatable(layer.layerId, logTable); }); }; export const getLayerDimensions = ( - layer: CommonXYDataLayerConfig | CommonXYReferenceLineLayerConfig + layer: CommonXYDataLayerConfig | ReferenceLineLayerConfig ): Dimension[] => { let xAccessor; let splitAccessor; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index 842baeb82d78d..6d76a230737ed 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -7,7 +7,7 @@ */ import './annotations.scss'; -import './reference_lines.scss'; +import './reference_lines/reference_lines.scss'; import React from 'react'; import { snakeCase } from 'lodash'; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.test.tsx deleted file mode 100644 index 23e5011fe54a7..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.test.tsx +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { LineAnnotation, RectAnnotation } from '@elastic/charts'; -import { shallow } from 'enzyme'; -import React from 'react'; -import { Datatable } from '@kbn/expressions-plugin/common'; -import { FieldFormat } from '@kbn/field-formats-plugin/common'; -import { LayerTypes } from '../../common/constants'; -import { - ReferenceLineLayerArgs, - ReferenceLineLayerConfig, - ExtendedYConfig, -} from '../../common/types'; -import { ReferenceLineAnnotations, ReferenceLineAnnotationsProps } from './reference_lines'; - -const row: Record = { - xAccessorFirstId: 1, - xAccessorSecondId: 2, - yAccessorLeftFirstId: 5, - yAccessorLeftSecondId: 10, - yAccessorRightFirstId: 5, - yAccessorRightSecondId: 10, -}; - -const data: Datatable = { - type: 'datatable', - rows: [row], - columns: Object.keys(row).map((id) => ({ - id, - name: `Static value: ${row[id]}`, - meta: { - type: 'number', - params: { id: 'number' }, - }, - })), -}; - -function createLayers(yConfigs: ReferenceLineLayerArgs['yConfig']): ReferenceLineLayerConfig[] { - return [ - { - layerId: 'first', - accessors: (yConfigs || []).map(({ forAccessor }) => forAccessor), - yConfig: yConfigs, - type: 'referenceLineLayer', - layerType: LayerTypes.REFERENCELINE, - table: data, - }, - ]; -} - -interface YCoords { - y0: number | undefined; - y1: number | undefined; -} -interface XCoords { - x0: number | undefined; - x1: number | undefined; -} - -function getAxisFromId(layerPrefix: string): ExtendedYConfig['axisMode'] { - return /left/i.test(layerPrefix) ? 'left' : /right/i.test(layerPrefix) ? 'right' : 'bottom'; -} - -const emptyCoords = { x0: undefined, x1: undefined, y0: undefined, y1: undefined }; - -describe('ReferenceLineAnnotations', () => { - describe('with fill', () => { - let formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; - let defaultProps: Omit; - - beforeEach(() => { - formatters = { - left: { convert: jest.fn((x) => x) } as unknown as FieldFormat, - right: { convert: jest.fn((x) => x) } as unknown as FieldFormat, - bottom: { convert: jest.fn((x) => x) } as unknown as FieldFormat, - }; - - defaultProps = { - formatters, - isHorizontal: false, - axesMap: { left: true, right: false }, - paddingMap: {}, - }; - }); - - it.each([ - ['yAccessorLeft', 'above'], - ['yAccessorLeft', 'below'], - ['yAccessorRight', 'above'], - ['yAccessorRight', 'below'], - ] as Array<[string, ExtendedYConfig['fill']]>)( - 'should render a RectAnnotation for a reference line with fill set: %s %s', - (layerPrefix, fill) => { - const axisMode = getAxisFromId(layerPrefix); - const wrapper = shallow( - - ); - - const y0 = fill === 'above' ? 5 : undefined; - const y1 = fill === 'above' ? undefined : 5; - - expect(wrapper.find(LineAnnotation).exists()).toBe(true); - expect(wrapper.find(RectAnnotation).exists()).toBe(true); - expect(wrapper.find(RectAnnotation).prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { x0: undefined, x1: undefined, y0, y1 }, - details: y0 ?? y1, - header: undefined, - }, - ]) - ); - } - ); - - it.each([ - ['xAccessor', 'above'], - ['xAccessor', 'below'], - ] as Array<[string, ExtendedYConfig['fill']]>)( - 'should render a RectAnnotation for a reference line with fill set: %s %s', - (layerPrefix, fill) => { - const wrapper = shallow( - - ); - - const x0 = fill === 'above' ? 1 : undefined; - const x1 = fill === 'above' ? undefined : 1; - - expect(wrapper.find(LineAnnotation).exists()).toBe(true); - expect(wrapper.find(RectAnnotation).exists()).toBe(true); - expect(wrapper.find(RectAnnotation).prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, x0, x1 }, - details: x0 ?? x1, - header: undefined, - }, - ]) - ); - } - ); - - it.each([ - ['yAccessorLeft', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], - ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], - ['yAccessorRight', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], - ['yAccessorRight', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], - ] as Array<[string, ExtendedYConfig['fill'], YCoords, YCoords]>)( - 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', - (layerPrefix, fill, coordsA, coordsB) => { - const axisMode = getAxisFromId(layerPrefix); - const wrapper = shallow( - - ); - - expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...coordsA }, - details: coordsA.y0 ?? coordsA.y1, - header: undefined, - }, - ]) - ); - expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...coordsB }, - details: coordsB.y1 ?? coordsB.y0, - header: undefined, - }, - ]) - ); - } - ); - - it.each([ - ['xAccessor', 'above', { x0: 1, x1: 2 }, { x0: 2, x1: undefined }], - ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: 1, x1: 2 }], - ] as Array<[string, ExtendedYConfig['fill'], XCoords, XCoords]>)( - 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', - (layerPrefix, fill, coordsA, coordsB) => { - const wrapper = shallow( - - ); - - expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...coordsA }, - details: coordsA.x0 ?? coordsA.x1, - header: undefined, - }, - ]) - ); - expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...coordsB }, - details: coordsB.x1 ?? coordsB.x0, - header: undefined, - }, - ]) - ); - } - ); - - it.each(['yAccessorLeft', 'yAccessorRight', 'xAccessor'])( - 'should let areas in different directions overlap: %s', - (layerPrefix) => { - const axisMode = getAxisFromId(layerPrefix); - - const wrapper = shallow( - - ); - - expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x0: 1 } : { y0: 5 }) }, - details: axisMode === 'bottom' ? 1 : 5, - header: undefined, - }, - ]) - ); - expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x1: 2 } : { y1: 10 }) }, - details: axisMode === 'bottom' ? 2 : 10, - header: undefined, - }, - ]) - ); - } - ); - - it.each([ - ['above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], - ['below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], - ] as Array<[ExtendedYConfig['fill'], YCoords, YCoords]>)( - 'should be robust and works also for different axes when on same direction: 1x Left + 1x Right both %s', - (fill, coordsA, coordsB) => { - const wrapper = shallow( - - ); - - expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...coordsA }, - details: coordsA.y0 ?? coordsA.y1, - header: undefined, - }, - ]) - ); - expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( - expect.arrayContaining([ - { - coordinates: { ...emptyCoords, ...coordsB }, - details: coordsB.y1 ?? coordsB.y0, - header: undefined, - }, - ]) - ); - } - ); - }); -}); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx deleted file mode 100644 index d17dbf2a70ad1..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import './reference_lines.scss'; - -import React from 'react'; -import { groupBy } from 'lodash'; -import { RectAnnotation, AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts'; -import { euiLightVars } from '@kbn/ui-theme'; -import type { FieldFormat } from '@kbn/field-formats-plugin/common'; -import type { CommonXYReferenceLineLayerConfig, IconPosition, YAxisMode } from '../../common/types'; -import { - LINES_MARKER_SIZE, - mapVerticalToHorizontalPlacement, - Marker, - MarkerBody, -} from '../helpers'; - -export const computeChartMargins = ( - referenceLinePaddings: Partial>, - labelVisibility: Partial>, - titleVisibility: Partial>, - axesMap: Record<'left' | 'right', unknown>, - isHorizontal: boolean -) => { - const result: Partial> = {}; - if (!labelVisibility?.x && !titleVisibility?.x && referenceLinePaddings.bottom) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom'; - result[placement] = referenceLinePaddings.bottom; - } - if ( - referenceLinePaddings.left && - (isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft)) - ) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left'; - result[placement] = referenceLinePaddings.left; - } - if ( - referenceLinePaddings.right && - (isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight)) - ) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right'; - result[placement] = referenceLinePaddings.right; - } - // there's no top axis, so just check if a margin has been computed - if (referenceLinePaddings.top) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top'; - result[placement] = referenceLinePaddings.top; - } - return result; -}; - -// if there's just one axis, put it on the other one -// otherwise use the same axis -// this function assume the chart is vertical -export function getBaseIconPlacement( - iconPosition: IconPosition | undefined, - axesMap?: Record, - axisMode?: YAxisMode -) { - if (iconPosition === 'auto') { - if (axisMode === 'bottom') { - return Position.Top; - } - if (axesMap) { - if (axisMode === 'left') { - return axesMap.right ? Position.Left : Position.Right; - } - return axesMap.left ? Position.Right : Position.Left; - } - } - - if (iconPosition === 'left') { - return Position.Left; - } - if (iconPosition === 'right') { - return Position.Right; - } - if (iconPosition === 'below') { - return Position.Bottom; - } - return Position.Top; -} - -export interface ReferenceLineAnnotationsProps { - layers: CommonXYReferenceLineLayerConfig[]; - formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; - axesMap: Record<'left' | 'right', boolean>; - isHorizontal: boolean; - paddingMap: Partial>; -} - -export const ReferenceLineAnnotations = ({ - layers, - formatters, - axesMap, - isHorizontal, - paddingMap, -}: ReferenceLineAnnotationsProps) => { - return ( - <> - {layers.flatMap((layer) => { - if (!layer.yConfig) { - return []; - } - const { columnToLabel, yConfig: yConfigs, table } = layer; - const columnToLabelMap: Record = columnToLabel - ? JSON.parse(columnToLabel) - : {}; - - const row = table.rows[0]; - - const yConfigByValue = yConfigs.sort( - ({ forAccessor: idA }, { forAccessor: idB }) => row[idA] - row[idB] - ); - - const groupedByDirection = groupBy(yConfigByValue, 'fill'); - if (groupedByDirection.below) { - groupedByDirection.below.reverse(); - } - - return yConfigByValue.flatMap((yConfig, i) => { - // Find the formatter for the given axis - const groupId = - yConfig.axisMode === 'bottom' - ? undefined - : yConfig.axisMode === 'right' - ? 'right' - : 'left'; - - const formatter = formatters[groupId || 'bottom']; - - const defaultColor = euiLightVars.euiColorDarkShade; - - // get the position for vertical chart - const markerPositionVertical = getBaseIconPlacement( - yConfig.iconPosition, - axesMap, - yConfig.axisMode - ); - // the padding map is built for vertical chart - const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE; - - const props = { - groupId, - marker: ( - - ), - markerBody: ( - - ), - // rotate the position if required - markerPosition: isHorizontal - ? mapVerticalToHorizontalPlacement(markerPositionVertical) - : markerPositionVertical, - }; - const annotations = []; - - const sharedStyle = { - strokeWidth: yConfig.lineWidth || 1, - stroke: yConfig.color || defaultColor, - dash: - yConfig.lineStyle === 'dashed' - ? [(yConfig.lineWidth || 1) * 3, yConfig.lineWidth || 1] - : yConfig.lineStyle === 'dotted' - ? [yConfig.lineWidth || 1, yConfig.lineWidth || 1] - : undefined, - }; - - annotations.push( - ({ - dataValue: row[yConfig.forAccessor], - header: columnToLabelMap[yConfig.forAccessor], - details: formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], - }))} - domainType={ - yConfig.axisMode === 'bottom' - ? AnnotationDomainType.XDomain - : AnnotationDomainType.YDomain - } - style={{ - line: { - ...sharedStyle, - opacity: 1, - }, - }} - /> - ); - - if (yConfig.fill && yConfig.fill !== 'none') { - const isFillAbove = yConfig.fill === 'above'; - const indexFromSameType = groupedByDirection[yConfig.fill].findIndex( - ({ forAccessor }) => forAccessor === yConfig.forAccessor - ); - const shouldCheckNextReferenceLine = - indexFromSameType < groupedByDirection[yConfig.fill].length - 1; - annotations.push( - { - const nextValue = shouldCheckNextReferenceLine - ? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor] - : undefined; - if (yConfig.axisMode === 'bottom') { - return { - coordinates: { - x0: isFillAbove ? row[yConfig.forAccessor] : nextValue, - y0: undefined, - x1: isFillAbove ? nextValue : row[yConfig.forAccessor], - y1: undefined, - }, - header: columnToLabelMap[yConfig.forAccessor], - details: - formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], - }; - } - return { - coordinates: { - x0: undefined, - y0: isFillAbove ? row[yConfig.forAccessor] : nextValue, - x1: undefined, - y1: isFillAbove ? nextValue : row[yConfig.forAccessor], - }, - header: columnToLabelMap[yConfig.forAccessor], - details: - formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], - }; - })} - style={{ - ...sharedStyle, - fill: yConfig.color || defaultColor, - opacity: 0.1, - }} - /> - ); - } - return annotations; - }); - })} - - ); -}; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/index.ts b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/index.ts new file mode 100644 index 0000000000000..62b3b31bf8bd5 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './reference_lines'; +export * from './utils'; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx new file mode 100644 index 0000000000000..74bb18597f2f2 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC } from 'react'; +import { Position } from '@elastic/charts'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { ReferenceLineConfig } from '../../../common/types'; +import { getGroupId } from './utils'; +import { ReferenceLineAnnotations } from './reference_line_annotations'; + +interface ReferenceLineProps { + layer: ReferenceLineConfig; + paddingMap: Partial>; + formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + axesMap: Record<'left' | 'right', boolean>; + isHorizontal: boolean; +} + +export const ReferenceLine: FC = ({ + layer, + axesMap, + formatters, + paddingMap, + isHorizontal, +}) => { + const { + yConfig: [yConfig], + } = layer; + + if (!yConfig) { + return null; + } + + const { axisMode, value } = yConfig; + + // Find the formatter for the given axis + const groupId = getGroupId(axisMode); + + const formatter = formatters[groupId || 'bottom']; + const id = `${layer.layerId}-${value}`; + + return ( + + ); +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_annotations.tsx new file mode 100644 index 0000000000000..b5b94b4c2df51 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_annotations.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AnnotationDomainType, LineAnnotation, Position, RectAnnotation } from '@elastic/charts'; +import { euiLightVars } from '@kbn/ui-theme'; +import React, { FC } from 'react'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { LINES_MARKER_SIZE } from '../../helpers'; +import { + AvailableReferenceLineIcon, + FillStyle, + IconPosition, + LineStyle, + YAxisMode, +} from '../../../common/types'; +import { + getBaseIconPlacement, + getBottomRect, + getGroupId, + getHorizontalRect, + getLineAnnotationProps, + getSharedStyle, +} from './utils'; + +export interface ReferenceLineAnnotationConfig { + id: string; + name?: string; + value: number; + nextValue?: number; + icon?: AvailableReferenceLineIcon; + lineWidth?: number; + lineStyle?: LineStyle; + fill?: FillStyle; + iconPosition?: IconPosition; + textVisibility?: boolean; + axisMode?: YAxisMode; + color?: string; +} + +interface Props { + config: ReferenceLineAnnotationConfig; + paddingMap: Partial>; + formatter?: FieldFormat; + axesMap: Record<'left' | 'right', boolean>; + isHorizontal: boolean; +} + +const getRectDataValue = ( + annotationConfig: ReferenceLineAnnotationConfig, + formatter: FieldFormat | undefined +) => { + const { name, value, nextValue, fill, axisMode } = annotationConfig; + const isFillAbove = fill === 'above'; + + if (axisMode === 'bottom') { + return getBottomRect(name, isFillAbove, formatter, value, nextValue); + } + + return getHorizontalRect(name, isFillAbove, formatter, value, nextValue); +}; + +export const ReferenceLineAnnotations: FC = ({ + config, + axesMap, + formatter, + paddingMap, + isHorizontal, +}) => { + const { id, axisMode, iconPosition, name, textVisibility, value, fill, color } = config; + + // Find the formatter for the given axis + const groupId = getGroupId(axisMode); + const defaultColor = euiLightVars.euiColorDarkShade; + // get the position for vertical chart + const markerPositionVertical = getBaseIconPlacement(iconPosition, axesMap, axisMode); + // the padding map is built for vertical chart + const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE; + + const props = getLineAnnotationProps( + config, + { + markerLabel: name, + markerBodyLabel: textVisibility && !hasReducedPadding ? name : undefined, + }, + axesMap, + paddingMap, + groupId, + isHorizontal + ); + + const sharedStyle = getSharedStyle(config); + + const dataValues = { + dataValue: value, + header: name, + details: formatter?.convert(value) || value.toString(), + }; + + const line = ( + + ); + + let rect; + if (fill && fill !== 'none') { + const rectDataValues = getRectDataValue(config, formatter); + + rect = ( + + ); + } + return ( + <> + {line} + {rect} + + ); +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_layer.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_layer.tsx new file mode 100644 index 0000000000000..210f5bda0126b --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_layer.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC } from 'react'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { groupBy } from 'lodash'; +import { Position } from '@elastic/charts'; +import { ReferenceLineLayerConfig } from '../../../common/types'; +import { getGroupId } from './utils'; +import { ReferenceLineAnnotations } from './reference_line_annotations'; + +interface ReferenceLineLayerProps { + layer: ReferenceLineLayerConfig; + formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + paddingMap: Partial>; + axesMap: Record<'left' | 'right', boolean>; + isHorizontal: boolean; +} + +export const ReferenceLineLayer: FC = ({ + layer, + formatters, + paddingMap, + axesMap, + isHorizontal, +}) => { + if (!layer.yConfig) { + return null; + } + + const { columnToLabel, yConfig: yConfigs, table } = layer; + const columnToLabelMap: Record = columnToLabel ? JSON.parse(columnToLabel) : {}; + + const row = table.rows[0]; + + const yConfigByValue = yConfigs.sort( + ({ forAccessor: idA }, { forAccessor: idB }) => row[idA] - row[idB] + ); + + const groupedByDirection = groupBy(yConfigByValue, 'fill'); + if (groupedByDirection.below) { + groupedByDirection.below.reverse(); + } + + const referenceLineElements = yConfigByValue.flatMap((yConfig) => { + const { axisMode } = yConfig; + + // Find the formatter for the given axis + const groupId = getGroupId(axisMode); + + const formatter = formatters[groupId || 'bottom']; + const name = columnToLabelMap[yConfig.forAccessor]; + const value = row[yConfig.forAccessor]; + const yConfigsWithSameDirection = groupedByDirection[yConfig.fill!]; + const indexFromSameType = yConfigsWithSameDirection.findIndex( + ({ forAccessor }) => forAccessor === yConfig.forAccessor + ); + + const shouldCheckNextReferenceLine = indexFromSameType < yConfigsWithSameDirection.length - 1; + + const nextValue = shouldCheckNextReferenceLine + ? row[yConfigsWithSameDirection[indexFromSameType + 1].forAccessor] + : undefined; + + const { forAccessor, type, ...restAnnotationConfig } = yConfig; + const id = `${layer.layerId}-${yConfig.forAccessor}`; + + return ( + + ); + }); + + return <>{referenceLineElements}; +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.scss b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.scss similarity index 100% rename from src/plugins/chart_expressions/expression_xy/public/components/reference_lines.scss rename to src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.scss diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.test.tsx new file mode 100644 index 0000000000000..35e434d65bc18 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.test.tsx @@ -0,0 +1,683 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { LineAnnotation, RectAnnotation } from '@elastic/charts'; +import { shallow } from 'enzyme'; +import React from 'react'; +import { Datatable } from '@kbn/expressions-plugin/common'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { LayerTypes } from '../../../common/constants'; +import { + ReferenceLineLayerArgs, + ReferenceLineLayerConfig, + ExtendedYConfig, + ReferenceLineArgs, + ReferenceLineConfig, +} from '../../../common/types'; +import { ReferenceLines, ReferenceLinesProps } from './reference_lines'; +import { ReferenceLineLayer } from './reference_line_layer'; +import { ReferenceLine } from './reference_line'; +import { ReferenceLineAnnotations } from './reference_line_annotations'; + +const row: Record = { + xAccessorFirstId: 1, + xAccessorSecondId: 2, + yAccessorLeftFirstId: 5, + yAccessorLeftSecondId: 10, + yAccessorRightFirstId: 5, + yAccessorRightSecondId: 10, +}; + +const data: Datatable = { + type: 'datatable', + rows: [row], + columns: Object.keys(row).map((id) => ({ + id, + name: `Static value: ${row[id]}`, + meta: { + type: 'number', + params: { id: 'number' }, + }, + })), +}; + +function createLayers(yConfigs: ReferenceLineLayerArgs['yConfig']): ReferenceLineLayerConfig[] { + return [ + { + layerId: 'first', + accessors: (yConfigs || []).map(({ forAccessor }) => forAccessor), + yConfig: yConfigs, + type: 'referenceLineLayer', + layerType: LayerTypes.REFERENCELINE, + table: data, + }, + ]; +} + +function createReferenceLine( + layerId: string, + lineLength: number, + args: ReferenceLineArgs +): ReferenceLineConfig { + return { + layerId, + type: 'referenceLine', + layerType: 'referenceLine', + lineLength, + yConfig: [{ type: 'referenceLineYConfig', ...args }], + }; +} + +interface YCoords { + y0: number | undefined; + y1: number | undefined; +} +interface XCoords { + x0: number | undefined; + x1: number | undefined; +} + +function getAxisFromId(layerPrefix: string): ExtendedYConfig['axisMode'] { + return /left/i.test(layerPrefix) ? 'left' : /right/i.test(layerPrefix) ? 'right' : 'bottom'; +} + +const emptyCoords = { x0: undefined, x1: undefined, y0: undefined, y1: undefined }; + +describe('ReferenceLines', () => { + describe('referenceLineLayers', () => { + let formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + let defaultProps: Omit; + + beforeEach(() => { + formatters = { + left: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + right: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + bottom: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + }; + + defaultProps = { + formatters, + isHorizontal: false, + axesMap: { left: true, right: false }, + paddingMap: {}, + }; + }); + + it.each([ + ['yAccessorLeft', 'above'], + ['yAccessorLeft', 'below'], + ['yAccessorRight', 'above'], + ['yAccessorRight', 'below'], + ] as Array<[string, ExtendedYConfig['fill']]>)( + 'should render a RectAnnotation for a reference line with fill set: %s %s', + (layerPrefix, fill) => { + const axisMode = getAxisFromId(layerPrefix); + const wrapper = shallow( + + ); + const referenceLineLayer = wrapper.find(ReferenceLineLayer).dive(); + + const y0 = fill === 'above' ? 5 : undefined; + const y1 = fill === 'above' ? undefined : 5; + + const referenceLineAnnotation = referenceLineLayer.find(ReferenceLineAnnotations).dive(); + expect(referenceLineAnnotation.find(LineAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { x0: undefined, x1: undefined, y0, y1 }, + details: y0 ?? y1, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['xAccessor', 'above'], + ['xAccessor', 'below'], + ] as Array<[string, ExtendedYConfig['fill']]>)( + 'should render a RectAnnotation for a reference line with fill set: %s %s', + (layerPrefix, fill) => { + const wrapper = shallow( + + ); + const referenceLineLayer = wrapper.find(ReferenceLineLayer).dive(); + + const x0 = fill === 'above' ? 1 : undefined; + const x1 = fill === 'above' ? undefined : 1; + + const referenceLineAnnotation = referenceLineLayer.find(ReferenceLineAnnotations).dive(); + expect(referenceLineAnnotation.find(LineAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, x0, x1 }, + details: x0 ?? x1, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['yAccessorLeft', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], + ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], + ['yAccessorRight', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], + ['yAccessorRight', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], + ] as Array<[string, ExtendedYConfig['fill'], YCoords, YCoords]>)( + 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', + (layerPrefix, fill, coordsA, coordsB) => { + const axisMode = getAxisFromId(layerPrefix); + const wrapper = shallow( + + ); + const referenceLineLayer = wrapper.find(ReferenceLineLayer).dive(); + const referenceLineAnnotations = referenceLineLayer.find(ReferenceLineAnnotations); + + expect( + referenceLineAnnotations.first().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: coordsA.y0 ?? coordsA.y1, + header: undefined, + }, + ]) + ); + expect( + referenceLineAnnotations.last().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: coordsB.y1 ?? coordsB.y0, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['xAccessor', 'above', { x0: 1, x1: 2 }, { x0: 2, x1: undefined }], + ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: 1, x1: 2 }], + ] as Array<[string, ExtendedYConfig['fill'], XCoords, XCoords]>)( + 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', + (layerPrefix, fill, coordsA, coordsB) => { + const wrapper = shallow( + + ); + const referenceLineLayer = wrapper.find(ReferenceLineLayer).dive(); + const referenceLineAnnotations = referenceLineLayer.find(ReferenceLineAnnotations); + + expect( + referenceLineAnnotations.first().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: coordsA.x0 ?? coordsA.x1, + header: undefined, + }, + ]) + ); + expect( + referenceLineAnnotations.last().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: coordsB.x1 ?? coordsB.x0, + header: undefined, + }, + ]) + ); + } + ); + + it.each(['yAccessorLeft', 'yAccessorRight', 'xAccessor'])( + 'should let areas in different directions overlap: %s', + (layerPrefix) => { + const axisMode = getAxisFromId(layerPrefix); + + const wrapper = shallow( + + ); + const referenceLineLayer = wrapper.find(ReferenceLineLayer).dive(); + const referenceLineAnnotations = referenceLineLayer.find(ReferenceLineAnnotations); + + expect( + referenceLineAnnotations.first().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x0: 1 } : { y0: 5 }) }, + details: axisMode === 'bottom' ? 1 : 5, + header: undefined, + }, + ]) + ); + expect( + referenceLineAnnotations.last().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x1: 2 } : { y1: 10 }) }, + details: axisMode === 'bottom' ? 2 : 10, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], + ['below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], + ] as Array<[ExtendedYConfig['fill'], YCoords, YCoords]>)( + 'should be robust and works also for different axes when on same direction: 1x Left + 1x Right both %s', + (fill, coordsA, coordsB) => { + const wrapper = shallow( + + ); + const referenceLineLayer = wrapper.find(ReferenceLineLayer).dive(); + const referenceLineAnnotations = referenceLineLayer.find(ReferenceLineAnnotations); + + expect( + referenceLineAnnotations.first().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: coordsA.y0 ?? coordsA.y1, + header: undefined, + }, + ]) + ); + expect( + referenceLineAnnotations.last().dive().find(RectAnnotation).prop('dataValues') + ).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: coordsB.y1 ?? coordsB.y0, + header: undefined, + }, + ]) + ); + } + ); + }); + + describe('referenceLines', () => { + let formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + let defaultProps: Omit; + + beforeEach(() => { + formatters = { + left: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + right: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + bottom: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + }; + + defaultProps = { + formatters, + isHorizontal: false, + axesMap: { left: true, right: false }, + paddingMap: {}, + }; + }); + + it.each([ + ['yAccessorLeft', 'above'], + ['yAccessorLeft', 'below'], + ['yAccessorRight', 'above'], + ['yAccessorRight', 'below'], + ] as Array<[string, ExtendedYConfig['fill']]>)( + 'should render a RectAnnotation for a reference line with fill set: %s %s', + (layerPrefix, fill) => { + const axisMode = getAxisFromId(layerPrefix); + const value = 5; + const wrapper = shallow( + + ); + const referenceLine = wrapper.find(ReferenceLine).dive(); + const referenceLineAnnotation = referenceLine.find(ReferenceLineAnnotations).dive(); + + const y0 = fill === 'above' ? value : undefined; + const y1 = fill === 'above' ? undefined : value; + + expect(referenceLineAnnotation.find(LineAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { x0: undefined, x1: undefined, y0, y1 }, + details: y0 ?? y1, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['xAccessor', 'above'], + ['xAccessor', 'below'], + ] as Array<[string, ExtendedYConfig['fill']]>)( + 'should render a RectAnnotation for a reference line with fill set: %s %s', + (layerPrefix, fill) => { + const value = 1; + const wrapper = shallow( + + ); + const referenceLine = wrapper.find(ReferenceLine).dive(); + const referenceLineAnnotation = referenceLine.find(ReferenceLineAnnotations).dive(); + + const x0 = fill === 'above' ? value : undefined; + const x1 = fill === 'above' ? undefined : value; + + expect(referenceLineAnnotation.find(LineAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).exists()).toBe(true); + expect(referenceLineAnnotation.find(RectAnnotation).prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, x0, x1 }, + details: x0 ?? x1, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['yAccessorLeft', 'above', { y0: 10, y1: undefined }, { y0: 10, y1: undefined }], + ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: undefined, y1: 5 }], + ] as Array<[string, ExtendedYConfig['fill'], YCoords, YCoords]>)( + 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', + (layerPrefix, fill, coordsA, coordsB) => { + const axisMode = getAxisFromId(layerPrefix); + const value = coordsA.y0 ?? coordsA.y1!; + const wrapper = shallow( + + ); + const referenceLine = wrapper.find(ReferenceLine).first().dive(); + const referenceLineAnnotation = referenceLine.find(ReferenceLineAnnotations).dive(); + + expect(referenceLineAnnotation.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: value, + header: undefined, + }, + ]) + ); + expect(referenceLineAnnotation.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: value, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['xAccessor', 'above', { x0: 1, x1: undefined }, { x0: 1, x1: undefined }], + ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: undefined, x1: 1 }], + ] as Array<[string, ExtendedYConfig['fill'], XCoords, XCoords]>)( + 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', + (layerPrefix, fill, coordsA, coordsB) => { + const value = coordsA.x0 ?? coordsA.x1!; + const wrapper = shallow( + + ); + const referenceLine1 = wrapper.find(ReferenceLine).first().dive(); + const referenceLineAnnotation1 = referenceLine1.find(ReferenceLineAnnotations).dive(); + + expect(referenceLineAnnotation1.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: value, + header: undefined, + }, + ]) + ); + + const referenceLine2 = wrapper.find(ReferenceLine).last().dive(); + const referenceLineAnnotation2 = referenceLine2.find(ReferenceLineAnnotations).dive(); + + expect(referenceLineAnnotation2.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: value, + header: undefined, + }, + ]) + ); + } + ); + + it.each(['yAccessorLeft', 'yAccessorRight', 'xAccessor'])( + 'should let areas in different directions overlap: %s', + (layerPrefix) => { + const axisMode = getAxisFromId(layerPrefix); + const value1 = 1; + const value2 = 10; + const wrapper = shallow( + + ); + const referenceLine1 = wrapper.find(ReferenceLine).first().dive(); + const referenceLineAnnotation1 = referenceLine1.find(ReferenceLineAnnotations).dive(); + + expect(referenceLineAnnotation1.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { + ...emptyCoords, + ...(axisMode === 'bottom' ? { x0: value1 } : { y0: value1 }), + }, + details: value1, + header: undefined, + }, + ]) + ); + + const referenceLine2 = wrapper.find(ReferenceLine).last().dive(); + const referenceLineAnnotation2 = referenceLine2.find(ReferenceLineAnnotations).dive(); + + expect(referenceLineAnnotation2.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { + ...emptyCoords, + ...(axisMode === 'bottom' ? { x1: value2 } : { y1: value2 }), + }, + details: value2, + header: undefined, + }, + ]) + ); + } + ); + }); +}); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx new file mode 100644 index 0000000000000..9dca7b6107072 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import './reference_lines.scss'; + +import React from 'react'; +import { Position } from '@elastic/charts'; +import type { FieldFormat } from '@kbn/field-formats-plugin/common'; +import type { CommonXYReferenceLineLayerConfig } from '../../../common/types'; +import { isReferenceLine, mapVerticalToHorizontalPlacement } from '../../helpers'; +import { ReferenceLineLayer } from './reference_line_layer'; +import { ReferenceLine } from './reference_line'; + +export const computeChartMargins = ( + referenceLinePaddings: Partial>, + labelVisibility: Partial>, + titleVisibility: Partial>, + axesMap: Record<'left' | 'right', unknown>, + isHorizontal: boolean +) => { + const result: Partial> = {}; + if (!labelVisibility?.x && !titleVisibility?.x && referenceLinePaddings.bottom) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom'; + result[placement] = referenceLinePaddings.bottom; + } + if ( + referenceLinePaddings.left && + (isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft)) + ) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left'; + result[placement] = referenceLinePaddings.left; + } + if ( + referenceLinePaddings.right && + (isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight)) + ) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right'; + result[placement] = referenceLinePaddings.right; + } + // there's no top axis, so just check if a margin has been computed + if (referenceLinePaddings.top) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top'; + result[placement] = referenceLinePaddings.top; + } + return result; +}; + +export interface ReferenceLinesProps { + layers: CommonXYReferenceLineLayerConfig[]; + formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + axesMap: Record<'left' | 'right', boolean>; + isHorizontal: boolean; + paddingMap: Partial>; +} + +export const ReferenceLines = ({ layers, ...rest }: ReferenceLinesProps) => { + return ( + <> + {layers.flatMap((layer) => { + if (!layer.yConfig) { + return null; + } + + if (isReferenceLine(layer)) { + return ; + } + + return ( + + ); + })} + + ); +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx new file mode 100644 index 0000000000000..1a6eae6a490e6 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { Position } from '@elastic/charts'; +import { euiLightVars } from '@kbn/ui-theme'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { IconPosition, YAxisMode } from '../../../common/types'; +import { + LINES_MARKER_SIZE, + mapVerticalToHorizontalPlacement, + Marker, + MarkerBody, +} from '../../helpers'; +import { ReferenceLineAnnotationConfig } from './reference_line_annotations'; + +// if there's just one axis, put it on the other one +// otherwise use the same axis +// this function assume the chart is vertical +export function getBaseIconPlacement( + iconPosition: IconPosition | undefined, + axesMap?: Record, + axisMode?: YAxisMode +) { + if (iconPosition === 'auto') { + if (axisMode === 'bottom') { + return Position.Top; + } + if (axesMap) { + if (axisMode === 'left') { + return axesMap.right ? Position.Left : Position.Right; + } + return axesMap.left ? Position.Right : Position.Left; + } + } + + if (iconPosition === 'left') { + return Position.Left; + } + if (iconPosition === 'right') { + return Position.Right; + } + if (iconPosition === 'below') { + return Position.Bottom; + } + return Position.Top; +} + +export const getSharedStyle = (config: ReferenceLineAnnotationConfig) => ({ + strokeWidth: config.lineWidth || 1, + stroke: config.color || euiLightVars.euiColorDarkShade, + dash: + config.lineStyle === 'dashed' + ? [(config.lineWidth || 1) * 3, config.lineWidth || 1] + : config.lineStyle === 'dotted' + ? [config.lineWidth || 1, config.lineWidth || 1] + : undefined, +}); + +export const getLineAnnotationProps = ( + config: ReferenceLineAnnotationConfig, + labels: { markerLabel?: string; markerBodyLabel?: string }, + axesMap: Record<'left' | 'right', boolean>, + paddingMap: Partial>, + groupId: 'left' | 'right' | undefined, + isHorizontal: boolean +) => { + // get the position for vertical chart + const markerPositionVertical = getBaseIconPlacement( + config.iconPosition, + axesMap, + config.axisMode + ); + // the padding map is built for vertical chart + const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE; + + return { + groupId, + marker: ( + + ), + markerBody: ( + + ), + // rotate the position if required + markerPosition: isHorizontal + ? mapVerticalToHorizontalPlacement(markerPositionVertical) + : markerPositionVertical, + }; +}; + +export const getGroupId = (axisMode: YAxisMode | undefined) => + axisMode === 'bottom' ? undefined : axisMode === 'right' ? 'right' : 'left'; + +export const getBottomRect = ( + headerLabel: string | undefined, + isFillAbove: boolean, + formatter: FieldFormat | undefined, + currentValue: number, + nextValue?: number +) => ({ + coordinates: { + x0: isFillAbove ? currentValue : nextValue, + y0: undefined, + x1: isFillAbove ? nextValue : currentValue, + y1: undefined, + }, + header: headerLabel, + details: formatter?.convert(currentValue) || currentValue.toString(), +}); + +export const getHorizontalRect = ( + headerLabel: string | undefined, + isFillAbove: boolean, + formatter: FieldFormat | undefined, + currentValue: number, + nextValue?: number +) => ({ + coordinates: { + x0: undefined, + y0: isFillAbove ? currentValue : nextValue, + x1: undefined, + y1: isFillAbove ? nextValue : currentValue, + }, + header: headerLabel, + details: formatter?.convert(currentValue) || currentValue.toString(), +}); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index 9bb3ea4f498e4..80048bcb84038 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -42,14 +42,24 @@ import { LegendSizeToPixels, } from '@kbn/visualizations-plugin/common/constants'; import type { FilterEvent, BrushEvent, FormatFactory } from '../types'; -import type { CommonXYDataLayerConfig, SeriesType, XYChartProps } from '../../common/types'; +import type { + CommonXYDataLayerConfig, + ExtendedYConfig, + ReferenceLineYConfig, + SeriesType, + XYChartProps, +} from '../../common/types'; import { isHorizontalChart, getAnnotationsLayers, getDataLayers, Series, getFormat, + isReferenceLineYConfig, getFormattedTablesByLayers, +} from '../helpers'; + +import { getFilteredLayers, getReferenceLayers, isDataLayer, @@ -60,7 +70,7 @@ import { } from '../helpers'; import { getXDomain, XyEndzones } from './x_domain'; import { getLegendAction } from './legend_action'; -import { ReferenceLineAnnotations, computeChartMargins } from './reference_lines'; +import { ReferenceLines, computeChartMargins } from './reference_lines'; import { visualizationDefinitions } from '../definitions'; import { CommonXYLayerConfig } from '../../common/types'; import { SplitChart } from './split_chart'; @@ -270,6 +280,7 @@ export function XYChart({ }; const referenceLineLayers = getReferenceLayers(layers); + const annotationsLayers = getAnnotationsLayers(layers); const firstTable = dataLayers[0]?.table; @@ -286,7 +297,9 @@ export function XYChart({ const rangeAnnotations = getRangeAnnotations(annotationsLayers); const visualConfigs = [ - ...referenceLineLayers.flatMap(({ yConfig }) => yConfig), + ...referenceLineLayers.flatMap( + ({ yConfig }) => yConfig + ), ...groupedLineAnnotations, ].filter(Boolean); @@ -364,9 +377,10 @@ export function XYChart({ l.yConfig ? l.yConfig.map((yConfig) => ({ layerId: l.layerId, yConfig })) : [] ) .filter(({ yConfig }) => yConfig.axisMode === axis.groupId) - .map( - ({ layerId, yConfig }) => - `${layerId}-${yConfig.forAccessor}-${yConfig.fill !== 'none' ? 'rect' : 'line'}` + .map(({ layerId, yConfig }) => + isReferenceLineYConfig(yConfig) + ? `${layerId}-${yConfig.value}-${yConfig.fill !== 'none' ? 'rect' : 'line'}` + : `${layerId}-${yConfig.forAccessor}-${yConfig.fill !== 'none' ? 'rect' : 'line'}` ), }; }; @@ -668,7 +682,7 @@ export function XYChart({ /> )} {referenceLineLayers.length ? ( - ( - (layer): layer is CommonXYReferenceLineLayerConfig | CommonXYDataLayerConfig => { + return layers.filter( + (layer): layer is ReferenceLineLayerConfig | CommonXYDataLayerConfig => { let table: Datatable | undefined; let accessors: Array = []; let xAccessor: undefined | string | number; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts index e2f95491dbce8..900cba4784853 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts @@ -7,7 +7,7 @@ */ import type { CommonXYLayerConfig, SeriesType, ExtendedYConfig, YConfig } from '../../common'; -import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization'; +import { getDataLayers, isAnnotationsLayer, isDataLayer, isReferenceLine } from './visualization'; export function isHorizontalSeries(seriesType: SeriesType) { return ( @@ -26,7 +26,11 @@ export function isHorizontalChart(layers: CommonXYLayerConfig[]) { } export const getSeriesColor = (layer: CommonXYLayerConfig, accessor: string) => { - if ((isDataLayer(layer) && layer.splitAccessor) || isAnnotationsLayer(layer)) { + if ( + (isDataLayer(layer) && layer.splitAccessor) || + isAnnotationsLayer(layer) || + isReferenceLine(layer) + ) { return null; } const yConfig: Array | undefined = layer?.yConfig; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts index db0b431d56fac..480fa5374238e 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts @@ -6,12 +6,21 @@ * Side Public License, v 1. */ -import { LayerTypes } from '../../common/constants'; +import { + LayerTypes, + REFERENCE_LINE, + REFERENCE_LINE_LAYER, + REFERENCE_LINE_Y_CONFIG, +} from '../../common/constants'; import { CommonXYLayerConfig, CommonXYDataLayerConfig, CommonXYReferenceLineLayerConfig, CommonXYAnnotationLayerConfig, + ReferenceLineLayerConfig, + ReferenceLineConfig, + ExtendedYConfigResult, + ReferenceLineYConfig, } from '../../common/types'; export const isDataLayer = (layer: CommonXYLayerConfig): layer is CommonXYDataLayerConfig => @@ -20,13 +29,24 @@ export const isDataLayer = (layer: CommonXYLayerConfig): layer is CommonXYDataLa export const getDataLayers = (layers: CommonXYLayerConfig[]) => (layers || []).filter((layer): layer is CommonXYDataLayerConfig => isDataLayer(layer)); -export const isReferenceLayer = ( +export const isReferenceLayer = (layer: CommonXYLayerConfig): layer is ReferenceLineLayerConfig => + layer.layerType === LayerTypes.REFERENCELINE && layer.type === REFERENCE_LINE_LAYER; + +export const isReferenceLine = (layer: CommonXYLayerConfig): layer is ReferenceLineConfig => + layer.type === REFERENCE_LINE; + +export const isReferenceLineYConfig = ( + yConfig: ReferenceLineYConfig | ExtendedYConfigResult +): yConfig is ReferenceLineYConfig => yConfig.type === REFERENCE_LINE_Y_CONFIG; + +export const isReferenceLineOrLayer = ( layer: CommonXYLayerConfig ): layer is CommonXYReferenceLineLayerConfig => layer.layerType === LayerTypes.REFERENCELINE; export const getReferenceLayers = (layers: CommonXYLayerConfig[]) => - (layers || []).filter((layer): layer is CommonXYReferenceLineLayerConfig => - isReferenceLayer(layer) + (layers || []).filter( + (layer): layer is CommonXYReferenceLineLayerConfig => + isReferenceLayer(layer) || isReferenceLine(layer) ); const isAnnotationLayerCommon = ( diff --git a/src/plugins/chart_expressions/expression_xy/public/plugin.ts b/src/plugins/chart_expressions/expression_xy/public/plugin.ts index 5c27da6b82b28..0dc6f62df3183 100755 --- a/src/plugins/chart_expressions/expression_xy/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/public/plugin.ts @@ -24,8 +24,8 @@ import { gridlinesConfigFunction, axisExtentConfigFunction, tickLabelsConfigFunction, + referenceLineFunction, referenceLineLayerFunction, - extendedReferenceLineLayerFunction, annotationLayerFunction, labelsOrientationConfigFunction, axisTitlesVisibilityConfigFunction, @@ -64,8 +64,8 @@ export class ExpressionXyPlugin { expressions.registerFunction(annotationLayerFunction); expressions.registerFunction(extendedAnnotationLayerFunction); expressions.registerFunction(labelsOrientationConfigFunction); + expressions.registerFunction(referenceLineFunction); expressions.registerFunction(referenceLineLayerFunction); - expressions.registerFunction(extendedReferenceLineLayerFunction); expressions.registerFunction(axisTitlesVisibilityConfigFunction); expressions.registerFunction(xyVisFunction); expressions.registerFunction(layeredXyVisFunction); diff --git a/src/plugins/chart_expressions/expression_xy/server/plugin.ts b/src/plugins/chart_expressions/expression_xy/server/plugin.ts index cefde5d38a5f4..4ddac2b3a3f79 100755 --- a/src/plugins/chart_expressions/expression_xy/server/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/server/plugin.ts @@ -19,10 +19,10 @@ import { tickLabelsConfigFunction, annotationLayerFunction, labelsOrientationConfigFunction, - referenceLineLayerFunction, + referenceLineFunction, axisTitlesVisibilityConfigFunction, extendedDataLayerFunction, - extendedReferenceLineLayerFunction, + referenceLineLayerFunction, layeredXyVisFunction, extendedAnnotationLayerFunction, } from '../common/expression_functions'; @@ -42,8 +42,8 @@ export class ExpressionXyPlugin expressions.registerFunction(annotationLayerFunction); expressions.registerFunction(extendedAnnotationLayerFunction); expressions.registerFunction(labelsOrientationConfigFunction); + expressions.registerFunction(referenceLineFunction); expressions.registerFunction(referenceLineLayerFunction); - expressions.registerFunction(extendedReferenceLineLayerFunction); expressions.registerFunction(axisTitlesVisibilityConfigFunction); expressions.registerFunction(xyVisFunction); expressions.registerFunction(layeredXyVisFunction); diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index cb6e6cff2d70e..ff5a692a76e96 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -356,7 +356,7 @@ const referenceLineLayerToExpression = ( chain: [ { type: 'function', - function: 'extendedReferenceLineLayer', + function: 'referenceLineLayer', arguments: { layerId: [layer.layerId], yConfig: layer.yConfig From 24bdc97413fbdd749db4d007ccf9f06cc1a243c8 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 20 May 2022 10:45:50 +0200 Subject: [PATCH 019/120] [ML] Explain log rate spikes: Page setup (#132121) Builds out UI/code boilerplate necessary before we start implementing the feature's own UI on a dedicated page. - Updates navigation to bring up data view/saved search selection before moving on to the explain log spike rates page. The bar chart race demo page was moved to the aiops/single_endpoint_streaming_demo url. It is kept in this PR so we have two different pages + API endpoints that use streaming. With this still in place it's easier to update the streaming code to be more generic and reusable. - The url/page aiops/explain_log_rate_spikes has been added with some dummy request that slowly streams a data view's fields to the client. This page will host the actual UI to be brought over from the PoC in follow ups to this PR. - The structure to embed aiops plugin pages in the ml plugin has been simplified. Instead of a lot of custom code to load the components at runtime in the aiops plugin itself, this now uses React lazy loading with Suspense, similar to how we load Vega charts in other places. We no longer initialize the aiops client side code during startup of the plugin itself and augment it, instead we statically import components and pass on props/contexts from the ml plugin. - The code to handle streaming chunks on the client side in stream_fetch.ts/use_stream_fetch_reducer.ts has been improved to make better use of TS generics so for a given API endpoint it's able to return the appropriate coresponding return data type and only allows to use the supported reducer actions for that endpoint. Buffering client side actions has been tweaked to handle state updates more quickly if updates from the server are stalling. --- .../aiops/common/api/example_stream.ts | 5 +- .../common/api/explain_log_rate_spikes.ts | 34 ++++ x-pack/plugins/aiops/common/api/index.ts | 15 +- x-pack/plugins/aiops/kibana.json | 2 +- x-pack/plugins/aiops/public/api/index.ts | 15 -- .../plugins/aiops/public/components/app.tsx | 167 ------------------ .../components/explain_log_rate_spikes.tsx | 34 ---- .../explain_log_rate_spikes.tsx | 49 +++++ .../explain_log_rate_spikes/index.ts | 13 ++ .../explain_log_rate_spikes/stream_reducer.ts | 37 ++++ .../get_status_message.tsx | 0 .../single_endpoint_streaming_demo}/index.ts | 7 +- .../single_endpoint_streaming_demo.tsx | 135 ++++++++++++++ .../stream_reducer.ts | 4 +- .../{components => hooks}/stream_fetch.ts | 35 +++- .../use_stream_fetch_reducer.ts | 23 ++- x-pack/plugins/aiops/public/index.ts | 4 +- .../plugins/aiops/public/kibana_services.ts | 19 -- .../aiops/public/lazy_load_bundle/index.ts | 30 ---- x-pack/plugins/aiops/public/plugin.ts | 11 +- .../aiops/public/shared_lazy_components.tsx | 42 +++++ .../server/lib/accept_compression.test.ts | 42 +++++ .../aiops/server/lib/accept_compression.ts | 44 +++++ .../aiops/server/lib/stream_factory.test.ts | 106 +++++++++++ .../aiops/server/lib/stream_factory.ts | 70 ++++++++ x-pack/plugins/aiops/server/plugin.ts | 27 ++- .../aiops/server/routes/example_stream.ts | 109 ++++++++++++ .../server/routes/explain_log_rate_spikes.ts | 90 ++++++++++ x-pack/plugins/aiops/server/routes/index.ts | 124 +------------ x-pack/plugins/aiops/server/types.ts | 10 ++ x-pack/plugins/ml/common/constants/locator.ts | 2 + x-pack/plugins/ml/common/types/locator.ts | 4 +- .../aiops/explain_log_rate_spikes.tsx | 40 ++--- .../aiops/single_endpoint_streaming_demo.tsx | 34 ++++ .../components/ml_page/side_nav.tsx | 11 +- .../application/contexts/ml/ml_context.ts | 6 +- .../public/application/routing/breadcrumbs.ts | 2 +- .../routes/aiops/explain_log_rate_spikes.tsx | 2 +- .../application/routing/routes/aiops/index.ts | 1 + .../aiops/single_endpoint_streaming_demo.tsx | 63 +++++++ .../routes/new_job/index_or_search.tsx | 30 ++++ .../plugins/ml/public/locator/ml_locator.ts | 2 + .../apis/aiops/example_stream.ts | 29 +-- .../apis/aiops/explain_log_rate_spikes.ts | 126 +++++++++++++ .../test/api_integration/apis/aiops/index.ts | 1 + .../apis/aiops/parse_stream.ts | 28 +++ 46 files changed, 1203 insertions(+), 481 deletions(-) create mode 100644 x-pack/plugins/aiops/common/api/explain_log_rate_spikes.ts delete mode 100644 x-pack/plugins/aiops/public/api/index.ts delete mode 100755 x-pack/plugins/aiops/public/components/app.tsx delete mode 100644 x-pack/plugins/aiops/public/components/explain_log_rate_spikes.tsx create mode 100644 x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes.tsx create mode 100644 x-pack/plugins/aiops/public/components/explain_log_rate_spikes/index.ts create mode 100644 x-pack/plugins/aiops/public/components/explain_log_rate_spikes/stream_reducer.ts rename x-pack/plugins/aiops/public/components/{ => single_endpoint_streaming_demo}/get_status_message.tsx (100%) rename x-pack/plugins/aiops/public/{lazy_load_bundle/lazy => components/single_endpoint_streaming_demo}/index.ts (52%) create mode 100644 x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/single_endpoint_streaming_demo.tsx rename x-pack/plugins/aiops/public/components/{ => single_endpoint_streaming_demo}/stream_reducer.ts (92%) rename x-pack/plugins/aiops/public/{components => hooks}/stream_fetch.ts (62%) rename x-pack/plugins/aiops/public/{components => hooks}/use_stream_fetch_reducer.ts (77%) delete mode 100644 x-pack/plugins/aiops/public/kibana_services.ts delete mode 100644 x-pack/plugins/aiops/public/lazy_load_bundle/index.ts create mode 100644 x-pack/plugins/aiops/public/shared_lazy_components.tsx create mode 100644 x-pack/plugins/aiops/server/lib/accept_compression.test.ts create mode 100644 x-pack/plugins/aiops/server/lib/accept_compression.ts create mode 100644 x-pack/plugins/aiops/server/lib/stream_factory.test.ts create mode 100644 x-pack/plugins/aiops/server/lib/stream_factory.ts create mode 100644 x-pack/plugins/aiops/server/routes/example_stream.ts create mode 100644 x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts create mode 100644 x-pack/plugins/ml/public/application/aiops/single_endpoint_streaming_demo.tsx create mode 100644 x-pack/plugins/ml/public/application/routing/routes/aiops/single_endpoint_streaming_demo.tsx create mode 100644 x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts create mode 100644 x-pack/test/api_integration/apis/aiops/parse_stream.ts diff --git a/x-pack/plugins/aiops/common/api/example_stream.ts b/x-pack/plugins/aiops/common/api/example_stream.ts index 1210cccf55487..ccef04fc8473a 100644 --- a/x-pack/plugins/aiops/common/api/example_stream.ts +++ b/x-pack/plugins/aiops/common/api/example_stream.ts @@ -65,4 +65,7 @@ export function deleteEntityAction(payload: string): ApiActionDeleteEntity { }; } -export type ApiAction = ApiActionUpdateProgress | ApiActionAddToEntity | ApiActionDeleteEntity; +export type AiopsExampleStreamApiAction = + | ApiActionUpdateProgress + | ApiActionAddToEntity + | ApiActionDeleteEntity; diff --git a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes.ts b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes.ts new file mode 100644 index 0000000000000..b5c5524cdef01 --- /dev/null +++ b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const aiopsExplainLogRateSpikesSchema = schema.object({ + /** The index to query for log rate spikes */ + index: schema.string(), +}); + +export type AiopsExplainLogRateSpikesSchema = TypeOf; + +export const API_ACTION_NAME = { + ADD_FIELDS: 'add_fields', +} as const; +export type ApiActionName = typeof API_ACTION_NAME[keyof typeof API_ACTION_NAME]; + +interface ApiActionAddFields { + type: typeof API_ACTION_NAME.ADD_FIELDS; + payload: string[]; +} + +export function addFieldsAction(payload: string[]): ApiActionAddFields { + return { + type: API_ACTION_NAME.ADD_FIELDS, + payload, + }; +} + +export type AiopsExplainLogRateSpikesApiAction = ApiActionAddFields; diff --git a/x-pack/plugins/aiops/common/api/index.ts b/x-pack/plugins/aiops/common/api/index.ts index da1e091d3fb54..6b987fef13d1a 100644 --- a/x-pack/plugins/aiops/common/api/index.ts +++ b/x-pack/plugins/aiops/common/api/index.ts @@ -5,15 +5,24 @@ * 2.0. */ -import type { AiopsExampleStreamSchema } from './example_stream'; +import type { + AiopsExplainLogRateSpikesSchema, + AiopsExplainLogRateSpikesApiAction, +} from './explain_log_rate_spikes'; +import type { AiopsExampleStreamSchema, AiopsExampleStreamApiAction } from './example_stream'; export const API_ENDPOINT = { EXAMPLE_STREAM: '/internal/aiops/example_stream', - ANOTHER: '/internal/aiops/another', + EXPLAIN_LOG_RATE_SPIKES: '/internal/aiops/explain_log_rate_spikes', } as const; export type ApiEndpoint = typeof API_ENDPOINT[keyof typeof API_ENDPOINT]; export interface ApiEndpointOptions { [API_ENDPOINT.EXAMPLE_STREAM]: AiopsExampleStreamSchema; - [API_ENDPOINT.ANOTHER]: { anotherOption: string }; + [API_ENDPOINT.EXPLAIN_LOG_RATE_SPIKES]: AiopsExplainLogRateSpikesSchema; +} + +export interface ApiEndpointActions { + [API_ENDPOINT.EXAMPLE_STREAM]: AiopsExampleStreamApiAction; + [API_ENDPOINT.EXPLAIN_LOG_RATE_SPIKES]: AiopsExplainLogRateSpikesApiAction; } diff --git a/x-pack/plugins/aiops/kibana.json b/x-pack/plugins/aiops/kibana.json index b74a23bf2bc9e..2d1e60bca74e3 100755 --- a/x-pack/plugins/aiops/kibana.json +++ b/x-pack/plugins/aiops/kibana.json @@ -9,7 +9,7 @@ "description": "AIOps plugin maintained by ML team.", "server": true, "ui": true, - "requiredPlugins": [], + "requiredPlugins": ["data"], "optionalPlugins": [], "requiredBundles": ["kibanaReact"], "extraPublicDirs": ["common"] diff --git a/x-pack/plugins/aiops/public/api/index.ts b/x-pack/plugins/aiops/public/api/index.ts deleted file mode 100644 index 6aa171df5286c..0000000000000 --- a/x-pack/plugins/aiops/public/api/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { lazyLoadModules } from '../lazy_load_bundle'; - -import type { ExplainLogRateSpikesSpec } from '../components/explain_log_rate_spikes'; - -export async function getExplainLogRateSpikesComponent(): Promise<() => ExplainLogRateSpikesSpec> { - const modules = await lazyLoadModules(); - return () => modules.ExplainLogRateSpikes; -} diff --git a/x-pack/plugins/aiops/public/components/app.tsx b/x-pack/plugins/aiops/public/components/app.tsx deleted file mode 100755 index 963253b154e27..0000000000000 --- a/x-pack/plugins/aiops/public/components/app.tsx +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState } from 'react'; - -import { Chart, Settings, Axis, BarSeries, Position, ScaleType } from '@elastic/charts'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; - -import { - EuiBadge, - EuiButton, - EuiCheckbox, - EuiFlexGroup, - EuiFlexItem, - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPageContentBody, - EuiPageContentHeader, - EuiProgress, - EuiSpacer, - EuiTitle, - EuiText, -} from '@elastic/eui'; - -import { getStatusMessage } from './get_status_message'; -import { initialState, resetStream, streamReducer } from './stream_reducer'; -import { useStreamFetchReducer } from './use_stream_fetch_reducer'; - -export const AiopsApp = () => { - const { notifications } = useKibana(); - - const [simulateErrors, setSimulateErrors] = useState(false); - - const { dispatch, start, cancel, data, isCancelled, isRunning } = useStreamFetchReducer( - '/internal/aiops/example_stream', - streamReducer, - initialState, - { simulateErrors } - ); - - const { errors, progress, entities } = data; - - const onClickHandler = async () => { - if (isRunning) { - cancel(); - } else { - dispatch(resetStream()); - start(); - } - }; - - useEffect(() => { - if (errors.length > 0) { - notifications.toasts.danger({ body: errors[errors.length - 1] }); - } - }, [errors, notifications.toasts]); - - const buttonLabel = isRunning - ? i18n.translate('xpack.aiops.stopbuttonText', { - defaultMessage: 'Stop development', - }) - : i18n.translate('xpack.aiops.startbuttonText', { - defaultMessage: 'Start development', - }); - - return ( - - - - - -

- -

-
-
- - - - - - {buttonLabel} - - - - - {progress}% - - - - - - - -
- - - - - - { - return { - x, - y, - }; - }) - .sort((a, b) => b.y - a.y)} - /> - -
-

{getStatusMessage(isRunning, isCancelled, data.progress)}

- setSimulateErrors(!simulateErrors)} - compressed - /> -
-
-
-
-
- ); -}; diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes.tsx deleted file mode 100644 index 21d7b39a2a148..0000000000000 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FC } from 'react'; - -import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; -import { I18nProvider } from '@kbn/i18n-react'; - -import { getCoreStart } from '../kibana_services'; - -import { AiopsApp } from './app'; - -/** - * Spec used for lazy loading in the ML plugin - */ -export type ExplainLogRateSpikesSpec = typeof ExplainLogRateSpikes; - -export const ExplainLogRateSpikes: FC = () => { - const coreStart = getCoreStart(); - - return ( - - - - - - - - ); -}; diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes.tsx new file mode 100644 index 0000000000000..12c4837194f80 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, FC } from 'react'; + +import { EuiBadge, EuiSpacer, EuiText } from '@elastic/eui'; + +import type { DataView } from '@kbn/data-views-plugin/public'; + +import { useStreamFetchReducer } from '../../hooks/use_stream_fetch_reducer'; + +import { initialState, streamReducer } from './stream_reducer'; + +/** + * ExplainLogRateSpikes props require a data view. + */ +export interface ExplainLogRateSpikesProps { + /** The data view to analyze. */ + dataView: DataView; +} + +export const ExplainLogRateSpikes: FC = ({ dataView }) => { + const { start, data, isRunning } = useStreamFetchReducer( + '/internal/aiops/explain_log_rate_spikes', + streamReducer, + initialState, + { index: dataView.title } + ); + + useEffect(() => { + start(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + +

{dataView.title}

+

{isRunning ? 'Loading fields ...' : 'Loaded all fields.'}

+ + {data.fields.map((field) => ( + {field} + ))} +
+ ); +}; diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/index.ts b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/index.ts new file mode 100644 index 0000000000000..3e48c6816dda9 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { ExplainLogRateSpikesProps } from './explain_log_rate_spikes'; +import { ExplainLogRateSpikes } from './explain_log_rate_spikes'; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default ExplainLogRateSpikes; diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/stream_reducer.ts b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/stream_reducer.ts new file mode 100644 index 0000000000000..7ec710f4ae65d --- /dev/null +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/stream_reducer.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + API_ACTION_NAME, + AiopsExplainLogRateSpikesApiAction, +} from '../../../common/api/explain_log_rate_spikes'; + +interface StreamState { + fields: string[]; +} + +export const initialState: StreamState = { + fields: [], +}; + +export function streamReducer( + state: StreamState, + action: AiopsExplainLogRateSpikesApiAction | AiopsExplainLogRateSpikesApiAction[] +): StreamState { + if (Array.isArray(action)) { + return action.reduce(streamReducer, state); + } + + switch (action.type) { + case API_ACTION_NAME.ADD_FIELDS: + return { + fields: [...state.fields, ...action.payload], + }; + default: + return state; + } +} diff --git a/x-pack/plugins/aiops/public/components/get_status_message.tsx b/x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/get_status_message.tsx similarity index 100% rename from x-pack/plugins/aiops/public/components/get_status_message.tsx rename to x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/get_status_message.tsx diff --git a/x-pack/plugins/aiops/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/index.ts similarity index 52% rename from x-pack/plugins/aiops/public/lazy_load_bundle/lazy/index.ts rename to x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/index.ts index 967525de9bd6e..38eb279568051 100644 --- a/x-pack/plugins/aiops/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/index.ts @@ -5,5 +5,8 @@ * 2.0. */ -export type { ExplainLogRateSpikesSpec } from '../../components/explain_log_rate_spikes'; -export { ExplainLogRateSpikes } from '../../components/explain_log_rate_spikes'; +import { SingleEndpointStreamingDemo } from './single_endpoint_streaming_demo'; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default SingleEndpointStreamingDemo; diff --git a/x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/single_endpoint_streaming_demo.tsx b/x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/single_endpoint_streaming_demo.tsx new file mode 100644 index 0000000000000..12f33aada133c --- /dev/null +++ b/x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/single_endpoint_streaming_demo.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState, FC } from 'react'; + +import { Chart, Settings, Axis, BarSeries, Position, ScaleType } from '@elastic/charts'; + +import { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +import { + EuiBadge, + EuiButton, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { useStreamFetchReducer } from '../../hooks/use_stream_fetch_reducer'; + +import { getStatusMessage } from './get_status_message'; +import { initialState, resetStream, streamReducer } from './stream_reducer'; + +export const SingleEndpointStreamingDemo: FC = () => { + const { notifications } = useKibana(); + + const [simulateErrors, setSimulateErrors] = useState(false); + + const { dispatch, start, cancel, data, isCancelled, isRunning } = useStreamFetchReducer( + '/internal/aiops/example_stream', + streamReducer, + initialState, + { simulateErrors } + ); + + const { errors, progress, entities } = data; + + const onClickHandler = async () => { + if (isRunning) { + cancel(); + } else { + dispatch(resetStream()); + start(); + } + }; + + useEffect(() => { + if (errors.length > 0) { + notifications.toasts.danger({ body: errors[errors.length - 1] }); + } + }, [errors, notifications.toasts]); + + const buttonLabel = isRunning + ? i18n.translate('xpack.aiops.stopbuttonText', { + defaultMessage: 'Stop development', + }) + : i18n.translate('xpack.aiops.startbuttonText', { + defaultMessage: 'Start development', + }); + + return ( + + + + + {buttonLabel} + + + + + {progress}% + + + + + + + +
+ + + + + + { + return { + x, + y, + }; + }) + .sort((a, b) => b.y - a.y)} + /> + +
+

{getStatusMessage(isRunning, isCancelled, data.progress)}

+ setSimulateErrors(!simulateErrors)} + compressed + /> +
+ ); +}; diff --git a/x-pack/plugins/aiops/public/components/stream_reducer.ts b/x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/stream_reducer.ts similarity index 92% rename from x-pack/plugins/aiops/public/components/stream_reducer.ts rename to x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/stream_reducer.ts index 3e68e139ceeca..a3e9724f24a1f 100644 --- a/x-pack/plugins/aiops/public/components/stream_reducer.ts +++ b/x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/stream_reducer.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ApiAction, API_ACTION_NAME } from '../../common/api/example_stream'; +import { AiopsExampleStreamApiAction, API_ACTION_NAME } from '../../../common/api/example_stream'; export const UI_ACTION_NAME = { ERROR: 'error', @@ -37,7 +37,7 @@ export function resetStream(): UiActionResetStream { } type UiAction = UiActionResetStream | UiActionError; -export type ReducerAction = ApiAction | UiAction; +export type ReducerAction = AiopsExampleStreamApiAction | UiAction; export function streamReducer( state: StreamState, action: ReducerAction | ReducerAction[] diff --git a/x-pack/plugins/aiops/public/components/stream_fetch.ts b/x-pack/plugins/aiops/public/hooks/stream_fetch.ts similarity index 62% rename from x-pack/plugins/aiops/public/components/stream_fetch.ts rename to x-pack/plugins/aiops/public/hooks/stream_fetch.ts index 37d7c13dd3b55..abfec63702012 100644 --- a/x-pack/plugins/aiops/public/components/stream_fetch.ts +++ b/x-pack/plugins/aiops/public/hooks/stream_fetch.ts @@ -7,14 +7,19 @@ import type React from 'react'; -import type { ApiEndpoint, ApiEndpointOptions } from '../../common/api'; +import type { ApiEndpoint, ApiEndpointActions, ApiEndpointOptions } from '../../common/api'; -export async function* streamFetch( +interface ErrorAction { + type: 'error'; + payload: string; +} + +export async function* streamFetch( endpoint: E, abortCtrl: React.MutableRefObject, - options: ApiEndpointOptions[ApiEndpoint], + options: ApiEndpointOptions[E], basePath = '' -) { +): AsyncGenerator> { const stream = await fetch(`${basePath}${endpoint}`, { signal: abortCtrl.current.signal, method: 'POST', @@ -36,7 +41,7 @@ export async function* streamFetch( const bufferBounce = 100; let partial = ''; - let actionBuffer: A[] = []; + let actionBuffer: Array = []; let lastCall = 0; while (true) { @@ -52,7 +57,7 @@ export async function* streamFetch( partial = last ?? ''; - const actions = parts.map((p) => JSON.parse(p)); + const actions = parts.map((p) => JSON.parse(p)) as Array; actionBuffer.push(...actions); const now = Date.now(); @@ -61,10 +66,26 @@ export async function* streamFetch( yield actionBuffer; actionBuffer = []; lastCall = now; + + // In cases where the next chunk takes longer to be received than the `bufferBounce` timeout, + // we trigger this client side timeout to clear a potential intermediate buffer state. + // Since `yield` cannot be passed on to other scopes like callbacks, + // this pattern using a Promise is used to wait for the timeout. + yield new Promise>((resolve) => { + setTimeout(() => { + if (actionBuffer.length > 0) { + resolve(actionBuffer); + actionBuffer = []; + lastCall = now; + } else { + resolve([]); + } + }, bufferBounce + 10); + }); } } catch (error) { if (error.name !== 'AbortError') { - yield { type: 'error', payload: error.toString() }; + yield [{ type: 'error', payload: error.toString() }]; } break; } diff --git a/x-pack/plugins/aiops/public/components/use_stream_fetch_reducer.ts b/x-pack/plugins/aiops/public/hooks/use_stream_fetch_reducer.ts similarity index 77% rename from x-pack/plugins/aiops/public/components/use_stream_fetch_reducer.ts rename to x-pack/plugins/aiops/public/hooks/use_stream_fetch_reducer.ts index 77ac09e0ff429..ba64831bec60e 100644 --- a/x-pack/plugins/aiops/public/components/use_stream_fetch_reducer.ts +++ b/x-pack/plugins/aiops/public/hooks/use_stream_fetch_reducer.ts @@ -5,7 +5,15 @@ * 2.0. */ -import { useReducer, useRef, useState, Reducer, ReducerAction, ReducerState } from 'react'; +import { + useEffect, + useReducer, + useRef, + useState, + Reducer, + ReducerAction, + ReducerState, +} from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; @@ -13,11 +21,11 @@ import type { ApiEndpoint, ApiEndpointOptions } from '../../common/api'; import { streamFetch } from './stream_fetch'; -export const useStreamFetchReducer = , E = ApiEndpoint>( +export const useStreamFetchReducer = , E extends ApiEndpoint>( endpoint: E, reducer: R, initialState: ReducerState, - options: ApiEndpointOptions[ApiEndpoint] + options: ApiEndpointOptions[E] ) => { const kibana = useKibana(); @@ -44,7 +52,9 @@ export const useStreamFetchReducer = , E = ApiEndpoi options, kibana.services.http?.basePath.get() )) { - dispatch(actions as ReducerAction); + if (actions.length > 0) { + dispatch(actions as ReducerAction); + } } setIsRunning(false); @@ -56,6 +66,11 @@ export const useStreamFetchReducer = , E = ApiEndpoi setIsRunning(false); }; + // If components using this custom hook get unmounted, cancel any ongoing request. + useEffect(() => { + return () => abortCtrl.current.abort(); + }, []); + return { cancel, data, diff --git a/x-pack/plugins/aiops/public/index.ts b/x-pack/plugins/aiops/public/index.ts index 30bcaf5afabdc..53fc1d7a6eeca 100755 --- a/x-pack/plugins/aiops/public/index.ts +++ b/x-pack/plugins/aiops/public/index.ts @@ -13,6 +13,6 @@ export function plugin() { return new AiopsPlugin(); } +export type { ExplainLogRateSpikesProps } from './components/explain_log_rate_spikes'; +export { ExplainLogRateSpikes, SingleEndpointStreamingDemo } from './shared_lazy_components'; export type { AiopsPluginSetup, AiopsPluginStart } from './types'; - -export type { ExplainLogRateSpikesSpec } from './components/explain_log_rate_spikes'; diff --git a/x-pack/plugins/aiops/public/kibana_services.ts b/x-pack/plugins/aiops/public/kibana_services.ts deleted file mode 100644 index 9a43d2de5e5a1..0000000000000 --- a/x-pack/plugins/aiops/public/kibana_services.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreStart } from '@kbn/core/public'; -import { AppPluginStartDependencies } from './types'; - -let coreStart: CoreStart; -let pluginsStart: AppPluginStartDependencies; -export function setStartServices(core: CoreStart, plugins: AppPluginStartDependencies) { - coreStart = core; - pluginsStart = plugins; -} - -export const getCoreStart = () => coreStart; -export const getPluginsStart = () => pluginsStart; diff --git a/x-pack/plugins/aiops/public/lazy_load_bundle/index.ts b/x-pack/plugins/aiops/public/lazy_load_bundle/index.ts deleted file mode 100644 index 0072336080175..0000000000000 --- a/x-pack/plugins/aiops/public/lazy_load_bundle/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ExplainLogRateSpikesSpec } from '../components/explain_log_rate_spikes'; - -let loadModulesPromise: Promise; - -interface LazyLoadedModules { - ExplainLogRateSpikes: ExplainLogRateSpikesSpec; -} - -export async function lazyLoadModules(): Promise { - if (typeof loadModulesPromise !== 'undefined') { - return loadModulesPromise; - } - - loadModulesPromise = new Promise(async (resolve, reject) => { - try { - const lazyImports = await import('./lazy'); - resolve({ ...lazyImports }); - } catch (error) { - reject(error); - } - }); - return loadModulesPromise; -} diff --git a/x-pack/plugins/aiops/public/plugin.ts b/x-pack/plugins/aiops/public/plugin.ts index 3c3cff39abb80..ef65ab247c40f 100755 --- a/x-pack/plugins/aiops/public/plugin.ts +++ b/x-pack/plugins/aiops/public/plugin.ts @@ -7,19 +7,10 @@ import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; -import { getExplainLogRateSpikesComponent } from './api'; -import { setStartServices } from './kibana_services'; import { AiopsPluginSetup, AiopsPluginStart } from './types'; export class AiopsPlugin implements Plugin { public setup(core: CoreSetup) {} - - public start(core: CoreStart) { - setStartServices(core, {}); - return { - getExplainLogRateSpikesComponent, - }; - } - + public start(core: CoreStart) {} public stop() {} } diff --git a/x-pack/plugins/aiops/public/shared_lazy_components.tsx b/x-pack/plugins/aiops/public/shared_lazy_components.tsx new file mode 100644 index 0000000000000..f707a77cf7f90 --- /dev/null +++ b/x-pack/plugins/aiops/public/shared_lazy_components.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, Suspense } from 'react'; + +import { EuiErrorBoundary, EuiLoadingContent } from '@elastic/eui'; + +import type { ExplainLogRateSpikesProps } from './components/explain_log_rate_spikes'; + +const ExplainLogRateSpikesLazy = React.lazy(() => import('./components/explain_log_rate_spikes')); +const SingleEndpointStreamingDemoLazy = React.lazy( + () => import('./components/single_endpoint_streaming_demo') +); + +const LazyWrapper: FC = ({ children }) => ( + + }>{children} + +); + +/** + * Lazy-wrapped ExplainLogRateSpikes React component + * @param {ExplainLogRateSpikesProps} props - properties specifying the data on which to run the analysis. + */ +export const ExplainLogRateSpikes: FC = (props) => ( + + + +); + +/** + * Lazy-wrapped SingleEndpointStreamingDemo React component + */ +export const SingleEndpointStreamingDemo: FC = () => ( + + + +); diff --git a/x-pack/plugins/aiops/server/lib/accept_compression.test.ts b/x-pack/plugins/aiops/server/lib/accept_compression.test.ts new file mode 100644 index 0000000000000..f1c51f75cbe0c --- /dev/null +++ b/x-pack/plugins/aiops/server/lib/accept_compression.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { acceptCompression } from './accept_compression'; + +describe('acceptCompression', () => { + it('should return false for empty headers', () => { + expect(acceptCompression({})).toBe(false); + }); + it('should return false for other header containing gzip as string', () => { + expect(acceptCompression({ 'other-header': 'gzip, other' })).toBe(false); + }); + it('should return false for other header containing gzip as array', () => { + expect(acceptCompression({ 'other-header': ['gzip', 'other'] })).toBe(false); + }); + it('should return true for upper-case header containing gzip as string', () => { + expect(acceptCompression({ 'Accept-Encoding': 'gzip, other' })).toBe(true); + }); + it('should return true for lower-case header containing gzip as string', () => { + expect(acceptCompression({ 'accept-encoding': 'gzip, other' })).toBe(true); + }); + it('should return true for upper-case header containing gzip as array', () => { + expect(acceptCompression({ 'Accept-Encoding': ['gzip', 'other'] })).toBe(true); + }); + it('should return true for lower-case header containing gzip as array', () => { + expect(acceptCompression({ 'accept-encoding': ['gzip', 'other'] })).toBe(true); + }); + it('should return true for mixed headers containing gzip as string', () => { + expect( + acceptCompression({ 'accept-encoding': 'gzip, other', 'other-header': 'other-value' }) + ).toBe(true); + }); + it('should return true for mixed headers containing gzip as array', () => { + expect( + acceptCompression({ 'accept-encoding': ['gzip', 'other'], 'other-header': 'other-value' }) + ).toBe(true); + }); +}); diff --git a/x-pack/plugins/aiops/server/lib/accept_compression.ts b/x-pack/plugins/aiops/server/lib/accept_compression.ts new file mode 100644 index 0000000000000..0fd092d647314 --- /dev/null +++ b/x-pack/plugins/aiops/server/lib/accept_compression.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Headers } from '@kbn/core/server'; + +/** + * Returns whether request headers accept a response using gzip compression. + * + * @param headers - Request headers. + * @returns boolean + */ +export function acceptCompression(headers: Headers) { + let compressed = false; + + Object.keys(headers).forEach((key) => { + if (key.toLocaleLowerCase() === 'accept-encoding') { + const acceptEncoding = headers[key]; + + function containsGzip(s: string) { + return s + .split(',') + .map((d) => d.trim()) + .includes('gzip'); + } + + if (typeof acceptEncoding === 'string') { + compressed = containsGzip(acceptEncoding); + } else if (Array.isArray(acceptEncoding)) { + for (const ae of acceptEncoding) { + if (containsGzip(ae)) { + compressed = true; + break; + } + } + } + } + }); + + return compressed; +} diff --git a/x-pack/plugins/aiops/server/lib/stream_factory.test.ts b/x-pack/plugins/aiops/server/lib/stream_factory.test.ts new file mode 100644 index 0000000000000..7082a4e7e763c --- /dev/null +++ b/x-pack/plugins/aiops/server/lib/stream_factory.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import zlib from 'zlib'; + +import { loggerMock, MockedLogger } from '@kbn/logging-mocks'; + +import { API_ENDPOINT } from '../../common/api'; +import type { ApiEndpointActions } from '../../common/api'; + +import { streamFactory } from './stream_factory'; + +type Action = ApiEndpointActions['/internal/aiops/explain_log_rate_spikes']; + +const mockItem1: Action = { + type: 'add_fields', + payload: ['clientip'], +}; +const mockItem2: Action = { + type: 'add_fields', + payload: ['referer'], +}; + +describe('streamFactory', () => { + let mockLogger: MockedLogger; + + beforeEach(() => { + mockLogger = loggerMock.create(); + }); + + it('should encode and receive an uncompressed stream', async () => { + const { DELIMITER, end, push, responseWithHeaders, stream } = streamFactory< + typeof API_ENDPOINT.EXPLAIN_LOG_RATE_SPIKES + >(mockLogger, {}); + + push(mockItem1); + push(mockItem2); + end(); + + let streamResult = ''; + for await (const chunk of stream) { + streamResult += chunk.toString('utf8'); + } + + const streamItems = streamResult.split(DELIMITER); + const lastItem = streamItems.pop(); + + const parsedItems = streamItems.map((d) => JSON.parse(d)); + + expect(responseWithHeaders.headers).toBe(undefined); + expect(parsedItems).toHaveLength(2); + expect(parsedItems[0]).toStrictEqual(mockItem1); + expect(parsedItems[1]).toStrictEqual(mockItem2); + expect(lastItem).toBe(''); + }); + + // Because zlib.gunzip's API expects a callback, we need to use `done` here + // to indicate once all assertions are run. However, it's not allowed to use both + // `async` and `done` for the test callback. That's why we're using an "async IIFE" + // pattern inside the tests callback to still be able to do async/await for the + // `for await()` part. Note that the unzipping here is done just to be able to + // decode the stream for the test and assert it. When used in actual code, + // the browser on the client side will automatically take care of unzipping + // without the need for additional custom code. + it('should encode and receive a compressed stream', (done) => { + (async () => { + const { DELIMITER, end, push, responseWithHeaders, stream } = streamFactory< + typeof API_ENDPOINT.EXPLAIN_LOG_RATE_SPIKES + >(mockLogger, { 'accept-encoding': 'gzip' }); + + push(mockItem1); + push(mockItem2); + end(); + + const chunks = []; + for await (const chunk of stream) { + chunks.push(chunk); + } + + const buffer = Buffer.concat(chunks); + + zlib.gunzip(buffer, function (err, decoded) { + expect(err).toBe(null); + + const streamResult = decoded.toString('utf8'); + + const streamItems = streamResult.split(DELIMITER); + const lastItem = streamItems.pop(); + + const parsedItems = streamItems.map((d) => JSON.parse(d)); + + expect(responseWithHeaders.headers).toStrictEqual({ 'content-encoding': 'gzip' }); + expect(parsedItems).toHaveLength(2); + expect(parsedItems[0]).toStrictEqual(mockItem1); + expect(parsedItems[1]).toStrictEqual(mockItem2); + expect(lastItem).toBe(''); + + done(); + }); + })(); + }); +}); diff --git a/x-pack/plugins/aiops/server/lib/stream_factory.ts b/x-pack/plugins/aiops/server/lib/stream_factory.ts new file mode 100644 index 0000000000000..dc67a54902527 --- /dev/null +++ b/x-pack/plugins/aiops/server/lib/stream_factory.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Stream } from 'stream'; +import zlib from 'zlib'; + +import type { Headers, Logger } from '@kbn/core/server'; + +import { ApiEndpoint, ApiEndpointActions } from '../../common/api'; + +import { acceptCompression } from './accept_compression'; + +// We need this otherwise Kibana server will crash with a 'ERR_METHOD_NOT_IMPLEMENTED' error. +class ResponseStream extends Stream.PassThrough { + flush() {} + _read() {} +} + +const DELIMITER = '\n'; + +/** + * Sets up a response stream with support for gzip compression depending on provided + * request headers. + * + * @param logger - Kibana provided logger. + * @param headers - Request headers. + * @returns An object with stream attributes and methods. + */ +export function streamFactory(logger: Logger, headers: Headers) { + const isCompressed = acceptCompression(headers); + + const stream = isCompressed ? zlib.createGzip() : new ResponseStream(); + + function push(d: ApiEndpointActions[T]) { + try { + const line = JSON.stringify(d); + stream.write(`${line}${DELIMITER}`); + + // Calling .flush() on a compression stream will + // make zlib return as much output as currently possible. + if (isCompressed) { + stream.flush(); + } + } catch (error) { + logger.error('Could not serialize or stream a message.'); + logger.error(error); + } + } + + function end() { + stream.end(); + } + + const responseWithHeaders = { + body: stream, + ...(isCompressed + ? { + headers: { + 'content-encoding': 'gzip', + }, + } + : {}), + }; + + return { DELIMITER, end, push, responseWithHeaders, stream }; +} diff --git a/x-pack/plugins/aiops/server/plugin.ts b/x-pack/plugins/aiops/server/plugin.ts index c6b1b8b22a187..3743d32e3a081 100755 --- a/x-pack/plugins/aiops/server/plugin.ts +++ b/x-pack/plugins/aiops/server/plugin.ts @@ -6,23 +6,38 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; +import type { DataRequestHandlerContext } from '@kbn/data-plugin/server'; -import { AiopsPluginSetup, AiopsPluginStart } from './types'; -import { defineRoutes } from './routes'; +import { AIOPS_ENABLED } from '../common'; -export class AiopsPlugin implements Plugin { +import { + AiopsPluginSetup, + AiopsPluginStart, + AiopsPluginSetupDeps, + AiopsPluginStartDeps, +} from './types'; +import { defineExampleStreamRoute, defineExplainLogRateSpikesRoute } from './routes'; + +export class AiopsPlugin + implements Plugin +{ private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, deps: AiopsPluginSetupDeps) { this.logger.debug('aiops: Setup'); - const router = core.http.createRouter(); + const router = core.http.createRouter(); // Register server side APIs - defineRoutes(router, this.logger); + if (AIOPS_ENABLED) { + core.getStartServices().then(([_, depsStart]) => { + defineExampleStreamRoute(router, this.logger); + defineExplainLogRateSpikesRoute(router, this.logger); + }); + } return {}; } diff --git a/x-pack/plugins/aiops/server/routes/example_stream.ts b/x-pack/plugins/aiops/server/routes/example_stream.ts new file mode 100644 index 0000000000000..38ca28ce6f176 --- /dev/null +++ b/x-pack/plugins/aiops/server/routes/example_stream.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IRouter, Logger } from '@kbn/core/server'; + +import { + aiopsExampleStreamSchema, + updateProgressAction, + addToEntityAction, + deleteEntityAction, +} from '../../common/api/example_stream'; +import { API_ENDPOINT } from '../../common/api'; + +import { streamFactory } from '../lib/stream_factory'; + +export const defineExampleStreamRoute = (router: IRouter, logger: Logger) => { + router.post( + { + path: API_ENDPOINT.EXAMPLE_STREAM, + validate: { + body: aiopsExampleStreamSchema, + }, + }, + async (context, request, response) => { + const maxTimeoutMs = request.body.timeout ?? 250; + const simulateError = request.body.simulateErrors ?? false; + + let shouldStop = false; + request.events.aborted$.subscribe(() => { + shouldStop = true; + }); + request.events.completed$.subscribe(() => { + shouldStop = true; + }); + + const { DELIMITER, end, push, responseWithHeaders, stream } = streamFactory< + typeof API_ENDPOINT.EXAMPLE_STREAM + >(logger, request.headers); + + const entities = [ + 'kimchy', + 's1monw', + 'martijnvg', + 'jasontedor', + 'nik9000', + 'javanna', + 'rjernst', + 'jrodewig', + ]; + + const actions = [...Array(19).fill('add'), 'delete']; + + if (simulateError) { + actions.push('server-only-error'); + actions.push('server-to-client-error'); + actions.push('client-error'); + } + + let progress = 0; + + async function pushStreamUpdate() { + setTimeout(() => { + try { + progress++; + + if (progress > 100 || shouldStop) { + end(); + return; + } + + push(updateProgressAction(progress)); + + const randomEntity = entities[Math.floor(Math.random() * entities.length)]; + const randomAction = actions[Math.floor(Math.random() * actions.length)]; + + if (randomAction === 'add') { + const randomCommits = Math.floor(Math.random() * 100); + push(addToEntityAction(randomEntity, randomCommits)); + } else if (randomAction === 'delete') { + push(deleteEntityAction(randomEntity)); + } else if (randomAction === 'server-to-client-error') { + // Throw an error. It should not crash Kibana! + throw new Error('There was a (simulated) server side error!'); + } else if (randomAction === 'client-error') { + // Return not properly encoded JSON to the client. + stream.push(`{body:'Not valid JSON${DELIMITER}`); + } + + pushStreamUpdate(); + } catch (error) { + stream.push( + `${JSON.stringify({ type: 'error', payload: error.toString() })}${DELIMITER}` + ); + end(); + } + }, Math.floor(Math.random() * maxTimeoutMs)); + } + + // do not call this using `await` so it will run asynchronously while we return the stream already. + pushStreamUpdate(); + + return response.ok(responseWithHeaders); + } + ); +}; diff --git a/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts b/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts new file mode 100644 index 0000000000000..f8aeb06435b76 --- /dev/null +++ b/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { firstValueFrom } from 'rxjs'; + +import type { IRouter, Logger } from '@kbn/core/server'; +import type { DataRequestHandlerContext, IEsSearchRequest } from '@kbn/data-plugin/server'; + +import { + aiopsExplainLogRateSpikesSchema, + addFieldsAction, +} from '../../common/api/explain_log_rate_spikes'; +import { API_ENDPOINT } from '../../common/api'; + +import { streamFactory } from '../lib/stream_factory'; + +export const defineExplainLogRateSpikesRoute = ( + router: IRouter, + logger: Logger +) => { + router.post( + { + path: API_ENDPOINT.EXPLAIN_LOG_RATE_SPIKES, + validate: { + body: aiopsExplainLogRateSpikesSchema, + }, + }, + async (context, request, response) => { + const index = request.body.index; + + const controller = new AbortController(); + + let shouldStop = false; + request.events.aborted$.subscribe(() => { + shouldStop = true; + controller.abort(); + }); + request.events.completed$.subscribe(() => { + shouldStop = true; + controller.abort(); + }); + + const search = await context.search; + const res = await firstValueFrom( + search.search( + { + params: { + index, + body: { size: 1 }, + }, + } as IEsSearchRequest, + { abortSignal: controller.signal } + ) + ); + + const doc = res.rawResponse.hits.hits.pop(); + const fields = Object.keys(doc?._source ?? {}); + + const { end, push, responseWithHeaders } = streamFactory< + typeof API_ENDPOINT.EXPLAIN_LOG_RATE_SPIKES + >(logger, request.headers); + + async function pushField() { + setTimeout(() => { + if (shouldStop) { + end(); + return; + } + + const field = fields.pop(); + + if (field !== undefined) { + push(addFieldsAction([field])); + pushField(); + } else { + end(); + } + }, Math.random() * 1000); + } + + pushField(); + + return response.ok(responseWithHeaders); + } + ); +}; diff --git a/x-pack/plugins/aiops/server/routes/index.ts b/x-pack/plugins/aiops/server/routes/index.ts index e87c27e2af81e..d69ef6cc7df09 100755 --- a/x-pack/plugins/aiops/server/routes/index.ts +++ b/x-pack/plugins/aiops/server/routes/index.ts @@ -5,125 +5,5 @@ * 2.0. */ -import { Readable } from 'stream'; - -import type { IRouter, Logger } from '@kbn/core/server'; - -import { AIOPS_ENABLED } from '../../common'; -import type { ApiAction } from '../../common/api/example_stream'; -import { - aiopsExampleStreamSchema, - updateProgressAction, - addToEntityAction, - deleteEntityAction, -} from '../../common/api/example_stream'; - -// We need this otherwise Kibana server will crash with a 'ERR_METHOD_NOT_IMPLEMENTED' error. -class ResponseStream extends Readable { - _read(): void {} -} - -const delimiter = '\n'; - -export function defineRoutes(router: IRouter, logger: Logger) { - if (AIOPS_ENABLED) { - router.post( - { - path: '/internal/aiops/example_stream', - validate: { - body: aiopsExampleStreamSchema, - }, - }, - async (context, request, response) => { - const maxTimeoutMs = request.body.timeout ?? 250; - const simulateError = request.body.simulateErrors ?? false; - - let shouldStop = false; - request.events.aborted$.subscribe(() => { - shouldStop = true; - }); - request.events.completed$.subscribe(() => { - shouldStop = true; - }); - - const stream = new ResponseStream(); - - function streamPush(d: ApiAction) { - try { - const line = JSON.stringify(d); - stream.push(`${line}${delimiter}`); - } catch (error) { - logger.error('Could not serialize or stream a message.'); - logger.error(error); - } - } - - const entities = [ - 'kimchy', - 's1monw', - 'martijnvg', - 'jasontedor', - 'nik9000', - 'javanna', - 'rjernst', - 'jrodewig', - ]; - - const actions = [...Array(19).fill('add'), 'delete']; - - if (simulateError) { - actions.push('server-only-error'); - actions.push('server-to-client-error'); - actions.push('client-error'); - } - - let progress = 0; - - async function pushStreamUpdate() { - setTimeout(() => { - try { - progress++; - - if (progress > 100 || shouldStop) { - stream.push(null); - return; - } - - streamPush(updateProgressAction(progress)); - - const randomEntity = entities[Math.floor(Math.random() * entities.length)]; - const randomAction = actions[Math.floor(Math.random() * actions.length)]; - - if (randomAction === 'add') { - const randomCommits = Math.floor(Math.random() * 100); - streamPush(addToEntityAction(randomEntity, randomCommits)); - } else if (randomAction === 'delete') { - streamPush(deleteEntityAction(randomEntity)); - } else if (randomAction === 'server-to-client-error') { - // Throw an error. It should not crash Kibana! - throw new Error('There was a (simulated) server side error!'); - } else if (randomAction === 'client-error') { - // Return not properly encoded JSON to the client. - stream.push(`{body:'Not valid JSON${delimiter}`); - } - - pushStreamUpdate(); - } catch (error) { - stream.push( - `${JSON.stringify({ type: 'error', payload: error.toString() })}${delimiter}` - ); - stream.push(null); - } - }, Math.floor(Math.random() * maxTimeoutMs)); - } - - // do not call this using `await` so it will run asynchronously while we return the stream already. - pushStreamUpdate(); - - return response.ok({ - body: stream, - }); - } - ); - } -} +export { defineExampleStreamRoute } from './example_stream'; +export { defineExplainLogRateSpikesRoute } from './explain_log_rate_spikes'; diff --git a/x-pack/plugins/aiops/server/types.ts b/x-pack/plugins/aiops/server/types.ts index 526e7280e9495..3d27a9625db4c 100755 --- a/x-pack/plugins/aiops/server/types.ts +++ b/x-pack/plugins/aiops/server/types.ts @@ -5,6 +5,16 @@ * 2.0. */ +import { PluginSetup, PluginStart } from '@kbn/data-plugin/server'; + +export interface AiopsPluginSetupDeps { + data: PluginSetup; +} + +export interface AiopsPluginStartDeps { + data: PluginStart; +} + /** * aiops plugin server setup contract */ diff --git a/x-pack/plugins/ml/common/constants/locator.ts b/x-pack/plugins/ml/common/constants/locator.ts index 7b98eefe0ab24..a5b94836e5a1d 100644 --- a/x-pack/plugins/ml/common/constants/locator.ts +++ b/x-pack/plugins/ml/common/constants/locator.ts @@ -54,6 +54,8 @@ export const ML_PAGES = { OVERVIEW: 'overview', AIOPS: 'aiops', AIOPS_EXPLAIN_LOG_RATE_SPIKES: 'aiops/explain_log_rate_spikes', + AIOPS_EXPLAIN_LOG_RATE_SPIKES_INDEX_SELECT: 'aiops/explain_log_rate_spikes_index_select', + AIOPS_SINGLE_ENDPOINT_STREAMING_DEMO: 'aiops/single_endpoint_streaming_demo', } as const; export type MlPages = typeof ML_PAGES[keyof typeof ML_PAGES]; diff --git a/x-pack/plugins/ml/common/types/locator.ts b/x-pack/plugins/ml/common/types/locator.ts index 0d5cb7aeddd81..742486c78b5bf 100644 --- a/x-pack/plugins/ml/common/types/locator.ts +++ b/x-pack/plugins/ml/common/types/locator.ts @@ -63,7 +63,9 @@ export type MlGenericUrlState = MLPageState< | typeof ML_PAGES.DATA_VISUALIZER_FILE | typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT | typeof ML_PAGES.AIOPS - | typeof ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES, + | typeof ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES + | typeof ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES_INDEX_SELECT + | typeof ML_PAGES.AIOPS_SINGLE_ENDPOINT_STREAMING_DEMO, MlGenericUrlPageState | undefined >; diff --git a/x-pack/plugins/ml/public/application/aiops/explain_log_rate_spikes.tsx b/x-pack/plugins/ml/public/application/aiops/explain_log_rate_spikes.tsx index 473525d40ca9a..39fa5272799fd 100644 --- a/x-pack/plugins/ml/public/application/aiops/explain_log_rate_spikes.tsx +++ b/x-pack/plugins/ml/public/application/aiops/explain_log_rate_spikes.tsx @@ -5,44 +5,32 @@ * 2.0. */ -import React, { FC, useEffect, useState } from 'react'; +import React, { FC } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { ExplainLogRateSpikesSpec } from '@kbn/aiops-plugin/public'; -import { useMlKibana, useTimefilter } from '../contexts/kibana'; +import { ExplainLogRateSpikes } from '@kbn/aiops-plugin/public'; + +import { useMlContext } from '../contexts/ml'; +import { useMlKibana } from '../contexts/kibana'; import { HelpMenu } from '../components/help_menu'; import { MlPageHeader } from '../components/page_header'; export const ExplainLogRateSpikesPage: FC = () => { - useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); const { - services: { docLinks, aiops }, + services: { docLinks }, } = useMlKibana(); - const [ExplainLogRateSpikes, setExplainLogRateSpikes] = useState( - null - ); - - useEffect(() => { - if (aiops !== undefined) { - const { getExplainLogRateSpikesComponent } = aiops; - getExplainLogRateSpikesComponent().then(setExplainLogRateSpikes); - } - }, []); + const context = useMlContext(); return ( <> - {ExplainLogRateSpikes !== null ? ( - <> - - - - - - ) : null} + + + + ); diff --git a/x-pack/plugins/ml/public/application/aiops/single_endpoint_streaming_demo.tsx b/x-pack/plugins/ml/public/application/aiops/single_endpoint_streaming_demo.tsx new file mode 100644 index 0000000000000..fa2bc7f7051e4 --- /dev/null +++ b/x-pack/plugins/ml/public/application/aiops/single_endpoint_streaming_demo.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { SingleEndpointStreamingDemo } from '@kbn/aiops-plugin/public'; +import { useMlKibana, useTimefilter } from '../contexts/kibana'; +import { HelpMenu } from '../components/help_menu'; + +import { MlPageHeader } from '../components/page_header'; + +export const SingleEndpointStreamingDemoPage: FC = () => { + useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); + const { + services: { docLinks }, + } = useMlKibana(); + + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx index 84474e85330d6..250dbc52cfd9c 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx @@ -229,13 +229,22 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) { items: [ { id: 'explainlogratespikes', - pathId: ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES, + pathId: ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES_INDEX_SELECT, name: i18n.translate('xpack.ml.navMenu.explainLogRateSpikesLinkText', { defaultMessage: 'Explain log rate spikes', }), disabled: disableLinks, testSubj: 'mlMainTab explainLogRateSpikes', }, + { + id: 'singleEndpointStreamingDemo', + pathId: ML_PAGES.AIOPS_SINGLE_ENDPOINT_STREAMING_DEMO, + name: i18n.translate('xpack.ml.navMenu.singleEndpointStreamingDemoLinkText', { + defaultMessage: 'Single endpoint streaming demo', + }), + disabled: disableLinks, + testSubj: 'mlMainTab singleEndpointStreamingDemo', + }, ], }); } diff --git a/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts b/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts index 2a8806bf3ff38..8b755b02f99b9 100644 --- a/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts @@ -6,9 +6,9 @@ */ import React from 'react'; -import { DataView, DataViewsContract } from '@kbn/data-views-plugin/public'; -import { SavedSearchSavedObject } from '../../../../common/types/kibana'; -import { MlServicesContext } from '../../app'; +import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public'; +import type { SavedSearchSavedObject } from '../../../../common/types/kibana'; +import type { MlServicesContext } from '../../app'; export interface MlContextValue { combinedQuery: any; diff --git a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts index 54aedb4a71857..38ace0233cbb8 100644 --- a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts +++ b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts @@ -59,7 +59,7 @@ export const AIOPS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.aiopsBreadcrumbLabel', { defaultMessage: 'AIOps', }), - href: '/aiops', + href: '/aiops/explain_log_rate_spikes_index_select', }); export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ diff --git a/x-pack/plugins/ml/public/application/routing/routes/aiops/explain_log_rate_spikes.tsx b/x-pack/plugins/ml/public/application/routing/routes/aiops/explain_log_rate_spikes.tsx index ca670df258a6a..5fac891a79675 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/aiops/explain_log_rate_spikes.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/aiops/explain_log_rate_spikes.tsx @@ -37,7 +37,7 @@ export const explainLogRateSpikesRouteFactory = ( getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB', navigateToPath, basePath), { - text: i18n.translate('xpack.ml.AiopsBreadcrumbs.explainLogRateSpikesLabel', { + text: i18n.translate('xpack.ml.aiopsBreadcrumbs.explainLogRateSpikesLabel', { defaultMessage: 'Explain log rate spikes', }), }, diff --git a/x-pack/plugins/ml/public/application/routing/routes/aiops/index.ts b/x-pack/plugins/ml/public/application/routing/routes/aiops/index.ts index f2b192a4cd097..10f0eba1adeda 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/aiops/index.ts +++ b/x-pack/plugins/ml/public/application/routing/routes/aiops/index.ts @@ -6,3 +6,4 @@ */ export * from './explain_log_rate_spikes'; +export * from './single_endpoint_streaming_demo'; diff --git a/x-pack/plugins/ml/public/application/routing/routes/aiops/single_endpoint_streaming_demo.tsx b/x-pack/plugins/ml/public/application/routing/routes/aiops/single_endpoint_streaming_demo.tsx new file mode 100644 index 0000000000000..636357518d0d0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/routing/routes/aiops/single_endpoint_streaming_demo.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { parse } from 'query-string'; + +import { i18n } from '@kbn/i18n'; + +import { AIOPS_ENABLED } from '@kbn/aiops-plugin/common'; + +import { NavigateToPath } from '../../../contexts/kibana'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { SingleEndpointStreamingDemoPage as Page } from '../../../aiops/single_endpoint_streaming_demo'; + +import { checkBasicLicense } from '../../../license'; +import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; +import { cacheDataViewsContract } from '../../../util/index_utils'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const singleEndpointStreamingDemoRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ + id: 'single_endpoint_streaming_demo', + path: '/aiops/single_endpoint_streaming_demo', + title: i18n.translate('xpack.ml.aiops.singleEndpointStreamingDemo.docTitle', { + defaultMessage: 'Single endpoint streaming demo', + }), + render: (props, deps) => , + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB', navigateToPath, basePath), + { + text: i18n.translate('xpack.ml.aiopsBreadcrumbs.singleEndpointStreamingDemoLabel', { + defaultMessage: 'Single endpoint streaming demo', + }), + }, + ], + disabled: !AIOPS_ENABLED, +}); + +const PageWrapper: FC = ({ location, deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + + const { index, savedSearchId }: Record = parse(location.search, { sort: false }); + const { context } = useResolver(index, savedSearchId, deps.config, deps.dataViewsContract, { + checkBasicLicense, + cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract), + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), + }); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index d1d547ca8bc90..5ea3bfa9d35eb 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -50,6 +50,16 @@ const getDataVisBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) }, ]; +const getExplainLogRateSpikesBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB', navigateToPath, basePath), + { + text: i18n.translate('xpack.ml.aiopsBreadcrumbs.selectDateViewLabel', { + defaultMessage: 'Data View', + }), + }, +]; + export const indexOrSearchRouteFactory = ( navigateToPath: NavigateToPath, basePath: string @@ -86,6 +96,26 @@ export const dataVizIndexOrSearchRouteFactory = ( breadcrumbs: getDataVisBreadcrumbs(navigateToPath, basePath), }); +export const explainLogRateSpikesIndexOrSearchRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ + id: 'data_view_explain_log_rate_spikes', + path: '/aiops/explain_log_rate_spikes_index_select', + title: i18n.translate('xpack.ml.selectDataViewLabel', { + defaultMessage: 'Select Data View', + }), + render: (props, deps) => ( + + ), + breadcrumbs: getExplainLogRateSpikesBreadcrumbs(navigateToPath, basePath), +}); + const PageWrapper: FC = ({ nextStepPath, deps, mode }) => { const { services: { diff --git a/x-pack/plugins/ml/public/locator/ml_locator.ts b/x-pack/plugins/ml/public/locator/ml_locator.ts index 295dbaebbbae6..b36029329c087 100644 --- a/x-pack/plugins/ml/public/locator/ml_locator.ts +++ b/x-pack/plugins/ml/public/locator/ml_locator.ts @@ -86,6 +86,8 @@ export class MlLocatorDefinition implements LocatorDefinition { case ML_PAGES.DATA_VISUALIZER_INDEX_SELECT: case ML_PAGES.AIOPS: case ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES: + case ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES_INDEX_SELECT: + case ML_PAGES.AIOPS_SINGLE_ENDPOINT_STREAMING_DEMO: case ML_PAGES.OVERVIEW: case ML_PAGES.SETTINGS: case ML_PAGES.FILTER_LISTS_MANAGE: diff --git a/x-pack/test/api_integration/apis/aiops/example_stream.ts b/x-pack/test/api_integration/apis/aiops/example_stream.ts index 693a6de2c6716..c1e410655dbfc 100644 --- a/x-pack/test/api_integration/apis/aiops/example_stream.ts +++ b/x-pack/test/api_integration/apis/aiops/example_stream.ts @@ -12,6 +12,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { parseStream } from './parse_stream'; + export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const config = getService('config'); @@ -67,34 +69,15 @@ export default ({ getService }: FtrProviderContext) => { expect(stream).not.to.be(null); if (stream !== null) { - let partial = ''; - let threw = false; const progressData: any[] = []; - try { - for await (const value of stream) { - const full = `${partial}${value}`; - const parts = full.split('\n'); - const last = parts.pop(); - - partial = last ?? ''; - - const actions = parts.map((p) => JSON.parse(p)); - - actions.forEach((action) => { - expect(typeof action.type).to.be('string'); - - if (action.type === 'update_progress') { - progressData.push(action); - } - }); + for await (const action of parseStream(stream)) { + expect(action.type).not.to.be('error'); + if (action.type === 'update_progress') { + progressData.push(action); } - } catch (e) { - threw = true; } - expect(threw).to.be(false); - expect(progressData.length).to.be(100); expect(progressData[0].payload).to.be(1); expect(progressData[progressData.length - 1].payload).to.be(100); diff --git a/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts b/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts new file mode 100644 index 0000000000000..11ef63809a52f --- /dev/null +++ b/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import fetch from 'node-fetch'; +import { format as formatUrl } from 'url'; + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { parseStream } from './parse_stream'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const config = getService('config'); + const kibanaServerUrl = formatUrl(config.get('servers.kibana')); + + const expectedFields = [ + 'category', + 'currency', + 'customer_first_name', + 'customer_full_name', + 'customer_gender', + 'customer_id', + 'customer_last_name', + 'customer_phone', + 'day_of_week', + 'day_of_week_i', + 'email', + 'geoip', + 'manufacturer', + 'order_date', + 'order_id', + 'products', + 'sku', + 'taxful_total_price', + 'taxless_total_price', + 'total_quantity', + 'total_unique_products', + 'type', + 'user', + ]; + + describe('POST /internal/aiops/explain_log_rate_spikes', () => { + const esArchiver = getService('esArchiver'); + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + }); + + it('should return full data without streaming', async () => { + const resp = await supertest + .post(`/internal/aiops/explain_log_rate_spikes`) + .set('kbn-xsrf', 'kibana') + .send({ + index: 'ft_ecommerce', + }) + .expect(200); + + expect(Buffer.isBuffer(resp.body)).to.be(true); + + const chunks: string[] = resp.body.toString().split('\n'); + + expect(chunks.length).to.be(24); + + const lastChunk = chunks.pop(); + expect(lastChunk).to.be(''); + + let data: any[] = []; + + expect(() => { + data = chunks.map((c) => JSON.parse(c)); + }).not.to.throwError(); + + data.forEach((d) => { + expect(typeof d.type).to.be('string'); + }); + + const fields = data.map((d) => d.payload[0]).sort(); + + expect(fields.length).to.equal(expectedFields.length); + fields.forEach((f) => { + expect(expectedFields.includes(f)); + }); + }); + + it('should return data in chunks with streaming', async () => { + const response = await fetch(`${kibanaServerUrl}/internal/aiops/explain_log_rate_spikes`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'kbn-xsrf': 'stream', + }, + body: JSON.stringify({ index: 'ft_ecommerce' }), + }); + + const stream = response.body; + + expect(stream).not.to.be(null); + + if (stream !== null) { + const data: any[] = []; + + for await (const action of parseStream(stream)) { + expect(action.type).not.to.be('error'); + data.push(action); + } + + const fields = data.map((d) => d.payload[0]).sort(); + + expect(fields.length).to.equal(expectedFields.length); + fields.forEach((f) => { + expect(expectedFields.includes(f)); + }); + } + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/aiops/index.ts b/x-pack/test/api_integration/apis/aiops/index.ts index 04b4181906dbf..d2aacc454b567 100644 --- a/x-pack/test/api_integration/apis/aiops/index.ts +++ b/x-pack/test/api_integration/apis/aiops/index.ts @@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { this.tags(['ml']); loadTestFile(require.resolve('./example_stream')); + loadTestFile(require.resolve('./explain_log_rate_spikes')); }); } diff --git a/x-pack/test/api_integration/apis/aiops/parse_stream.ts b/x-pack/test/api_integration/apis/aiops/parse_stream.ts new file mode 100644 index 0000000000000..f3da52e6024bb --- /dev/null +++ b/x-pack/test/api_integration/apis/aiops/parse_stream.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export async function* parseStream(stream: NodeJS.ReadableStream) { + let partial = ''; + + try { + for await (const value of stream) { + const full = `${partial}${value}`; + const parts = full.split('\n'); + const last = parts.pop(); + + partial = last ?? ''; + + const actions = parts.map((p) => JSON.parse(p)); + + for (const action of actions) { + yield action; + } + } + } catch (error) { + yield { type: 'error', payload: error.toString() }; + } +} From 8c19c36b36e23aab26cefea9ed85df18c71d882d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 20 May 2022 09:46:09 +0100 Subject: [PATCH 020/120] [Content management] Surface "Last updated" column in Saved object management (#132525) --- .../saved_objects_table.test.tsx.snap | 6 ++ .../__snapshots__/table.test.tsx.snap | 34 ++++++++++- .../objects_table/components/table.test.tsx | 4 ++ .../objects_table/components/table.tsx | 58 ++++++++++++++++++- .../objects_table/saved_objects_table.tsx | 19 ++++-- .../server/routes/find.ts | 1 + .../apis/saved_objects_management/find.ts | 33 +++++++++++ 7 files changed, 145 insertions(+), 10 deletions(-) diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index f7026af66c500..61501ed45b47d 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -296,6 +296,12 @@ exports[`SavedObjectsTable should render normally 1`] = ` "onSelectionChange": [Function], } } + sort={ + Object { + "direction": "desc", + "field": "updated_at", + } + } totalItemCount={4} /> diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap index 2515a8ce6d788..4b3bc4f5bd0cf 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap @@ -131,7 +131,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` "field": "type", "name": "Type", "render": [Function], - "sortable": false, + "sortable": true, "width": "50px", }, Object { @@ -143,6 +143,13 @@ exports[`Table prevents saved objects from being deleted 1`] = ` "render": [Function], "sortable": false, }, + Object { + "field": "updated_at", + "name": "Last updated", + "render": [Function], + "sortable": true, + "width": "150px", + }, Object { "actions": Array [ Object { @@ -215,6 +222,14 @@ exports[`Table prevents saved objects from being deleted 1`] = ` "onSelectionChange": [Function], } } + sorting={ + Object { + "sort": Object { + "direction": "desc", + "field": "updated_at", + }, + } + } tableLayout="fixed" /> @@ -351,7 +366,7 @@ exports[`Table should render normally 1`] = ` "field": "type", "name": "Type", "render": [Function], - "sortable": false, + "sortable": true, "width": "50px", }, Object { @@ -363,6 +378,13 @@ exports[`Table should render normally 1`] = ` "render": [Function], "sortable": false, }, + Object { + "field": "updated_at", + "name": "Last updated", + "render": [Function], + "sortable": true, + "width": "150px", + }, Object { "actions": Array [ Object { @@ -435,6 +457,14 @@ exports[`Table should render normally 1`] = ` "onSelectionChange": [Function], } } + sorting={ + Object { + "sort": Object { + "direction": "desc", + "field": "updated_at", + }, + } + } tableLayout="fixed" /> diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx index 4ee1510a7627c..86f2b766002ac 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx @@ -50,6 +50,10 @@ const defaultProps: TableProps = { canGoInApp: () => true, pageIndex: 1, pageSize: 2, + sort: { + field: 'updated_at', + direction: 'desc', + }, items: [ { id: '1', diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index ff5d49da99c61..0ffd353c8ddd2 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -8,6 +8,7 @@ import { ApplicationStart, IBasePath } from '@kbn/core/public'; import React, { PureComponent, Fragment } from 'react'; +import moment from 'moment'; import { EuiSearchBar, EuiBasicTable, @@ -24,9 +25,10 @@ import { EuiTableFieldDataColumnType, EuiTableActionsColumnType, QueryType, + CriteriaWithPagination, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react'; import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { SavedObjectManagementTypeInfo } from '../../../../common/types'; import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; @@ -55,6 +57,7 @@ export interface TableProps { goInspectObject: (obj: SavedObjectWithMetadata) => void; pageIndex: number; pageSize: number; + sort: CriteriaWithPagination['sort']; items: SavedObjectWithMetadata[]; itemId: string | (() => string); totalItemCount: number; @@ -128,10 +131,59 @@ export class Table extends PureComponent { this.setState({ isExportPopoverOpen: false }); }; + getUpdatedAtColumn = () => { + const renderUpdatedAt = (dateTime?: string) => { + if (!dateTime) { + return ( + + - + + ); + } + const updatedAt = moment(dateTime); + + if (updatedAt.diff(moment(), 'days') > -7) { + return ( + + {(formattedDate: string) => ( + + {formattedDate} + + )} + + ); + } + return ( + + {updatedAt.format('LL')} + + ); + }; + + return { + field: 'updated_at', + name: i18n.translate('savedObjectsManagement.objectsTable.table.lastUpdatedColumnTitle', { + defaultMessage: 'Last updated', + }), + render: (field: string, record: { updated_at?: string }) => + renderUpdatedAt(record.updated_at), + sortable: true, + width: '150px', + }; + }; + render() { const { pageIndex, pageSize, + sort, itemId, items, totalItemCount, @@ -186,7 +238,7 @@ export class Table extends PureComponent { 'savedObjectsManagement.objectsTable.table.columnTypeDescription', { defaultMessage: 'Type of the saved object' } ), - sortable: false, + sortable: true, 'data-test-subj': 'savedObjectsTableRowType', render: (type: string, object: SavedObjectWithMetadata) => { const typeLabel = getSavedObjectLabel(type, allowedTypes); @@ -239,6 +291,7 @@ export class Table extends PureComponent { 'data-test-subj': `savedObjectsTableColumn-${column.id}`, }; }), + this.getUpdatedAtColumn(), { name: i18n.translate('savedObjectsManagement.objectsTable.table.columnActionsName', { defaultMessage: 'Actions', @@ -422,6 +475,7 @@ export class Table extends PureComponent { items={items} columns={columns as any} pagination={pagination} + sorting={{ sort }} selection={selection} onChange={onTableChange} rowProps={(item) => ({ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index c8330e0eb9cf3..b0afbcc163ef8 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -10,7 +10,7 @@ import React, { Component } from 'react'; import { debounce } from 'lodash'; // @ts-expect-error import { saveAs } from '@elastic/filesaver'; -import { EuiSpacer, Query } from '@elastic/eui'; +import { EuiSpacer, Query, CriteriaWithPagination } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SavedObjectsClientContract, @@ -78,6 +78,7 @@ export interface SavedObjectsTableState { totalCount: number; page: number; perPage: number; + sort: CriteriaWithPagination['sort']; savedObjects: SavedObjectWithMetadata[]; savedObjectCounts: Record; activeQuery: Query; @@ -114,6 +115,10 @@ export class SavedObjectsTable extends Component { typeToCountMap[type.name] = 0; @@ -211,7 +216,7 @@ export class SavedObjectsTable extends Component { - const { activeQuery: query, page, perPage } = this.state; + const { activeQuery: query, page, perPage, sort } = this.state; const { notifications, http, allowedTypes, taggingApi } = this.props; const { queryText, visibleTypes, selectedTags } = parseQuery(query, allowedTypes); @@ -228,9 +233,8 @@ export class SavedObjectsTable extends Component 1) { - findOptions.sortField = 'type'; - } + findOptions.sortField = sort?.field; + findOptions.sortOrder = sort?.direction; findOptions.hasReference = getTagFindReferences({ selectedTags, taggingApi }); @@ -352,7 +356,7 @@ export class SavedObjectsTable extends Component { + onTableChange = async (table: CriteriaWithPagination) => { const { index: page, size: perPage } = table.page || {}; this.setState( @@ -360,6 +364,7 @@ export class SavedObjectsTable extends Component { + it('sort objects by "type" in "asc" order', async () => { + await supertest + .get('/api/kibana/management/saved_objects/_find') + .query({ + type: ['visualization', 'dashboard'], + sortField: 'type', + sortOrder: 'asc', + }) + .expect(200) + .then((resp) => { + const objects = resp.body.saved_objects; + expect(objects.length).be.greaterThan(1); // Need more than 1 result for our test + expect(objects[0].type).to.be('dashboard'); + }); + }); + + it('sort objects by "type" in "desc" order', async () => { + await supertest + .get('/api/kibana/management/saved_objects/_find') + .query({ + type: ['visualization', 'dashboard'], + sortField: 'type', + sortOrder: 'desc', + }) + .expect(200) + .then((resp) => { + const objects = resp.body.saved_objects; + expect(objects[0].type).to.be('visualization'); + }); + }); + }); }); describe('meta attributes injected properly', () => { From 63e67ab630083ebb3eebba8bec9aab7572992478 Mon Sep 17 00:00:00 2001 From: mgiota Date: Fri, 20 May 2022 10:46:33 +0200 Subject: [PATCH 021/120] move error into a useEffect (#132491) --- .../public/pages/rule_details/components/actions.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/actions.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/actions.tsx index 5a692e570281a..e450404120e89 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/components/actions.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/components/actions.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { EuiText, EuiSpacer, @@ -38,6 +38,11 @@ export function Actions({ ruleActions }: ActionsProps) { notifications: { toasts }, } = useKibana().services; const { isLoadingActions, allActions, errorActions } = useFetchRuleActions({ http }); + useEffect(() => { + if (errorActions) { + toasts.addDanger({ title: errorActions }); + } + }, [errorActions, toasts]); if (ruleActions && ruleActions.length <= 0) return ( @@ -65,7 +70,6 @@ export function Actions({ ruleActions }: ActionsProps) { ))} - {errorActions && toasts.addDanger({ title: errorActions })}
); } From c1365153630afb5f5768b52592864d93ce1bd194 Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Fri, 20 May 2022 12:35:30 +0300 Subject: [PATCH 022/120] [Actionable Observability] Add execution log count in the last 24h in the Rule details page (#132411) * Add execution log count in the last 24h * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' --- .../use_fetch_last24h_rule_execution_log.ts | 67 +++++++++++++++++++ .../public/pages/rule_details/index.tsx | 25 +++++++ .../public/pages/rule_details/translations.ts | 6 ++ .../public/pages/rule_details/types.ts | 5 ++ .../triggers_actions_ui/public/index.ts | 1 + 5 files changed, 104 insertions(+) create mode 100644 x-pack/plugins/observability/public/hooks/use_fetch_last24h_rule_execution_log.ts diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_last24h_rule_execution_log.ts b/x-pack/plugins/observability/public/hooks/use_fetch_last24h_rule_execution_log.ts new file mode 100644 index 0000000000000..edb08f69b44f3 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_fetch_last24h_rule_execution_log.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState, useCallback } from 'react'; +import { loadExecutionLogAggregations } from '@kbn/triggers-actions-ui-plugin/public'; +import { IExecutionLogWithErrorsResult } from '@kbn/alerting-plugin/common'; +import moment from 'moment'; +import { FetchRuleExecutionLogProps } from '../pages/rule_details/types'; +import { EXECUTION_LOG_ERROR } from '../pages/rule_details/translations'; +import { useKibana } from '../utils/kibana_react'; + +interface FetchExecutionLog { + isLoadingExecutionLog: boolean; + executionLog: IExecutionLogWithErrorsResult; + errorExecutionLog?: string; +} + +export function useFetchLast24hRuleExecutionLog({ http, ruleId }: FetchRuleExecutionLogProps) { + const { + notifications: { toasts }, + } = useKibana().services; + const [executionLog, setExecutionLog] = useState({ + isLoadingExecutionLog: true, + executionLog: { + total: 0, + data: [], + totalErrors: 0, + errors: [], + }, + errorExecutionLog: undefined, + }); + + const fetchRuleActions = useCallback(async () => { + try { + const date = new Date().toISOString(); + const response = await loadExecutionLogAggregations({ + id: ruleId, + dateStart: moment(date).subtract(24, 'h').toISOString(), + dateEnd: date, + http, + }); + setExecutionLog((oldState: FetchExecutionLog) => ({ + ...oldState, + isLoadingExecutionLog: false, + executionLog: response, + })); + } catch (error) { + toasts.addDanger({ title: error }); + setExecutionLog((oldState: FetchExecutionLog) => ({ + ...oldState, + isLoadingExecutionLog: false, + errorExecutionLog: EXECUTION_LOG_ERROR( + error instanceof Error ? error.message : typeof error === 'string' ? error : '' + ), + })); + } + }, [http, ruleId, toasts]); + useEffect(() => { + fetchRuleActions(); + }, [fetchRuleActions]); + + return { ...executionLog, reloadExecutionLogs: useFetchLast24hRuleExecutionLog }; +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx index 31b9a888ec266..96af4de1eb053 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/index.tsx @@ -56,6 +56,7 @@ import { RULES_BREADCRUMB_TEXT } from '../rules/translations'; import { PageTitle, ItemTitleRuleSummary, ItemValueRuleSummary, Actions } from './components'; import { useKibana } from '../../utils/kibana_react'; import { useFetchLast24hAlerts } from '../../hooks/use_fetch_last24h_alerts'; +import { useFetchLast24hRuleExecutionLog } from '../../hooks/use_fetch_last24h_rule_execution_log'; import { formatInterval } from './utils'; import { hasExecuteActionsCapability, hasAllPrivilege } from './config'; import { paths } from '../../config/paths'; @@ -76,6 +77,7 @@ export function RuleDetailsPage() { const { ruleId } = useParams(); const { ObservabilityPageTemplate } = usePluginContext(); const { isRuleLoading, rule, errorRule, reloadRule } = useFetchRule({ ruleId, http }); + const { isLoadingExecutionLog, executionLog } = useFetchLast24hRuleExecutionLog({ http, ruleId }); const { ruleTypes, ruleTypeIndex } = useLoadRuleTypes({ filteredSolutions: OBSERVABILITY_SOLUTIONS, }); @@ -350,6 +352,29 @@ export function RuleDetailsPage() { )}`} />
+ + {isLoadingExecutionLog ? ( + + ) : ( + + + {i18n.translate('xpack.observability.ruleDetails.execution', { + defaultMessage: 'Executions', + })} + + + + + )} diff --git a/x-pack/plugins/observability/public/pages/rule_details/translations.ts b/x-pack/plugins/observability/public/pages/rule_details/translations.ts index f162f30906c21..bda8284c31a9e 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/translations.ts +++ b/x-pack/plugins/observability/public/pages/rule_details/translations.ts @@ -18,6 +18,12 @@ export const ACTIONS_LOAD_ERROR = (errorMessage: string) => values: { message: errorMessage }, }); +export const EXECUTION_LOG_ERROR = (errorMessage: string) => + i18n.translate('xpack.observability.ruleDetails.executionLogError', { + defaultMessage: 'Unable to load rule execution log. Reason: {message}', + values: { message: errorMessage }, + }); + export const TAGS_TITLE = i18n.translate('xpack.observability.ruleDetails.tagsTitle', { defaultMessage: 'Tags', }); diff --git a/x-pack/plugins/observability/public/pages/rule_details/types.ts b/x-pack/plugins/observability/public/pages/rule_details/types.ts index 9855bf2c7f184..0ce91d0481dd9 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/types.ts +++ b/x-pack/plugins/observability/public/pages/rule_details/types.ts @@ -35,6 +35,11 @@ export interface FetchRuleActionsProps { http: HttpSetup; } +export interface FetchRuleExecutionLogProps { + http: HttpSetup; + ruleId: string; +} + export interface FetchRuleSummary { isLoadingRuleSummary: boolean; ruleSummary?: RuleSummary; diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 9c08dfe597ecf..4580600b4bff8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -76,6 +76,7 @@ export { Plugin }; export * from './plugin'; // TODO remove this import when we expose the Rules tables as a component export { loadRules } from './application/lib/rule_api/rules'; +export { loadExecutionLogAggregations } from './application/lib/rule_api/load_execution_log_aggregations'; export { loadRuleTypes } from './application/lib/rule_api'; export { loadRuleSummary } from './application/lib/rule_api/rule_summary'; export { deleteRules } from './application/lib/rule_api/delete'; From de90ea592becedda956fe29e6ee1c4490b29fab0 Mon Sep 17 00:00:00 2001 From: mgiota Date: Fri, 20 May 2022 11:48:26 +0200 Subject: [PATCH 023/120] [Actionable Observability] Display action connector icon in o11y rule details page (#132026) * get iconClass from actionRegistry * use suspendedComponentWithProps when iconClass is a react component and write some tests * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * fix xmatters svg icon * apply design feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...observability_public_plugins_start.mock.ts | 6 ++ .../rule_details/components/actions.test.tsx | 84 +++++++++++++++++++ .../pages/rule_details/components/actions.tsx | 38 ++++----- .../public/pages/rule_details/index.tsx | 3 +- .../public/pages/rule_details/types.ts | 8 +- .../builtin_action_types/xmatters/logo.tsx | 5 +- .../triggers_actions_ui/public/index.ts | 1 + 7 files changed, 121 insertions(+), 24 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/rule_details/components/actions.test.tsx diff --git a/x-pack/plugins/observability/public/observability_public_plugins_start.mock.ts b/x-pack/plugins/observability/public/observability_public_plugins_start.mock.ts index ab1f769c1c4b9..a20e42cd37841 100644 --- a/x-pack/plugins/observability/public/observability_public_plugins_start.mock.ts +++ b/x-pack/plugins/observability/public/observability_public_plugins_start.mock.ts @@ -46,6 +46,12 @@ const triggersActionsUiStartMock = { get: jest.fn(), list: jest.fn(), }, + actionTypeRegistry: { + has: jest.fn((x) => true), + register: jest.fn(), + get: jest.fn(), + list: jest.fn(), + }, }; }, }; diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/actions.test.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/actions.test.tsx new file mode 100644 index 0000000000000..9000d9dbf5f99 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/actions.test.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { ReactWrapper, mount } from 'enzyme'; +import { Actions } from './actions'; +import { observabilityPublicPluginsStartMock } from '../../../observability_public_plugins_start.mock'; +import { kibanaStartMock } from '../../../utils/kibana_react.mock'; + +const mockUseKibanaReturnValue = kibanaStartMock.startContract(); + +jest.mock('../../../utils/kibana_react', () => ({ + __esModule: true, + useKibana: jest.fn(() => mockUseKibanaReturnValue), +})); + +jest.mock('../../../hooks/use_fetch_rule_actions', () => ({ + useFetchRuleActions: jest.fn(), +})); + +const { useFetchRuleActions } = jest.requireMock('../../../hooks/use_fetch_rule_actions'); + +describe('Actions', () => { + let wrapper: ReactWrapper; + async function setup() { + const ruleActions = [ + { + id: 1, + group: 'metrics.inventory_threshold.fired', + actionTypeId: '.server-log', + }, + { + id: 2, + group: 'metrics.inventory_threshold.fired', + actionTypeId: '.slack', + }, + ]; + const allActions = [ + { + id: 1, + name: 'Server log', + actionTypeId: '.server-log', + }, + { + id: 2, + name: 'Slack', + actionTypeId: '.slack', + }, + { + id: 3, + name: 'Email', + actionTypeId: '.email', + }, + ]; + useFetchRuleActions.mockReturnValue({ + allActions, + }); + + const actionTypeRegistryMock = + observabilityPublicPluginsStartMock.createStart().triggersActionsUi.actionTypeRegistry; + actionTypeRegistryMock.list.mockReturnValue([ + { id: '.server-log', iconClass: 'logsApp' }, + { id: '.slack', iconClass: 'logoSlack' }, + { id: '.email', iconClass: 'email' }, + { id: '.index', iconClass: 'indexOpen' }, + ]); + wrapper = mount( + + ); + } + + it("renders action connector icons for user's selected rule actions", async () => { + await setup(); + wrapper.debug(); + expect(wrapper.find('[data-euiicon-type]').length).toBe(2); + expect(wrapper.find('[data-euiicon-type="logsApp"]').length).toBe(1); + expect(wrapper.find('[data-euiicon-type="logoSlack"]').length).toBe(1); + expect(wrapper.find('[data-euiicon-type="index"]').length).toBe(0); + expect(wrapper.find('[data-euiicon-type="email"]').length).toBe(0); + }); +}); diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/actions.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/actions.tsx index e450404120e89..d3dbe3cf4bdef 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/components/actions.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/components/actions.tsx @@ -15,24 +15,13 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import { intersectionBy } from 'lodash'; +import { suspendedComponentWithProps } from '@kbn/triggers-actions-ui-plugin/public'; import { i18n } from '@kbn/i18n'; import { ActionsProps } from '../types'; import { useFetchRuleActions } from '../../../hooks/use_fetch_rule_actions'; import { useKibana } from '../../../utils/kibana_react'; -interface MapActionTypeIcon { - [key: string]: string | IconType; -} -const mapActionTypeIcon: MapActionTypeIcon = { - /* TODO: Add the rest of the application logs (SVGs ones) */ - '.server-log': 'logsApp', - '.email': 'email', - '.pagerduty': 'apps', - '.index': 'indexOpen', - '.slack': 'logoSlack', - '.webhook': 'logoWebhook', -}; -export function Actions({ ruleActions }: ActionsProps) { +export function Actions({ ruleActions, actionTypeRegistry }: ActionsProps) { const { http, notifications: { toasts }, @@ -53,22 +42,31 @@ export function Actions({ ruleActions }: ActionsProps) { ); + + function getActionIconClass(actionGroupId?: string): IconType | undefined { + const actionGroup = actionTypeRegistry.list().find((group) => group.id === actionGroupId); + return typeof actionGroup?.iconClass === 'string' + ? actionGroup?.iconClass + : suspendedComponentWithProps(actionGroup?.iconClass as React.ComponentType); + } const actions = intersectionBy(allActions, ruleActions, 'actionTypeId'); if (isLoadingActions) return ; return ( - {actions.map((action) => ( - <> - + {actions.map(({ actionTypeId, name }) => ( + + - + - - {action.name} + + + {name} + - + ))} ); diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx index 96af4de1eb053..99000a91671b8 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/index.tsx @@ -68,6 +68,7 @@ export function RuleDetailsPage() { ruleTypeRegistry, getRuleStatusDropdown, getEditAlertFlyout, + actionTypeRegistry, getRuleEventLogList, }, application: { capabilities, navigateToUrl }, @@ -481,7 +482,7 @@ export function RuleDetailsPage() { })} - + diff --git a/x-pack/plugins/observability/public/pages/rule_details/types.ts b/x-pack/plugins/observability/public/pages/rule_details/types.ts index 0ce91d0481dd9..4b1c62f7dbb9a 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/types.ts +++ b/x-pack/plugins/observability/public/pages/rule_details/types.ts @@ -6,7 +6,12 @@ */ import { HttpSetup } from '@kbn/core/public'; -import { Rule, RuleSummary, RuleType } from '@kbn/triggers-actions-ui-plugin/public'; +import { + Rule, + RuleSummary, + RuleType, + ActionTypeRegistryContract, +} from '@kbn/triggers-actions-ui-plugin/public'; export interface RuleDetailsPathParams { ruleId: string; @@ -68,6 +73,7 @@ export interface ItemValueRuleSummaryProps { } export interface ActionsProps { ruleActions: any[]; + actionTypeRegistry: ActionTypeRegistryContract; } export const EVENT_LOG_LIST_TAB = 'rule_event_log_list'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/logo.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/logo.tsx index f65f66587ba74..dad43f666ad0a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/logo.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/logo.tsx @@ -6,16 +6,17 @@ */ import React from 'react'; +import { LogoProps } from '../types'; -const Logo = () => ( +const Logo = (props: LogoProps) => ( x-logo diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 4580600b4bff8..8295fada788e3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -90,6 +90,7 @@ export { loadRuleAggregations, loadRuleTags } from './application/lib/rule_api/a export { useLoadRuleTypes } from './application/hooks/use_load_rule_types'; export { loadRule } from './application/lib/rule_api/get_rule'; export { loadAllActions } from './application/lib/action_connector_api'; +export { suspendedComponentWithProps } from './application/lib/suspended_component_with_props'; export { loadActionTypes } from './application/lib/action_connector_api/connector_types'; export { NOTIFY_WHEN_OPTIONS } from './application/sections/rule_form/rule_notify_when'; export type { TIME_UNITS } from './application/constants'; From f75b6fa1561fb8592a493c41c08302fddd136760 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Fri, 20 May 2022 12:50:46 +0300 Subject: [PATCH 024/120] [XY] Add `addTimeMarker` arg (#131495) * Add `addTimeMarker` arg * Some fixes * Update validation * Fix snapshots * Some fixes after merge * Add unit tests * Fix CI * Update src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx Co-authored-by: Yaroslav Kuznietsov * Fixed tests * Fix checks Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Yaroslav Kuznietsov --- .../expression_xy/common/__mocks__/index.ts | 6 +- .../__snapshots__/xy_vis.test.ts.snap | 2 + .../common_data_layer_args.ts | 1 - .../expression_functions/common_xy_args.ts | 5 + .../expression_functions/layered_xy_vis_fn.ts | 4 +- .../common/expression_functions/validate.ts | 14 + .../expression_functions/xy_vis.test.ts | 40 +- .../common/expression_functions/xy_vis_fn.ts | 2 + .../common/helpers/visualization.ts | 7 +- .../expression_xy/common/i18n/index.tsx | 4 + .../common/types/expression_functions.ts | 3 + .../__snapshots__/xy_chart.test.tsx.snap | 880 +++++++++--------- .../public/components/data_layers.tsx | 4 + .../public/components/xy_chart.test.tsx | 13 +- .../public/components/xy_chart.tsx | 16 +- .../public/components/xy_current_time.tsx | 26 + .../public/helpers/data_layers.tsx | 4 +- .../expression_xy/public/helpers/interval.ts | 5 +- 18 files changed, 580 insertions(+), 456 deletions(-) create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/xy_current_time.tsx diff --git a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts index 76e524960b159..1f19428e420bf 100644 --- a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts @@ -35,7 +35,7 @@ export const createSampleDatatableWithRows = (rows: DatatableRow[]): Datatable = id: 'c', name: 'c', meta: { - type: 'string', + type: 'date', field: 'order_date', sourceParams: { type: 'date-histogram', params: { interval: 'auto' } }, params: { id: 'string' }, @@ -128,8 +128,8 @@ export const createArgsWithLayers = ( export function sampleArgs() { const data = createSampleDatatableWithRows([ - { a: 1, b: 2, c: 'I', d: 'Foo' }, - { a: 1, b: 5, c: 'J', d: 'Bar' }, + { a: 1, b: 2, c: 1652034840000, d: 'Foo' }, + { a: 1, b: 5, c: 1652122440000, d: 'Bar' }, ]); return { diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/xy_vis.test.ts.snap b/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/xy_vis.test.ts.snap index 05109cc65446b..e396aace05191 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/xy_vis.test.ts.snap +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/xy_vis.test.ts.snap @@ -1,5 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`xyVis it should throw error if addTimeMarker applied for not time chart 1`] = `"Only time charts can have current time marker"`; + exports[`xyVis it should throw error if markSizeRatio is lower then 1 or greater then 100 1`] = `"Mark size ratio must be greater or equal to 1 and less or equal to 100"`; exports[`xyVis it should throw error if markSizeRatio is lower then 1 or greater then 100 2`] = `"Mark size ratio must be greater or equal to 1 and less or equal to 100"`; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts index 0c9085cce7664..f4543c5236ce2 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts @@ -36,7 +36,6 @@ export const commonDataLayerArgs: Omit< xScaleType: { options: [...Object.values(XScaleTypes)], help: strings.getXScaleTypeHelp(), - default: XScaleTypes.ORDINAL, strict: true, }, isHistogram: { diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts index 0921760f9f676..2e2e6765734cf 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts @@ -128,6 +128,11 @@ export const commonXYArgs: CommonXYFn['args'] = { types: ['string'], help: strings.getAriaLabelHelp(), }, + addTimeMarker: { + types: ['boolean'], + default: false, + help: strings.getAddTimeMakerHelp(), + }, markSizeRatio: { types: ['number'], help: strings.getMarkSizeRatioHelp(), diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts index 29624d8037393..fb7c91c682847 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts @@ -7,15 +7,16 @@ */ import { XY_VIS_RENDERER } from '../constants'; -import { appendLayerIds, getDataLayers } from '../helpers'; import { LayeredXyVisFn } from '../types'; import { logDatatables } from '../utils'; import { validateMarkSizeRatioLimits, + validateAddTimeMarker, validateMinTimeBarInterval, hasBarLayer, errors, } from './validate'; +import { appendLayerIds, getDataLayers } from '../helpers'; export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers) => { const layers = appendLayerIds(args.layers ?? [], 'layers'); @@ -24,6 +25,7 @@ export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers) const dataLayers = getDataLayers(layers); const hasBar = hasBarLayer(dataLayers); + validateAddTimeMarker(dataLayers, args.addTimeMarker); validateMarkSizeRatioLimits(args.markSizeRatio); validateMinTimeBarInterval(dataLayers, hasBar, args.minTimeBarInterval); const hasMarkSizeAccessors = diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts index 60e590b0f8cca..df7f9ee08632e 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts @@ -17,6 +17,7 @@ import { CommonXYDataLayerConfigResult, ValueLabelMode, CommonXYDataLayerConfig, + ExtendedDataLayerConfigResult, } from '../types'; import { isTimeChart } from '../helpers'; @@ -58,6 +59,10 @@ export const errors = { i18n.translate('expressionXY.reusable.function.xyVis.errors.dataBoundsForNotLineChartError', { defaultMessage: 'Only line charts can be fit to the data bounds', }), + timeMarkerForNotTimeChartsError: () => + i18n.translate('expressionXY.reusable.function.xyVis.errors.timeMarkerForNotTimeChartsError', { + defaultMessage: 'Only time charts can have current time marker', + }), isInvalidIntervalError: () => i18n.translate('expressionXY.reusable.function.xyVis.errors.isInvalidIntervalError', { defaultMessage: @@ -135,6 +140,15 @@ export const validateValueLabels = ( } }; +export const validateAddTimeMarker = ( + dataLayers: Array, + addTimeMarker?: boolean +) => { + if (addTimeMarker && !isTimeChart(dataLayers)) { + throw new Error(errors.timeMarkerForNotTimeChartsError()); + } +}; + export const validateMarkSizeForChartType = ( markSizeAccessor: ExpressionValueVisDimension | string | undefined, seriesType: SeriesType diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts index 73d4444217d90..8a327ccca9e20 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts @@ -7,7 +7,6 @@ */ import { xyVisFunction } from '.'; -import { Datatable } from '@kbn/expressions-plugin/common'; import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks'; import { sampleArgs, sampleLayer } from '../__mocks__'; import { XY_VIS } from '../constants'; @@ -15,26 +14,10 @@ import { XY_VIS } from '../constants'; describe('xyVis', () => { test('it renders with the specified data and args', async () => { const { data, args } = sampleArgs(); - const newData = { - ...data, - type: 'datatable', - - columns: data.columns.map((c) => - c.id !== 'c' - ? c - : { - ...c, - meta: { - type: 'string', - }, - } - ), - } as Datatable; - const { layers, ...rest } = args; const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer; const result = await xyVisFunction.fn( - newData, + data, { ...rest, ...restLayerArgs, referenceLines: [], annotationLayers: [] }, createMockExecutionContext() ); @@ -45,7 +28,7 @@ describe('xyVis', () => { value: { args: { ...rest, - layers: [{ layerType, table: newData, layerId: 'dataLayers-0', type, ...restLayerArgs }], + layers: [{ layerType, table: data, layerId: 'dataLayers-0', type, ...restLayerArgs }], }, }, }); @@ -120,6 +103,25 @@ describe('xyVis', () => { ).rejects.toThrowErrorMatchingSnapshot(); }); + test('it should throw error if addTimeMarker applied for not time chart', async () => { + const { data, args } = sampleArgs(); + const { layers, ...rest } = args; + const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer; + expect( + xyVisFunction.fn( + data, + { + ...rest, + ...restLayerArgs, + addTimeMarker: true, + referenceLines: [], + annotationLayers: [], + }, + createMockExecutionContext() + ) + ).rejects.toThrowErrorMatchingSnapshot(); + }); + test('it should throw error if splitRowAccessor is pointing to the absent column', async () => { const { data, args } = sampleArgs(); const { layers, ...rest } = args; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index 3de2dd35831e4..4c25e3378d523 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -25,6 +25,7 @@ import { validateFillOpacity, validateMarkSizeRatioLimits, validateValueLabels, + validateAddTimeMarker, validateMinTimeBarInterval, validateMarkSizeForChartType, validateMarkSizeRatioWithAccessor, @@ -107,6 +108,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { validateExtent(args.yLeftExtent, hasBar || hasArea, dataLayers); validateExtent(args.yRightExtent, hasBar || hasArea, dataLayers); validateFillOpacity(args.fillOpacity, hasArea); + validateAddTimeMarker(dataLayers, args.addTimeMarker); validateMinTimeBarInterval(dataLayers, hasBar, args.minTimeBarInterval); const hasNotHistogramBars = !hasHistogramBarLayer(dataLayers); diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.ts index 8ddbc4bc97f10..66d4c11a9f7ae 100644 --- a/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.ts +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.ts @@ -6,13 +6,16 @@ * Side Public License, v 1. */ +import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils'; import { XScaleTypes } from '../constants'; import { CommonXYDataLayerConfigResult } from '../types'; export function isTimeChart(layers: CommonXYDataLayerConfigResult[]) { return layers.every( (l): l is CommonXYDataLayerConfigResult => - l.table.columns.find((col) => col.id === l.xAccessor)?.meta.type === 'date' && - l.xScaleType === XScaleTypes.TIME + (l.xAccessor + ? getColumnByAccessor(l.xAccessor, l.table.columns)?.meta.type === 'date' + : false) && + (!l.xScaleType || l.xScaleType === XScaleTypes.TIME) ); } diff --git a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx index ba26bb973f64f..ed2ef4a7a57ce 100644 --- a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx @@ -121,6 +121,10 @@ export const strings = { i18n.translate('expressionXY.xyVis.ariaLabel.help', { defaultMessage: 'Specifies the aria label of the xy chart', }), + getAddTimeMakerHelp: () => + i18n.translate('expressionXY.xyVis.addTimeMaker.help', { + defaultMessage: 'Show time marker', + }), getMarkSizeRatioHelp: () => i18n.translate('expressionXY.xyVis.markSizeRatio.help', { defaultMessage: 'Specifies the ratio of the dots at the line and area charts', diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index 0a7b93c495c29..c0336fc67536f 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -207,6 +207,7 @@ export interface XYArgs extends DataLayerArgs { hideEndzones?: boolean; valuesInLegend?: boolean; ariaLabel?: string; + addTimeMarker?: boolean; markSizeRatio?: number; minTimeBarInterval?: string; splitRowAccessor?: ExpressionValueVisDimension | string; @@ -236,6 +237,7 @@ export interface LayeredXYArgs { hideEndzones?: boolean; valuesInLegend?: boolean; ariaLabel?: string; + addTimeMarker?: boolean; markSizeRatio?: number; minTimeBarInterval?: string; } @@ -263,6 +265,7 @@ export interface XYProps { hideEndzones?: boolean; valuesInLegend?: boolean; ariaLabel?: string; + addTimeMarker?: boolean; markSizeRatio?: number; minTimeBarInterval?: string; splitRowAccessor?: ExpressionValueVisDimension | string; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index e7a26ec20bbfc..c3d1fc980ad01 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -334,6 +334,10 @@ exports[`XYChart component it renders area 1`] = ` } } /> + + + + + + + + + + = ({ @@ -67,6 +69,7 @@ export const DataLayers: FC = ({ shouldShowValueLabels, formattedDatatables, chartHasMoreThanOneBarSeries, + defaultXScaleType, }) => { const colorAssignments = getColorAssignments(layers, formatFactory); return ( @@ -104,6 +107,7 @@ export const DataLayers: FC = ({ timeZone, emphasizeFitting, fillOpacity, + defaultXScaleType, }); const index = `${layer.layerId}-${accessorIndex}`; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index d03a5e648f366..91e5ae8ad1484 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -1967,17 +1967,10 @@ describe('XYChart component', () => { test('it should pass the formatter function to the axis', () => { const { args } = sampleArgs(); - const instance = shallow(); - - const tickFormatter = instance.find(Axis).first().prop('tickFormat'); - - if (!tickFormatter) { - throw new Error('tickFormatter prop not found'); - } - - tickFormatter('I'); + shallow(); - expect(convertSpy).toHaveBeenCalledWith('I'); + expect(convertSpy).toHaveBeenCalledWith(1652034840000); + expect(convertSpy).toHaveBeenCalledWith(1652122440000); }); test('it should set the tickLabel visibility on the x axis if the tick labels is hidden', () => { diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index 80048bcb84038..7eceb72ecf75d 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -42,6 +42,7 @@ import { LegendSizeToPixels, } from '@kbn/visualizations-plugin/common/constants'; import type { FilterEvent, BrushEvent, FormatFactory } from '../types'; +import { isTimeChart } from '../../common/helpers'; import type { CommonXYDataLayerConfig, ExtendedYConfig, @@ -81,8 +82,10 @@ import { OUTSIDE_RECT_ANNOTATION_WIDTH, OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION, } from './annotations'; -import { AxisExtentModes, SeriesTypes, ValueLabelModes } from '../../common/constants'; +import { AxisExtentModes, SeriesTypes, ValueLabelModes, XScaleTypes } from '../../common/constants'; import { DataLayers } from './data_layers'; +import { XYCurrentTime } from './xy_current_time'; + import './xy_chart.scss'; declare global { @@ -249,7 +252,10 @@ export function XYChart({ filteredBarLayers.some((layer) => layer.accessors.length > 1) || filteredBarLayers.some((layer) => isDataLayer(layer) && layer.splitAccessor); - const isTimeViz = Boolean(dataLayers.every((l) => l.xScaleType === 'time')); + const isTimeViz = isTimeChart(dataLayers); + + const defaultXScaleType = isTimeViz ? XScaleTypes.TIME : XScaleTypes.ORDINAL; + const isHistogramViz = dataLayers.every((l) => l.isHistogram); const { baseDomain: rawXDomain, extendedDomain: xDomain } = getXDomain( @@ -604,6 +610,11 @@ export function XYChart({ ariaLabel={args.ariaLabel} ariaUseDefaultSummary={!args.ariaLabel} /> + )} {referenceLineLayers.length ? ( diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_current_time.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_current_time.tsx new file mode 100644 index 0000000000000..68f1dd0d60b13 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_current_time.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC } from 'react'; +import { DomainRange } from '@elastic/charts'; +import { CurrentTime } from '@kbn/charts-plugin/public'; + +interface XYCurrentTime { + enabled: boolean; + isDarkMode: boolean; + domain?: DomainRange; +} + +export const XYCurrentTime: FC = ({ enabled, isDarkMode, domain }) => { + if (!enabled) { + return null; + } + + const domainEnd = domain && 'max' in domain ? domain.max : undefined; + return ; +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx index 7ac661ed9709d..08761f633f851 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx @@ -53,6 +53,7 @@ type GetSeriesPropsFn = (config: { emphasizeFitting?: boolean; fillOpacity?: number; formattedDatatableInfo: DatatableWithFormatInfo; + defaultXScaleType: XScaleType; }) => SeriesSpec; type GetSeriesNameFn = ( @@ -280,6 +281,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({ emphasizeFitting, fillOpacity, formattedDatatableInfo, + defaultXScaleType, }): SeriesSpec => { const { table, markSizeAccessor } = layer; const isStacked = layer.seriesType.includes('stacked'); @@ -342,7 +344,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({ markSizeAccessor: markSizeColumnId, markFormat: (value) => markFormatter.convert(value), data: rows, - xScaleType: xColumnId ? layer.xScaleType : 'ordinal', + xScaleType: xColumnId ? layer.xScaleType ?? defaultXScaleType : 'ordinal', yScaleType: formatter?.id === 'bytes' && yAxis?.scale === ScaleType.Linear ? ScaleType.LinearBinary diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts index a9f68ffc0a29b..5c202bb6200a9 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts @@ -9,13 +9,14 @@ import { search } from '@kbn/data-plugin/public'; import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils'; import { XYChartProps } from '../../common'; +import { isTimeChart } from '../../common/helpers'; import { getFilteredLayers } from './layers'; -import { isDataLayer } from './visualization'; +import { isDataLayer, getDataLayers } from './visualization'; export function calculateMinInterval({ args: { layers, minTimeBarInterval } }: XYChartProps) { const filteredLayers = getFilteredLayers(layers); if (filteredLayers.length === 0) return; - const isTimeViz = filteredLayers.every((l) => isDataLayer(l) && l.xScaleType === 'time'); + const isTimeViz = isTimeChart(getDataLayers(filteredLayers)); const xColumn = isDataLayer(filteredLayers[0]) && filteredLayers[0].xAccessor && From 569e10a6b81ae287b5395d1b3af83a441dd2d9ee Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 20 May 2022 11:51:04 +0200 Subject: [PATCH 025/120] expose docLinks from ConfigDeprecationContext (#132424) * expose docLinks from ConfigDeprecationContext * fix mock * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-config/BUILD.bazel | 2 ++ .../src/config_service.test.mocks.ts | 11 ++++++++++ .../kbn-config/src/config_service.test.ts | 11 +++++++++- packages/kbn-config/src/config_service.ts | 20 +++++++++++-------- .../deprecation/apply_deprecations.test.ts | 2 ++ .../src/deprecation/deprecations.mock.ts | 2 ++ packages/kbn-config/src/deprecation/types.ts | 3 +++ 7 files changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/kbn-config/BUILD.bazel b/packages/kbn-config/BUILD.bazel index 3567c549a77c4..e735e2cb346eb 100644 --- a/packages/kbn-config/BUILD.bazel +++ b/packages/kbn-config/BUILD.bazel @@ -38,6 +38,7 @@ RUNTIME_DEPS = [ "//packages/kbn-utility-types", "//packages/kbn-i18n", "//packages/kbn-plugin-discovery", + "//packages/kbn-doc-links", "@npm//js-yaml", "@npm//load-json-file", "@npm//lodash", @@ -54,6 +55,7 @@ TYPES_DEPS = [ "//packages/kbn-utility-types:npm_module_types", "//packages/kbn-i18n:npm_module_types", "//packages/kbn-plugin-discovery:npm_module_types", + "//packages/kbn-doc-links:npm_module_types", "@npm//load-json-file", "@npm//rxjs", "@npm//@types/jest", diff --git a/packages/kbn-config/src/config_service.test.mocks.ts b/packages/kbn-config/src/config_service.test.mocks.ts index 39aa551ae85f9..40379e69a3cb2 100644 --- a/packages/kbn-config/src/config_service.test.mocks.ts +++ b/packages/kbn-config/src/config_service.test.mocks.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import type { DocLinks } from '@kbn/doc-links'; + export const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); import type { applyDeprecations } from './deprecation/apply_deprecations'; @@ -26,3 +28,12 @@ export const mockApplyDeprecations = jest.fn< jest.mock('./deprecation/apply_deprecations', () => ({ applyDeprecations: mockApplyDeprecations, })); + +export const docLinksMock = { + settings: 'settings', +} as DocLinks; +export const getDocLinksMock = jest.fn().mockReturnValue(docLinksMock); + +jest.doMock('@kbn/doc-links', () => ({ + getDocLinks: getDocLinksMock, +})); diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index 51e67956637ee..b427af4e50229 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -9,7 +9,12 @@ import { BehaviorSubject, Observable } from 'rxjs'; import { first, take } from 'rxjs/operators'; -import { mockApplyDeprecations, mockedChangedPaths } from './config_service.test.mocks'; +import { + mockApplyDeprecations, + mockedChangedPaths, + docLinksMock, + getDocLinksMock, +} from './config_service.test.mocks'; import { rawConfigServiceMock } from './raw/raw_config_service.mock'; import { schema } from '@kbn/config-schema'; @@ -39,6 +44,7 @@ const getRawConfigProvider = (rawConfig: Record) => beforeEach(() => { logger = loggerMock.create(); mockApplyDeprecations.mockClear(); + getDocLinksMock.mockClear(); }); test('returns config at path as observable', async () => { @@ -469,6 +475,7 @@ test('calls `applyDeprecations` with the correct parameters', async () => { const context: ConfigDeprecationContext = { branch: defaultEnv.packageInfo.branch, version: defaultEnv.packageInfo.version, + docLinks: docLinksMock, }; const deprecationA = jest.fn(); @@ -479,6 +486,8 @@ test('calls `applyDeprecations` with the correct parameters', async () => { await configService.validate(); + expect(getDocLinksMock).toHaveBeenCalledTimes(1); + expect(mockApplyDeprecations).toHaveBeenCalledTimes(1); expect(mockApplyDeprecations).toHaveBeenCalledWith( cfg, diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts index bb7bb54e75ce5..0da30aad0e232 100644 --- a/packages/kbn-config/src/config_service.ts +++ b/packages/kbn-config/src/config_service.ts @@ -12,6 +12,7 @@ import { isEqual } from 'lodash'; import { BehaviorSubject, combineLatest, firstValueFrom, Observable } from 'rxjs'; import { distinctUntilChanged, first, map, shareReplay, tap } from 'rxjs/operators'; import { Logger, LoggerFactory } from '@kbn/logging'; +import { getDocLinks, DocLinks } from '@kbn/doc-links'; import { Config, ConfigPath, Env } from '.'; import { hasConfigPathIntersection } from './config'; @@ -42,6 +43,7 @@ export interface ConfigValidateParameters { export class ConfigService { private readonly log: Logger; private readonly deprecationLog: Logger; + private readonly docLinks: DocLinks; private validated = false; private readonly config$: Observable; @@ -67,6 +69,7 @@ export class ConfigService { ) { this.log = logger.get('config'); this.deprecationLog = logger.get('config', 'deprecation'); + this.docLinks = getDocLinks({ kibanaBranch: env.packageInfo.branch }); this.config$ = combineLatest([this.rawConfigProvider.getConfig$(), this.deprecations]).pipe( map(([rawConfig, deprecations]) => { @@ -104,7 +107,7 @@ export class ConfigService { ...provider(configDeprecationFactory).map((deprecation) => ({ deprecation, path: flatPath, - context: createDeprecationContext(this.env), + context: this.createDeprecationContext(), })), ]); } @@ -262,6 +265,14 @@ export class ConfigService { handledDeprecatedConfig.push(config); this.handledDeprecatedConfigs.set(domainId, handledDeprecatedConfig); } + + private createDeprecationContext(): ConfigDeprecationContext { + return { + branch: this.env.packageInfo.branch, + version: this.env.packageInfo.version, + docLinks: this.docLinks, + }; + } } const pathToString = (path: ConfigPath) => (Array.isArray(path) ? path.join('.') : path); @@ -272,10 +283,3 @@ const pathToString = (path: ConfigPath) => (Array.isArray(path) ? path.join('.') */ const isPathHandled = (path: string, handledPaths: string[]) => handledPaths.some((handledPath) => hasConfigPathIntersection(path, handledPath)); - -const createDeprecationContext = (env: Env): ConfigDeprecationContext => { - return { - branch: env.packageInfo.branch, - version: env.packageInfo.version, - }; -}; diff --git a/packages/kbn-config/src/deprecation/apply_deprecations.test.ts b/packages/kbn-config/src/deprecation/apply_deprecations.test.ts index 5acf725ba93a6..73e7b2b422017 100644 --- a/packages/kbn-config/src/deprecation/apply_deprecations.test.ts +++ b/packages/kbn-config/src/deprecation/apply_deprecations.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { DocLinks } from '@kbn/doc-links'; import { applyDeprecations } from './apply_deprecations'; import { ConfigDeprecation, ConfigDeprecationContext, ConfigDeprecationWithContext } from './types'; import { configDeprecationFactory as deprecations } from './deprecation_factory'; @@ -14,6 +15,7 @@ describe('applyDeprecations', () => { const context: ConfigDeprecationContext = { version: '7.16.2', branch: '7.16', + docLinks: {} as DocLinks, }; const wrapHandler = ( diff --git a/packages/kbn-config/src/deprecation/deprecations.mock.ts b/packages/kbn-config/src/deprecation/deprecations.mock.ts index 80b65c84b4879..06b467290b47e 100644 --- a/packages/kbn-config/src/deprecation/deprecations.mock.ts +++ b/packages/kbn-config/src/deprecation/deprecations.mock.ts @@ -6,12 +6,14 @@ * Side Public License, v 1. */ +import type { DocLinks } from '@kbn/doc-links'; import type { ConfigDeprecationContext } from './types'; const createMockedContext = (): ConfigDeprecationContext => { return { branch: 'master', version: '8.0.0', + docLinks: {} as DocLinks, }; }; diff --git a/packages/kbn-config/src/deprecation/types.ts b/packages/kbn-config/src/deprecation/types.ts index 052741c0b4be3..6d656ab97921f 100644 --- a/packages/kbn-config/src/deprecation/types.ts +++ b/packages/kbn-config/src/deprecation/types.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ import type { RecursiveReadonly } from '@kbn/utility-types'; +import type { DocLinks } from '@kbn/doc-links'; /** * Config deprecation hook used when invoking a {@link ConfigDeprecation} @@ -77,6 +78,8 @@ export interface ConfigDeprecationContext { version: string; /** The current Kibana branch, e.g `7.x`, `7.16`, `master` */ branch: string; + /** Allow direct access to the doc links from the deprecation handler */ + docLinks: DocLinks; } /** From 968f7a9ed3cdf15f0e337fef1954816571ca3041 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Fri, 20 May 2022 12:52:26 +0300 Subject: [PATCH 026/120] Remove `injectedMetadata` in `vega` (#132521) * Remove injectedMetadata in vega * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/vis_types/vega/public/data_model/search_api.ts | 3 +-- src/plugins/vis_types/vega/public/plugin.ts | 3 --- src/plugins/vis_types/vega/public/services.ts | 6 +----- src/plugins/vis_types/vega/public/vega_request_handler.ts | 3 +-- .../vega/public/vega_view/vega_map_view/view.test.ts | 2 -- .../vis_types/vega/public/vega_visualization.test.js | 3 --- 6 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/plugins/vis_types/vega/public/data_model/search_api.ts b/src/plugins/vis_types/vega/public/data_model/search_api.ts index 530449da9aa26..40238b445c8c2 100644 --- a/src/plugins/vis_types/vega/public/data_model/search_api.ts +++ b/src/plugins/vis_types/vega/public/data_model/search_api.ts @@ -8,7 +8,7 @@ import { combineLatest, from } from 'rxjs'; import { map, tap, switchMap } from 'rxjs/operators'; -import type { CoreStart, IUiSettingsClient, KibanaExecutionContext } from '@kbn/core/public'; +import type { IUiSettingsClient, KibanaExecutionContext } from '@kbn/core/public'; import { getSearchParamsFromRequest, SearchRequest, @@ -47,7 +47,6 @@ export const extendSearchParamsWithRuntimeFields = async ( export interface SearchAPIDependencies { uiSettings: IUiSettingsClient; - injectedMetadata: CoreStart['injectedMetadata']; search: DataPublicPluginStart['search']; indexPatterns: DataViewsPublicPluginStart; } diff --git a/src/plugins/vis_types/vega/public/plugin.ts b/src/plugins/vis_types/vega/public/plugin.ts index a95d646427306..c9af49f009dee 100644 --- a/src/plugins/vis_types/vega/public/plugin.ts +++ b/src/plugins/vis_types/vega/public/plugin.ts @@ -20,7 +20,6 @@ import { setDataViews, setInjectedVars, setUISettings, - setInjectedMetadata, setDocLinks, setMapsEms, } from './services'; @@ -73,7 +72,6 @@ export class VegaPlugin implements Plugin { ) { setInjectedVars({ enableExternalUrls: this.initializerContext.config.get().enableExternalUrls, - emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), }); setUISettings(core.uiSettings); @@ -98,7 +96,6 @@ export class VegaPlugin implements Plugin { setNotifications(core.notifications); setData(data); setDataViews(dataViews); - setInjectedMetadata(core.injectedMetadata); setDocLinks(core.docLinks); setMapsEms(mapsEms); } diff --git a/src/plugins/vis_types/vega/public/services.ts b/src/plugins/vis_types/vega/public/services.ts index f7f0444803a00..304d9965f056d 100644 --- a/src/plugins/vis_types/vega/public/services.ts +++ b/src/plugins/vis_types/vega/public/services.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { CoreStart, NotificationsStart, IUiSettingsClient, DocLinksStart } from '@kbn/core/public'; +import { NotificationsStart, IUiSettingsClient, DocLinksStart } from '@kbn/core/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; @@ -24,12 +24,8 @@ export const [getNotifications, setNotifications] = export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); export const [getMapsEms, setMapsEms] = createGetterSetter('mapsEms'); -export const [getInjectedMetadata, setInjectedMetadata] = - createGetterSetter('InjectedMetadata'); - export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ enableExternalUrls: boolean; - emsTileLayerId: unknown; }>('InjectedVars'); export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; diff --git a/src/plugins/vis_types/vega/public/vega_request_handler.ts b/src/plugins/vis_types/vega/public/vega_request_handler.ts index 8670fd9499529..84b5663df0be6 100644 --- a/src/plugins/vis_types/vega/public/vega_request_handler.ts +++ b/src/plugins/vis_types/vega/public/vega_request_handler.ts @@ -15,7 +15,7 @@ import { TimeCache } from './data_model/time_cache'; import { VegaVisualizationDependencies } from './plugin'; import { VisParams } from './vega_fn'; -import { getData, getInjectedMetadata, getDataViews } from './services'; +import { getData, getDataViews } from './services'; import { VegaInspectorAdapters } from './vega_inspector'; interface VegaRequestHandlerParams { @@ -57,7 +57,6 @@ export function createVegaRequestHandler( uiSettings, search, indexPatterns: dataViews, - injectedMetadata: getInjectedMetadata(), }, context.abortSignal, context.inspectorAdapters, diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts index 6c0d693349ef6..eafe75534154a 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts @@ -116,7 +116,6 @@ describe('vega_map_view/view', () => { let vegaParser: VegaParser; setInjectedVars({ - emsTileLayerId: {}, enableExternalUrls: true, }); setData(dataPluginStart); @@ -150,7 +149,6 @@ describe('vega_map_view/view', () => { search: dataPluginStart.search, indexPatterns: dataViewsStart, uiSettings: coreStart.uiSettings, - injectedMetadata: coreStart.injectedMetadata, }), new TimeCache(dataPluginStart.query.timefilter.timefilter, 0), {}, diff --git a/src/plugins/vis_types/vega/public/vega_visualization.test.js b/src/plugins/vis_types/vega/public/vega_visualization.test.js index d1c821e962021..024d935a2f356 100644 --- a/src/plugins/vis_types/vega/public/vega_visualization.test.js +++ b/src/plugins/vis_types/vega/public/vega_visualization.test.js @@ -56,7 +56,6 @@ describe('VegaVisualizations', () => { beforeEach(() => { setInjectedVars({ - emsTileLayerId: {}, enableExternalUrls: true, }); setData(dataPluginStart); @@ -97,7 +96,6 @@ describe('VegaVisualizations', () => { search: dataPluginStart.search, indexPatterns: dataViewsPluginStart, uiSettings: coreStart.uiSettings, - injectedMetadata: coreStart.injectedMetadata, }), 0, 0, @@ -130,7 +128,6 @@ describe('VegaVisualizations', () => { search: dataPluginStart.search, indexPatterns: dataViewsPluginStart, uiSettings: coreStart.uiSettings, - injectedMetadata: coreStart.injectedMetadata, }), 0, 0, From f88b140f9f23869590df985097e9739859bfbec1 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 20 May 2022 12:16:22 +0200 Subject: [PATCH 027/120] [data.query] Add `getState()` api to retrieve whole `QueryState` (#132035) --- .../lib/sync_dashboard_filter_state.ts | 4 +- src/plugins/dashboard/public/locator.ts | 9 +- src/plugins/data/README.mdx | 2 + src/plugins/data/public/index.ts | 2 + src/plugins/data/public/query/index.tsx | 1 + src/plugins/data/public/query/mocks.ts | 2 + .../data/public/query/query_service.test.ts | 91 +++++++++++++++++++ .../data/public/query/query_service.ts | 13 ++- src/plugins/data/public/query/query_state.ts | 40 ++++++++ .../state_sync/connect_to_query_state.test.ts | 5 +- .../state_sync/connect_to_query_state.ts | 6 +- ...le.ts => create_query_state_observable.ts} | 27 +++--- .../data/public/query/state_sync/index.ts | 4 +- .../state_sync/sync_state_with_url.test.ts | 8 +- .../query/state_sync/sync_state_with_url.ts | 20 ++-- .../data/public/query/state_sync/types.ts | 23 ++--- src/plugins/discover/public/locator.ts | 11 ++- .../utils/get_visualize_list_item_link.ts | 12 ++- .../index_data_visualizer/locator/locator.ts | 4 +- x-pack/plugins/maps/public/locators.ts | 11 ++- .../saved_map/get_initial_refresh_config.ts | 4 +- .../saved_map/get_initial_time_filters.ts | 4 +- 22 files changed, 240 insertions(+), 63 deletions(-) create mode 100644 src/plugins/data/public/query/query_service.test.ts create mode 100644 src/plugins/data/public/query/query_state.ts rename src/plugins/data/public/query/state_sync/{create_global_query_observable.ts => create_query_state_observable.ts} (79%) diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts index ff64f4672922c..94c9d996499c3 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts @@ -20,7 +20,7 @@ import { Filter, Query, waitUntilNextSessionCompletes$, - QueryState, + GlobalQueryStateFromUrl, } from '../../services/data'; import { cleanFiltersForSerialize } from '.'; @@ -166,7 +166,7 @@ export const applyDashboardFilterState = ({ * time range and refresh interval to the query service. */ if (currentDashboardState.timeRestore) { - const globalQueryState = kbnUrlStateStorage.get('_g'); + const globalQueryState = kbnUrlStateStorage.get('_g'); if (!globalQueryState?.time) { if (savedDashboard.timeFrom && savedDashboard.timeTo) { timefilterService.setTime({ diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts index 9c187ca0803cf..7649343e5bf6e 100644 --- a/src/plugins/dashboard/public/locator.ts +++ b/src/plugins/dashboard/public/locator.ts @@ -9,7 +9,12 @@ import type { SerializableRecord } from '@kbn/utility-types'; import { flow } from 'lodash'; import { type Filter } from '@kbn/es-query'; -import type { TimeRange, Query, QueryState, RefreshInterval } from '@kbn/data-plugin/public'; +import type { + TimeRange, + Query, + GlobalQueryStateFromUrl, + RefreshInterval, +} from '@kbn/data-plugin/public'; import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; @@ -155,7 +160,7 @@ export class DashboardAppLocatorDefinition implements LocatorDefinition( + path = setStateToKbnUrl( '_g', cleanEmptyKeys({ time: params.timeRange, diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx index e24a949a0c2ec..a8cb06ff9e60b 100644 --- a/src/plugins/data/README.mdx +++ b/src/plugins/data/README.mdx @@ -91,6 +91,8 @@ function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) { ``` +You can also retrieve a snapshot of the whole `QueryState` by using `data.query.getState()` + ### Timefilter `data.query.timefilter` is responsible for the time range filter and the auto refresh behavior settings. diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 90169ca552ac2..0f50384893b18 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -255,6 +255,7 @@ export { createSavedQueryService, connectToQueryState, syncQueryStateWithUrl, + syncGlobalQueryStateWithUrl, getDefaultQuery, FilterManager, TimeHistory, @@ -280,6 +281,7 @@ export type { QueryStringContract, QuerySetup, TimefilterSetup, + GlobalQueryStateFromUrl, } from './query'; export type { AggsStart } from './search/aggs'; diff --git a/src/plugins/data/public/query/index.tsx b/src/plugins/data/public/query/index.tsx index f426573e1bd6c..392b8fda14417 100644 --- a/src/plugins/data/public/query/index.tsx +++ b/src/plugins/data/public/query/index.tsx @@ -15,3 +15,4 @@ export * from './saved_query'; export * from './persisted_log'; export * from './state_sync'; export type { QueryStringContract } from './query_string'; +export type { QueryState } from './query_state'; diff --git a/src/plugins/data/public/query/mocks.ts b/src/plugins/data/public/query/mocks.ts index 2ab15aab26db6..a2d73e5b5ce34 100644 --- a/src/plugins/data/public/query/mocks.ts +++ b/src/plugins/data/public/query/mocks.ts @@ -21,6 +21,7 @@ const createSetupContractMock = () => { timefilter: timefilterServiceMock.createSetupContract(), queryString: queryStringManagerMock.createSetupContract(), state$: new Observable(), + getState: jest.fn(), }; return setupContract; @@ -33,6 +34,7 @@ const createStartContractMock = () => { queryString: queryStringManagerMock.createStartContract(), savedQueries: jest.fn() as any, state$: new Observable(), + getState: jest.fn(), timefilter: timefilterServiceMock.createStartContract(), getEsQuery: jest.fn(), }; diff --git a/src/plugins/data/public/query/query_service.test.ts b/src/plugins/data/public/query/query_service.test.ts new file mode 100644 index 0000000000000..5eb6815c3ba20 --- /dev/null +++ b/src/plugins/data/public/query/query_service.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FilterStateStore } from '@kbn/es-query'; +import { FilterManager } from './filter_manager'; +import { QueryStringContract } from './query_string'; +import { getFilter } from './filter_manager/test_helpers/get_stub_filter'; +import { UI_SETTINGS } from '../../common'; +import { coreMock } from '@kbn/core/public/mocks'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { QueryService, QueryStart } from './query_service'; +import { StubBrowserStorage } from '@kbn/test-jest-helpers'; +import { TimefilterContract } from './timefilter'; +import { createNowProviderMock } from '../now_provider/mocks'; + +const setupMock = coreMock.createSetup(); +const startMock = coreMock.createStart(); + +setupMock.uiSettings.get.mockImplementation((key: string) => { + switch (key) { + case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT: + return true; + case UI_SETTINGS.SEARCH_QUERY_LANGUAGE: + return 'kuery'; + case UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS: + return { from: 'now-15m', to: 'now' }; + case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: + return { pause: false, value: 0 }; + default: + throw new Error(`query_service test: not mocked uiSetting: ${key}`); + } +}); + +describe('query_service', () => { + let queryServiceStart: QueryStart; + let filterManager: FilterManager; + let timeFilter: TimefilterContract; + let queryStringManager: QueryStringContract; + + beforeEach(() => { + const queryService = new QueryService(); + queryService.setup({ + uiSettings: setupMock.uiSettings, + storage: new Storage(new StubBrowserStorage()), + nowProvider: createNowProviderMock(), + }); + queryServiceStart = queryService.start({ + uiSettings: setupMock.uiSettings, + storage: new Storage(new StubBrowserStorage()), + http: startMock.http, + }); + filterManager = queryServiceStart.filterManager; + timeFilter = queryServiceStart.timefilter.timefilter; + queryStringManager = queryServiceStart.queryString; + }); + + test('state is initialized with state from query service', () => { + const state = queryServiceStart.getState(); + + expect(state).toEqual({ + filters: filterManager.getFilters(), + refreshInterval: timeFilter.getRefreshInterval(), + time: timeFilter.getTime(), + query: queryStringManager.getQuery(), + }); + }); + + test('state is updated when underlying state in service updates', () => { + const filters = [getFilter(FilterStateStore.GLOBAL_STATE, true, true, 'key1', 'value1')]; + const query = { language: 'kql', query: 'query' }; + const time = { from: new Date().toISOString(), to: new Date().toISOString() }; + const refreshInterval = { pause: false, value: 10 }; + + filterManager.setFilters(filters); + queryStringManager.setQuery(query); + timeFilter.setTime(time); + timeFilter.setRefreshInterval(refreshInterval); + + expect(queryServiceStart.getState()).toEqual({ + filters, + refreshInterval, + time, + query, + }); + }); +}); diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index 1b634fda28996..8b309c9821d3e 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -15,7 +15,8 @@ import { createAddToQueryLog } from './lib'; import { TimefilterService } from './timefilter'; import type { TimefilterSetup } from './timefilter'; import { createSavedQueryService } from './saved_query/saved_query_service'; -import { createQueryStateObservable } from './state_sync/create_global_query_observable'; +import { createQueryStateObservable } from './state_sync/create_query_state_observable'; +import { getQueryState } from './query_state'; import type { QueryStringContract } from './query_string'; import { QueryStringManager } from './query_string'; import { getEsQueryConfig, TimeRange } from '../../common'; @@ -69,6 +70,7 @@ export class QueryService { timefilter: this.timefilter, queryString: this.queryStringManager, state$: this.state$, + getState: () => this.getQueryState(), }; } @@ -82,6 +84,7 @@ export class QueryService { queryString: this.queryStringManager, savedQueries: createSavedQueryService(http), state$: this.state$, + getState: () => this.getQueryState(), timefilter: this.timefilter, getEsQuery: (indexPattern: IndexPattern, timeRange?: TimeRange) => { const timeFilter = this.timefilter.timefilter.createFilter(indexPattern, timeRange); @@ -99,6 +102,14 @@ export class QueryService { public stop() { // nothing to do here yet } + + private getQueryState() { + return getQueryState({ + timefilter: this.timefilter, + queryString: this.queryStringManager, + filterManager: this.filterManager, + }); + } } /** @public */ diff --git a/src/plugins/data/public/query/query_state.ts b/src/plugins/data/public/query/query_state.ts new file mode 100644 index 0000000000000..77242c981bda2 --- /dev/null +++ b/src/plugins/data/public/query/query_state.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Filter } from '@kbn/es-query'; +import type { TimefilterSetup } from './timefilter'; +import type { FilterManager } from './filter_manager'; +import type { QueryStringContract } from './query_string'; +import type { RefreshInterval, TimeRange, Query } from '../../common'; + +/** + * All query state service state + */ +export interface QueryState { + time?: TimeRange; + refreshInterval?: RefreshInterval; + filters?: Filter[]; + query?: Query; +} + +export function getQueryState({ + timefilter: { timefilter }, + filterManager, + queryString, +}: { + timefilter: TimefilterSetup; + filterManager: FilterManager; + queryString: QueryStringContract; +}): QueryState { + return { + time: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + filters: filterManager.getFilters(), + query: queryString.getQuery(), + }; +} diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index d1d3ea5865c7e..515cc38783cbd 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -7,16 +7,17 @@ */ import { Subscription } from 'rxjs'; +import { Filter, FilterStateStore } from '@kbn/es-query'; import { FilterManager } from '../filter_manager'; import { getFilter } from '../filter_manager/test_helpers/get_stub_filter'; -import { Filter, FilterStateStore, UI_SETTINGS } from '../../../common'; +import { UI_SETTINGS } from '../../../common'; import { coreMock } from '@kbn/core/public/mocks'; import { BaseStateContainer, createStateContainer, Storage } from '@kbn/kibana-utils-plugin/public'; import { QueryService, QueryStart } from '../query_service'; import { StubBrowserStorage } from '@kbn/test-jest-helpers'; import { connectToQueryState } from './connect_to_query_state'; import { TimefilterContract } from '../timefilter'; -import { QueryState } from './types'; +import { QueryState } from '../query_state'; import { createNowProviderMock } from '../../now_provider/mocks'; const connectToQueryGlobalState = (query: QueryStart, state: BaseStateContainer) => diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts index b9bb05841f161..a625dff04b0a3 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts @@ -9,10 +9,12 @@ import { Subscription } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import _ from 'lodash'; +import { COMPARE_ALL_OPTIONS, compareFilters } from '@kbn/es-query'; import { BaseStateContainer } from '@kbn/kibana-utils-plugin/public'; import { QuerySetup, QueryStart } from '../query_service'; -import { QueryState, QueryStateChange } from './types'; -import { FilterStateStore, COMPARE_ALL_OPTIONS, compareFilters } from '../../../common'; +import { QueryState } from '../query_state'; +import { QueryStateChange } from './types'; +import { FilterStateStore } from '../../../common'; import { validateTimeRange } from '../timefilter'; /** diff --git a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts b/src/plugins/data/public/query/state_sync/create_query_state_observable.ts similarity index 79% rename from src/plugins/data/public/query/state_sync/create_global_query_observable.ts rename to src/plugins/data/public/query/state_sync/create_query_state_observable.ts index 2e054229a55da..39e7802753ee2 100644 --- a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts +++ b/src/plugins/data/public/query/state_sync/create_query_state_observable.ts @@ -8,16 +8,16 @@ import { Observable, Subscription } from 'rxjs'; import { map, tap } from 'rxjs/operators'; -import { isFilterPinned } from '@kbn/es-query'; +import { COMPARE_ALL_OPTIONS, compareFilters, isFilterPinned } from '@kbn/es-query'; import { createStateContainer } from '@kbn/kibana-utils-plugin/public'; import type { TimefilterSetup } from '../timefilter'; import { FilterManager } from '../filter_manager'; -import { QueryState, QueryStateChange } from '.'; -import { compareFilters, COMPARE_ALL_OPTIONS } from '../../../common'; +import { getQueryState, QueryState } from '../query_state'; +import { QueryStateChange } from './types'; import type { QueryStringContract } from '../query_string'; export function createQueryStateObservable({ - timefilter: { timefilter }, + timefilter, filterManager, queryString, }: { @@ -25,27 +25,24 @@ export function createQueryStateObservable({ filterManager: FilterManager; queryString: QueryStringContract; }): Observable<{ changes: QueryStateChange; state: QueryState }> { - return new Observable((subscriber) => { - const state = createStateContainer({ - time: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - filters: filterManager.getFilters(), - query: queryString.getQuery(), - }); + const state = createStateContainer( + getQueryState({ timefilter, filterManager, queryString }) + ); + return new Observable((subscriber) => { let currentChange: QueryStateChange = {}; const subs: Subscription[] = [ queryString.getUpdates$().subscribe(() => { currentChange.query = true; state.set({ ...state.get(), query: queryString.getQuery() }); }), - timefilter.getTimeUpdate$().subscribe(() => { + timefilter.timefilter.getTimeUpdate$().subscribe(() => { currentChange.time = true; - state.set({ ...state.get(), time: timefilter.getTime() }); + state.set({ ...state.get(), time: timefilter.timefilter.getTime() }); }), - timefilter.getRefreshIntervalUpdate$().subscribe(() => { + timefilter.timefilter.getRefreshIntervalUpdate$().subscribe(() => { currentChange.refreshInterval = true; - state.set({ ...state.get(), refreshInterval: timefilter.getRefreshInterval() }); + state.set({ ...state.get(), refreshInterval: timefilter.timefilter.getRefreshInterval() }); }), filterManager.getUpdates$().subscribe(() => { currentChange.filters = true; diff --git a/src/plugins/data/public/query/state_sync/index.ts b/src/plugins/data/public/query/state_sync/index.ts index 58740cfab06d0..ffeda864f5172 100644 --- a/src/plugins/data/public/query/state_sync/index.ts +++ b/src/plugins/data/public/query/state_sync/index.ts @@ -7,5 +7,5 @@ */ export { connectToQueryState } from './connect_to_query_state'; -export { syncQueryStateWithUrl } from './sync_state_with_url'; -export type { QueryState, QueryStateChange } from './types'; +export { syncQueryStateWithUrl, syncGlobalQueryStateWithUrl } from './sync_state_with_url'; +export type { QueryStateChange, GlobalQueryStateFromUrl } from './types'; diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts index edeaa7c772575..feb9fc5238ab6 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts @@ -21,7 +21,7 @@ import { QueryService, QueryStart } from '../query_service'; import { StubBrowserStorage } from '@kbn/test-jest-helpers'; import { TimefilterContract } from '../timefilter'; import { syncQueryStateWithUrl } from './sync_state_with_url'; -import { QueryState } from './types'; +import { GlobalQueryStateFromUrl } from './types'; import { createNowProviderMock } from '../../now_provider/mocks'; const setupMock = coreMock.createSetup(); @@ -100,14 +100,14 @@ describe('sync_query_state_with_url', () => { test('when filters change, global filters synced to urlStorage', () => { const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); filterManager.setFilters([gF, aF]); - expect(kbnUrlStateStorage.get('_g')?.filters).toHaveLength(1); + expect(kbnUrlStateStorage.get('_g')?.filters).toHaveLength(1); stop(); }); test('when time range changes, time synced to urlStorage', () => { const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); timefilter.setTime({ from: 'now-30m', to: 'now' }); - expect(kbnUrlStateStorage.get('_g')?.time).toEqual({ + expect(kbnUrlStateStorage.get('_g')?.time).toEqual({ from: 'now-30m', to: 'now', }); @@ -117,7 +117,7 @@ describe('sync_query_state_with_url', () => { test('when refresh interval changes, refresh interval is synced to urlStorage', () => { const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); timefilter.setRefreshInterval({ pause: true, value: 100 }); - expect(kbnUrlStateStorage.get('_g')?.refreshInterval).toEqual({ + expect(kbnUrlStateStorage.get('_g')?.refreshInterval).toEqual({ pause: true, value: 100, }); diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.ts index fd52ca5ffc979..030cc1f91d4fe 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.ts @@ -13,17 +13,17 @@ import { } from '@kbn/kibana-utils-plugin/public'; import { QuerySetup, QueryStart } from '../query_service'; import { connectToQueryState } from './connect_to_query_state'; -import { QueryState } from './types'; import { FilterStateStore } from '../../../common'; +import { GlobalQueryStateFromUrl } from './types'; const GLOBAL_STATE_STORAGE_KEY = '_g'; /** - * Helper to setup syncing of global data with the URL + * Helper to sync global query state {@link GlobalQueryStateFromUrl} with the URL (`_g` query param that is preserved between apps) * @param QueryService: either setup or start * @param kbnUrlStateStorage to use for syncing */ -export const syncQueryStateWithUrl = ( +export const syncGlobalQueryStateWithUrl = ( query: Pick, kbnUrlStateStorage: IKbnUrlStateStorage ) => { @@ -31,14 +31,15 @@ export const syncQueryStateWithUrl = ( timefilter: { timefilter }, filterManager, } = query; - const defaultState: QueryState = { + const defaultState: GlobalQueryStateFromUrl = { time: timefilter.getTime(), refreshInterval: timefilter.getRefreshInterval(), filters: filterManager.getGlobalFilters(), }; // retrieve current state from `_g` url - const initialStateFromUrl = kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY); + const initialStateFromUrl = + kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY); // remember whether there was info in the URL const hasInheritedQueryFromUrl = Boolean( @@ -46,7 +47,7 @@ export const syncQueryStateWithUrl = ( ); // prepare initial state, whatever was in URL takes precedences over current state in services - const initialState: QueryState = { + const initialState: GlobalQueryStateFromUrl = { ...defaultState, ...initialStateFromUrl, }; @@ -61,7 +62,7 @@ export const syncQueryStateWithUrl = ( // if there weren't any initial state in url, // then put _g key into url if (!initialStateFromUrl) { - kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, initialState, { + kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, initialState, { replace: true, }); } @@ -92,3 +93,8 @@ export const syncQueryStateWithUrl = ( hasInheritedQueryFromUrl, }; }; + +/** + * @deprecated use {@link syncGlobalQueryStateWithUrl} instead + */ +export const syncQueryStateWithUrl = syncGlobalQueryStateWithUrl; diff --git a/src/plugins/data/public/query/state_sync/types.ts b/src/plugins/data/public/query/state_sync/types.ts index 8bfd47987ab90..653dd36577b8d 100644 --- a/src/plugins/data/public/query/state_sync/types.ts +++ b/src/plugins/data/public/query/state_sync/types.ts @@ -6,17 +6,9 @@ * Side Public License, v 1. */ -import { Filter, RefreshInterval, TimeRange, Query } from '../../../common'; - -/** - * All query state service state - */ -export interface QueryState { - time?: TimeRange; - refreshInterval?: RefreshInterval; - filters?: Filter[]; - query?: Query; -} +import type { Filter } from '@kbn/es-query'; +import type { QueryState } from '../query_state'; +import { RefreshInterval, TimeRange } from '../../../common/types'; type QueryStateChangePartial = { [P in keyof QueryState]?: boolean; @@ -26,3 +18,12 @@ export interface QueryStateChange extends QueryStateChangePartial { appFilters?: boolean; // specifies if app filters change globalFilters?: boolean; // specifies if global filters change } + +/** + * Part of {@link QueryState} serialized in the `_g` portion of Url + */ +export interface GlobalQueryStateFromUrl { + time?: TimeRange; + refreshInterval?: RefreshInterval; + filters?: Filter[]; +} diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts index d1b4d73571550..eb4731bd44e64 100644 --- a/src/plugins/discover/public/locator.ts +++ b/src/plugins/discover/public/locator.ts @@ -8,7 +8,12 @@ import type { SerializableRecord } from '@kbn/utility-types'; import type { Filter } from '@kbn/es-query'; -import type { TimeRange, Query, QueryState, RefreshInterval } from '@kbn/data-plugin/public'; +import type { + TimeRange, + Query, + GlobalQueryStateFromUrl, + RefreshInterval, +} from '@kbn/data-plugin/public'; import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import type { VIEW_MODE } from './components/view_mode_toggle'; @@ -126,7 +131,7 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition('_g', queryState, { useHash }, path); + path = setStateToKbnUrl('_g', queryState, { useHash }, path); path = setStateToKbnUrl('_a', appState, { useHash }, path); if (searchSessionId) { diff --git a/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.ts b/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.ts index fc41486fae84a..1285da1f3bf15 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.ts @@ -8,7 +8,7 @@ import { ApplicationStart } from '@kbn/core/public'; import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; -import { QueryState } from '@kbn/data-plugin/public'; +import { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { getUISettings } from '../../services'; import { GLOBAL_STATE_STORAGE_KEY, VISUALIZE_APP_NAME } from '../../../common/constants'; @@ -24,8 +24,14 @@ export const getVisualizeListItemLink = ( path: editApp ? editUrl : `#${editUrl}`, }); const useHash = getUISettings().get('state:storeInSessionStorage'); - const globalStateInUrl = kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY) || {}; + const globalStateInUrl = + kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY) || {}; - url = setStateToKbnUrl(GLOBAL_STATE_STORAGE_KEY, globalStateInUrl, { useHash }, url); + url = setStateToKbnUrl( + GLOBAL_STATE_STORAGE_KEY, + globalStateInUrl, + { useHash }, + url + ); return url; }; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts index 0f197f4a13ddd..0b3176154c5ff 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts @@ -10,7 +10,7 @@ import { SerializableRecord } from '@kbn/utility-types'; import { Filter } from '@kbn/es-query'; import { RefreshInterval, TimeRange } from '@kbn/data-plugin/common'; import { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/common'; -import { QueryState } from '@kbn/data-plugin/public'; +import { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public'; import { Dictionary, isRisonSerializationRequired } from '../../common/util/url_state'; import { SearchQueryLanguage } from '../types/combined_query'; @@ -124,7 +124,7 @@ export class IndexDataVisualizerLocatorDefinition sortField?: string; showDistributions?: number; } = {}; - const queryState: QueryState = {}; + const queryState: GlobalQueryStateFromUrl = {}; if (query) { appState.searchQuery = query.searchQuery; diff --git a/x-pack/plugins/maps/public/locators.ts b/x-pack/plugins/maps/public/locators.ts index 6c5d5a730edf7..7cfdb7a0d3fb1 100644 --- a/x-pack/plugins/maps/public/locators.ts +++ b/x-pack/plugins/maps/public/locators.ts @@ -10,7 +10,12 @@ import rison from 'rison-node'; import type { SerializableRecord } from '@kbn/utility-types'; import { type Filter, isFilterPinned } from '@kbn/es-query'; -import type { TimeRange, Query, QueryState, RefreshInterval } from '@kbn/data-plugin/public'; +import type { + TimeRange, + Query, + GlobalQueryStateFromUrl, + RefreshInterval, +} from '@kbn/data-plugin/public'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; import type { LayerDescriptor } from '../common/descriptor_types'; @@ -78,7 +83,7 @@ export class MapsAppLocatorDefinition implements LocatorDefinition !isFilterPinned(f)); @@ -87,7 +92,7 @@ export class MapsAppLocatorDefinition implements LocatorDefinition('_g', queryState, { useHash }, path); + path = setStateToKbnUrl('_g', queryState, { useHash }, path); path = setStateToKbnUrl('_a', appState, { useHash }, path); if (initialLayers && initialLayers.length) { diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_refresh_config.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_refresh_config.ts index 8e816c6930fdb..79d3603055874 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_refresh_config.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_refresh_config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { QueryState } from '@kbn/data-plugin/public'; +import { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public'; import { UI_SETTINGS } from '@kbn/data-plugin/public'; import { getUiSettings } from '../../../kibana_services'; import { SerializedMapState } from './types'; @@ -15,7 +15,7 @@ export function getInitialRefreshConfig({ globalState = {}, }: { serializedMapState?: SerializedMapState; - globalState: QueryState; + globalState: GlobalQueryStateFromUrl; }) { const uiSettings = getUiSettings(); diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_time_filters.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_time_filters.ts index da293d5c52d29..fc3754256d659 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_time_filters.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_time_filters.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { QueryState } from '@kbn/data-plugin/public'; +import { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public'; import { getUiSettings } from '../../../kibana_services'; import { SerializedMapState } from './types'; @@ -14,7 +14,7 @@ export function getInitialTimeFilters({ globalState, }: { serializedMapState?: SerializedMapState; - globalState: QueryState; + globalState: GlobalQueryStateFromUrl; }) { if (serializedMapState?.timeFilters) { return serializedMapState.timeFilters; From 473141f58b23c799c83be976afb331e09ec2d022 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Fri, 20 May 2022 12:17:58 +0200 Subject: [PATCH 028/120] [Cases] Show deprecated icon in connectors with isDeprecated true (#132237) Co-authored-by: Christos Nasikas --- .../server/lib/is_conector_deprecated.test.ts | 25 +++++++ .../server/lib/is_conector_deprecated.ts | 40 +++++++++-- .../cases/common/api/connectors/index.ts | 10 ++- .../cases/public/common/mock/connectors.ts | 5 ++ .../components/all_cases/columns.test.tsx | 1 + .../connectors_dropdown.test.tsx | 23 ++++++ .../public/components/connectors/mock.ts | 2 + .../servicenow_itsm_case_fields.test.tsx | 19 ++--- .../servicenow_itsm_case_fields.tsx | 3 +- .../servicenow_sir_case_fields.test.tsx | 19 ++--- .../servicenow/servicenow_sir_case_fields.tsx | 3 +- .../servicenow/use_get_choices.test.tsx | 1 + .../connectors/servicenow/validator.test.ts | 48 ------------- .../connectors/servicenow/validator.ts | 34 --------- .../connectors/swimlane/validator.test.ts | 21 ++++++ .../connectors/swimlane/validator.ts | 19 +++-- .../cases/public/components/utils.test.ts | 70 ++++++++----------- .../plugins/cases/public/components/utils.ts | 45 +++++------- .../cases/server/client/cases/utils.test.ts | 1 + .../alerting_api_integration/common/config.ts | 11 +++ .../group2/tests/actions/get_all.ts | 24 +++++++ .../tests/telemetry/actions_telemetry.ts | 2 +- .../spaces_only/tests/actions/get.ts | 12 ++++ .../spaces_only/tests/actions/get_all.ts | 24 +++++++ .../cases_api_integration/common/config.ts | 1 + 25 files changed, 285 insertions(+), 178 deletions(-) delete mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/validator.test.ts delete mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/validator.ts diff --git a/x-pack/plugins/actions/server/lib/is_conector_deprecated.test.ts b/x-pack/plugins/actions/server/lib/is_conector_deprecated.test.ts index f5ace7e055254..c3697cea6a34e 100644 --- a/x-pack/plugins/actions/server/lib/is_conector_deprecated.test.ts +++ b/x-pack/plugins/actions/server/lib/is_conector_deprecated.test.ts @@ -17,6 +17,11 @@ describe('isConnectorDeprecated', () => { isPreconfigured: false as const, }; + it('returns false if the config is not defined', () => { + // @ts-expect-error + expect(isConnectorDeprecated({})).toBe(false); + }); + it('returns false if the connector is not ITSM or SecOps', () => { expect(isConnectorDeprecated(connector)).toBe(false); }); @@ -48,4 +53,24 @@ describe('isConnectorDeprecated', () => { }) ).toBe(true); }); + + it('returns true if the connector is .servicenow and the usesTableApi is omitted', () => { + expect( + isConnectorDeprecated({ + ...connector, + actionTypeId: '.servicenow', + config: { apiUrl: 'http://example.com' }, + }) + ).toBe(true); + }); + + it('returns true if the connector is .servicenow-sir and the usesTableApi is omitted', () => { + expect( + isConnectorDeprecated({ + ...connector, + actionTypeId: '.servicenow-sir', + config: { apiUrl: 'http://example.com' }, + }) + ).toBe(true); + }); }); diff --git a/x-pack/plugins/actions/server/lib/is_conector_deprecated.ts b/x-pack/plugins/actions/server/lib/is_conector_deprecated.ts index 210631cb532f6..ed46f5e685459 100644 --- a/x-pack/plugins/actions/server/lib/is_conector_deprecated.ts +++ b/x-pack/plugins/actions/server/lib/is_conector_deprecated.ts @@ -5,11 +5,14 @@ * 2.0. */ +import { isPlainObject } from 'lodash'; import { PreConfiguredAction, RawAction } from '../types'; export type ConnectorWithOptionalDeprecation = Omit & Pick, 'isDeprecated'>; +const isObject = (obj: unknown): obj is Record => isPlainObject(obj); + export const isConnectorDeprecated = ( connector: RawAction | ConnectorWithOptionalDeprecation ): boolean => { @@ -18,11 +21,40 @@ export const isConnectorDeprecated = ( * Connectors after the Elastic ServiceNow application use the * Import Set API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_ImportSetAPI) * A ServiceNow connector is considered deprecated if it uses the Table API. - * - * All other connectors do not have the usesTableApi config property - * so the function will always return false for them. */ - return !!connector.config?.usesTableApi; + + /** + * We cannot deduct if the connector is + * deprecated without config. In this case + * we always return false. + */ + if (!isObject(connector.config)) { + return false; + } + + /** + * If the usesTableApi is not defined it means that the connector is created + * before the introduction of the usesTableApi property. In that case, the connector is assumed + * to be deprecated because all connectors prior 7.16 where using the Table API. + * Migrations x-pack/plugins/actions/server/saved_objects/actions_migrations.ts set + * the usesTableApi property to true to all connectors prior 7.16. Pre configured connectors + * cannot be migrated. This check ensures that pre configured connectors without the + * usesTableApi property explicitly in the kibana.yml file are considered deprecated. + * According to the schema defined here x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts + * if the property is not defined it will be set to true at the execution of the connector. + */ + if (!Object.hasOwn(connector.config, 'usesTableApi')) { + return true; + } + + /** + * Connector created prior to 7.16 will be migrated to have the usesTableApi property set to true. + * Connectors created after 7.16 should have the usesTableApi property set to true or false. + * If the usesTableApi is omitted on an API call it will be defaulted to true. Check the schema + * here x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts. + * The !! is to make TS happy. + */ + return !!connector.config.usesTableApi; } return false; diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index bb1892525f8e0..df9a7b0e24fd7 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -7,7 +7,15 @@ import * as rt from 'io-ts'; -import { ActionResult, ActionType } from '@kbn/actions-plugin/common'; +import type { ActionType } from '@kbn/actions-plugin/common'; +/** + * ActionResult type from the common folder is outdated. + * The type from server is not exported properly so we + * disable the linting for the moment + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { ActionResult } from '@kbn/actions-plugin/server/types'; import { JiraFieldsRT } from './jira'; import { ResilientFieldsRT } from './resilient'; import { ServiceNowITSMFieldsRT } from './servicenow_itsm'; diff --git a/x-pack/plugins/cases/public/common/mock/connectors.ts b/x-pack/plugins/cases/public/common/mock/connectors.ts index 01afbbee118a8..d186b68053e7f 100644 --- a/x-pack/plugins/cases/public/common/mock/connectors.ts +++ b/x-pack/plugins/cases/public/common/mock/connectors.ts @@ -16,6 +16,7 @@ export const connectorsMock: ActionConnector[] = [ apiUrl: 'https://instance1.service-now.com', }, isPreconfigured: false, + isDeprecated: false, }, { id: 'resilient-2', @@ -26,6 +27,7 @@ export const connectorsMock: ActionConnector[] = [ orgId: '201', }, isPreconfigured: false, + isDeprecated: false, }, { id: 'jira-1', @@ -35,6 +37,7 @@ export const connectorsMock: ActionConnector[] = [ apiUrl: 'https://instance.atlassian.ne', }, isPreconfigured: false, + isDeprecated: false, }, { id: 'servicenow-sir', @@ -44,6 +47,7 @@ export const connectorsMock: ActionConnector[] = [ apiUrl: 'https://instance1.service-now.com', }, isPreconfigured: false, + isDeprecated: false, }, { id: 'servicenow-uses-table-api', @@ -54,6 +58,7 @@ export const connectorsMock: ActionConnector[] = [ usesTableApi: true, }, isPreconfigured: false, + isDeprecated: true, }, ]; diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.test.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.test.tsx index 764a51443b0e3..b09eecbb31f4f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.test.tsx @@ -77,6 +77,7 @@ describe('ExternalServiceColumn ', () => { name: 'None', config: {}, isPreconfigured: false, + isDeprecated: false, }, ]} /> diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx index 63fc2e2695a3a..e8093325c1e09 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx @@ -249,6 +249,7 @@ describe('ConnectorsDropdown', () => { name: 'None', config: {}, isPreconfigured: false, + isDeprecated: false, }, ]} />, @@ -269,4 +270,26 @@ describe('ConnectorsDropdown', () => { ); expect(tooltips[0]).toBeInTheDocument(); }); + + test('it shows the deprecated tooltip when the connector is deprecated by configuration', () => { + const connector = connectors[0]; + render( + , + { wrapper: ({ children }) => {children} } + ); + + const tooltips = screen.getAllByText( + 'This connector is deprecated. Update it, or create a new one.' + ); + expect(tooltips[0]).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/cases/public/components/connectors/mock.ts b/x-pack/plugins/cases/public/components/connectors/mock.ts index 2eb512af0f2ef..ba29319a8926c 100644 --- a/x-pack/plugins/cases/public/components/connectors/mock.ts +++ b/x-pack/plugins/cases/public/components/connectors/mock.ts @@ -13,6 +13,7 @@ export const connector = { actionTypeId: '.jira', config: {}, isPreconfigured: false, + isDeprecated: false, }; export const swimlaneConnector = { @@ -29,6 +30,7 @@ export const swimlaneConnector = { }, }, isPreconfigured: false, + isDeprecated: false, }; export const issues = [ diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx index cfc16f1fb6e8b..e2f4a683772c7 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx @@ -135,18 +135,18 @@ describe('ServiceNowITSM Fields', () => { ); }); - it('shows the deprecated callout when the connector uses the table API', async () => { - const tableApiConnector = { ...connector, config: { usesTableApi: true } }; + it('shows the deprecated callout if the connector is deprecated', async () => { + const tableApiConnector = { ...connector, isDeprecated: true }; render(); expect(screen.getByTestId('deprecated-connector-warning-callout')).toBeInTheDocument(); }); - it('does not show the deprecated callout when the connector does not uses the table API', async () => { + it('does not show the deprecated callout when the connector is not deprecated', async () => { render(); expect(screen.queryByTestId('deprecated-connector-warning-callout')).not.toBeInTheDocument(); }); - it('does not show the deprecated callout when the connector is preconfigured', async () => { + it('does not show the deprecated callout when the connector is preconfigured and not deprecated', async () => { render( { expect(screen.queryByTestId('deprecated-connector-warning-callout')).not.toBeInTheDocument(); }); - it('does not show the deprecated callout when the config of the connector is undefined', async () => { + it('shows the deprecated callout when the connector is preconfigured and deprecated', async () => { render( - // @ts-expect-error - + ); - expect(screen.queryByTestId('deprecated-connector-warning-callout')).not.toBeInTheDocument(); + expect(screen.queryByTestId('deprecated-connector-warning-callout')).toBeInTheDocument(); }); it('should hide subcategory if selecting a category without subcategories', async () => { diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx index f366cc95ff77a..2dae544ec274c 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx @@ -16,7 +16,6 @@ import { ConnectorCard } from '../card'; import { useGetChoices } from './use_get_choices'; import { Fields, Choice } from './types'; import { choicesToEuiOptions } from './helpers'; -import { connectorValidator } from './validator'; import { DeprecatedCallout } from '../deprecated_callout'; const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory']; @@ -44,7 +43,7 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent< } = fields ?? {}; const { http, notifications } = useKibana().services; const [choices, setChoices] = useState(defaultFields); - const showConnectorWarning = useMemo(() => connectorValidator(connector) != null, [connector]); + const showConnectorWarning = connector.isDeprecated; const categoryOptions = useMemo( () => choicesToEuiOptions(choices.category), diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx index a2c61ac78be0b..1b06e0cfdce81 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx @@ -169,18 +169,18 @@ describe('ServiceNowSIR Fields', () => { ]); }); - test('it shows the deprecated callout when the connector uses the table API', async () => { - const tableApiConnector = { ...connector, config: { usesTableApi: true } }; + test('shows the deprecated callout if the connector is deprecated', async () => { + const tableApiConnector = { ...connector, isDeprecated: true }; render(); expect(screen.getByTestId('deprecated-connector-warning-callout')).toBeInTheDocument(); }); - test('it does not show the deprecated callout when the connector does not uses the table API', async () => { + test('does not show the deprecated callout when the connector is not deprecated', async () => { render(); expect(screen.queryByTestId('deprecated-connector-warning-callout')).not.toBeInTheDocument(); }); - it('does not show the deprecated callout when the connector is preconfigured', async () => { + it('does not show the deprecated callout when the connector is preconfigured and not deprecated', async () => { render( { expect(screen.queryByTestId('deprecated-connector-warning-callout')).not.toBeInTheDocument(); }); - it('does not show the deprecated callout when the config of the connector is undefined', async () => { + it('shows the deprecated callout when the connector is preconfigured and deprecated', async () => { render( - // @ts-expect-error - + ); - expect(screen.queryByTestId('deprecated-connector-warning-callout')).not.toBeInTheDocument(); + expect(screen.queryByTestId('deprecated-connector-warning-callout')).toBeInTheDocument(); }); test('it should hide subcategory if selecting a category without subcategories', async () => { diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx index 99bbe8aabaeda..78f17a1d4215a 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx @@ -17,7 +17,6 @@ import { Choice, Fields } from './types'; import { choicesToEuiOptions } from './helpers'; import * as i18n from './translations'; -import { connectorValidator } from './validator'; import { DeprecatedCallout } from '../deprecated_callout'; const useGetChoicesFields = ['category', 'subcategory', 'priority']; @@ -43,7 +42,7 @@ const ServiceNowSIRFieldsComponent: React.FunctionComponent< const { http, notifications } = useKibana().services; const [choices, setChoices] = useState(defaultFields); - const showConnectorWarning = useMemo(() => connectorValidator(connector) != null, [connector]); + const showConnectorWarning = connector.isDeprecated; const onChangeCb = useCallback( ( diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx index 950b17d6f784f..9a4e19d126bba 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx @@ -29,6 +29,7 @@ const connector = { actionTypeId: '.servicenow', name: 'ServiceNow', isPreconfigured: false, + isDeprecated: false, config: { apiUrl: 'https://dev94428.service-now.com/', }, diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/validator.test.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/validator.test.ts deleted file mode 100644 index ab21a6b5c779c..0000000000000 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/validator.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { connector } from '../mock'; -import { connectorValidator } from './validator'; - -describe('ServiceNow validator', () => { - describe('connectorValidator', () => { - test('it returns an error message if the connector uses the table API', () => { - const invalidConnector = { - ...connector, - config: { - ...connector.config, - usesTableApi: true, - }, - }; - - expect(connectorValidator(invalidConnector)).toEqual({ message: 'Deprecated connector' }); - }); - - test('it does not return an error message if the connector does not uses the table API', () => { - const invalidConnector = { - ...connector, - config: { - ...connector.config, - usesTableApi: false, - }, - }; - - expect(connectorValidator(invalidConnector)).toBeFalsy(); - }); - - test('it does not return an error message if the config of the connector is undefined', () => { - const { config, ...invalidConnector } = connector; - - // @ts-expect-error - expect(connectorValidator(invalidConnector)).toBeFalsy(); - }); - - test('it does not return an error message if the config of the connector is preconfigured', () => { - expect(connectorValidator({ ...connector, isPreconfigured: true })).toBeFalsy(); - }); - }); -}); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/validator.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/validator.ts deleted file mode 100644 index fed2900715527..0000000000000 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/validator.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ValidationConfig } from '../../../common/shared_imports'; -import { CaseActionConnector } from '../../types'; - -/** - * The user can not create cases with connectors that use the table API - */ - -export const connectorValidator = ( - connector: CaseActionConnector -): ReturnType => { - /** - * It is not possible to know if a preconfigured connector - * is deprecated or not as the config property of a - * preconfigured connector is not returned by the - * actions framework - */ - - if (connector.isPreconfigured || connector.config == null) { - return; - } - - if (connector.config?.usesTableApi) { - return { - message: 'Deprecated connector', - }; - } -}; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts index c8cb142232972..a179091282991 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts @@ -56,5 +56,26 @@ describe('Swimlane validator', () => { expect(connectorValidator(invalidConnector)).toBe(undefined); } ); + + test('it does not return an error message if the config is undefined', () => { + const invalidConnector = { + ...connector, + config: undefined, + }; + + expect(connectorValidator(invalidConnector)).toBe(undefined); + }); + + test('it returns an error message if the mappings are undefined', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + mappings: undefined, + }, + }; + + expect(connectorValidator(invalidConnector)).toEqual({ message: 'Invalid connector' }); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts index 90d9946d4adb8..d3c94d0150bbe 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts @@ -28,10 +28,21 @@ export const isAnyRequiredFieldNotSet = (mapping: Record | unde export const connectorValidator = ( connector: CaseActionConnector ): ReturnType => { - const { - config: { mappings, connectorType }, - } = connector; - if (connectorType === SwimlaneConnectorType.Alerts || isAnyRequiredFieldNotSet(mappings)) { + const config = connector.config as + | { + mappings: Record | undefined; + connectorType: string; + } + | undefined; + + if (config == null) { + return; + } + + if ( + config.connectorType === SwimlaneConnectorType.Alerts || + isAnyRequiredFieldNotSet(config.mappings) + ) { return { message: 'Invalid connector', }; diff --git a/x-pack/plugins/cases/public/components/utils.test.ts b/x-pack/plugins/cases/public/components/utils.test.ts index 278bb28b86627..99ec0213ff4ad 100644 --- a/x-pack/plugins/cases/public/components/utils.test.ts +++ b/x-pack/plugins/cases/public/components/utils.test.ts @@ -7,9 +7,19 @@ import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock'; import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks'; -import { getConnectorIcon, isDeprecatedConnector } from './utils'; +import { connectorDeprecationValidator, getConnectorIcon, isDeprecatedConnector } from './utils'; describe('Utils', () => { + const connector = { + id: 'test', + actionTypeId: '.webhook', + name: 'Test', + config: { usesTableApi: false }, + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + }; + describe('getConnectorIcon', () => { const { createMockActionTypeModel } = actionTypeRegistryMock; const mockTriggersActionsUiService = triggersActionsUiMock.createStart(); @@ -38,60 +48,40 @@ describe('Utils', () => { }); }); - describe('isDeprecatedConnector', () => { - const connector = { - id: 'test', - actionTypeId: '.webhook', - name: 'Test', - config: { usesTableApi: false }, - secrets: {}, - isPreconfigured: false, - }; - - it('returns false if the connector is not defined', () => { - expect(isDeprecatedConnector()).toBe(false); + describe('connectorDeprecationValidator', () => { + it('returns undefined if the connector is not deprecated', () => { + expect(connectorDeprecationValidator(connector)).toBe(undefined); }); - it('returns false if the connector is not ITSM or SecOps', () => { - expect(isDeprecatedConnector(connector)).toBe(false); + it('returns a deprecation message if the connector is deprecated', () => { + expect(connectorDeprecationValidator({ ...connector, isDeprecated: true })).toEqual({ + message: 'Deprecated connector', + }); }); + }); - it('returns false if the connector is .servicenow and the usesTableApi=false', () => { - expect(isDeprecatedConnector({ ...connector, actionTypeId: '.servicenow' })).toBe(false); + describe('isDeprecatedConnector', () => { + it('returns false if the connector is not defined', () => { + expect(isDeprecatedConnector()).toBe(false); }); - it('returns false if the connector is .servicenow-sir and the usesTableApi=false', () => { - expect(isDeprecatedConnector({ ...connector, actionTypeId: '.servicenow-sir' })).toBe(false); + it('returns false if the connector is marked as deprecated', () => { + expect(isDeprecatedConnector({ ...connector, isDeprecated: false })).toBe(false); }); - it('returns true if the connector is .servicenow and the usesTableApi=true', () => { - expect( - isDeprecatedConnector({ - ...connector, - actionTypeId: '.servicenow', - config: { usesTableApi: true }, - }) - ).toBe(true); + it('returns true if the connector is marked as deprecated', () => { + expect(isDeprecatedConnector({ ...connector, isDeprecated: true })).toBe(true); }); - it('returns true if the connector is .servicenow-sir and the usesTableApi=true', () => { + it('returns true if the connector is marked as deprecated (preconfigured connector)', () => { expect( - isDeprecatedConnector({ - ...connector, - actionTypeId: '.servicenow-sir', - config: { usesTableApi: true }, - }) + isDeprecatedConnector({ ...connector, isDeprecated: true, isPreconfigured: true }) ).toBe(true); }); - it('returns false if the connector preconfigured', () => { - expect(isDeprecatedConnector({ ...connector, isPreconfigured: true })).toBe(false); - }); - - it('returns false if the config is undefined', () => { + it('returns false if the connector is not marked as deprecated (preconfigured connector)', () => { expect( - // @ts-expect-error - isDeprecatedConnector({ ...connector, config: undefined }) + isDeprecatedConnector({ ...connector, isDeprecated: false, isPreconfigured: true }) ).toBe(false); }); }); diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts index 34ebffb4eacb4..403f55574f9a6 100644 --- a/x-pack/plugins/cases/public/components/utils.ts +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -10,7 +10,6 @@ import { ConnectorTypes } from '../../common/api'; import { FieldConfig, ValidationConfig } from '../common/shared_imports'; import { CasesPluginStart } from '../types'; import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator'; -import { connectorValidator as servicenowConnectorValidator } from './connectors/servicenow/validator'; import { CaseActionConnector } from './types'; export const getConnectorById = ( @@ -23,8 +22,16 @@ const validators: Record< (connector: CaseActionConnector) => ReturnType > = { [ConnectorTypes.swimlane]: swimlaneConnectorValidator, - [ConnectorTypes.serviceNowITSM]: servicenowConnectorValidator, - [ConnectorTypes.serviceNowSIR]: servicenowConnectorValidator, +}; + +export const connectorDeprecationValidator = ( + connector: CaseActionConnector +): ReturnType => { + if (connector.isDeprecated) { + return { + message: 'Deprecated connector', + }; + } }; export const getConnectorsFormValidators = ({ @@ -36,6 +43,14 @@ export const getConnectorsFormValidators = ({ }): FieldConfig => ({ ...config, validations: [ + { + validator: ({ value: connectorId }) => { + const connector = getConnectorById(connectorId as string, connectors); + if (connector != null) { + return connectorDeprecationValidator(connector); + } + }, + }, { validator: ({ value: connectorId }) => { const connector = getConnectorById(connectorId as string, connectors); @@ -72,28 +87,6 @@ export const getConnectorIcon = ( return emptyResponse; }; -// TODO: Remove when the applications are certified export const isDeprecatedConnector = (connector?: CaseActionConnector): boolean => { - /** - * It is not possible to know if a preconfigured connector - * is deprecated or not as the config property of a - * preconfigured connector is not returned by the - * actions framework - */ - if (connector == null || connector.config == null || connector.isPreconfigured) { - return false; - } - - if (connector.actionTypeId === '.servicenow' || connector.actionTypeId === '.servicenow-sir') { - /** - * Connector's prior to the Elastic ServiceNow application - * use the Table API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_TableAPI) - * Connectors after the Elastic ServiceNow application use the - * Import Set API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_ImportSetAPI) - * A ServiceNow connector is considered deprecated if it uses the Table API. - */ - return !!connector.config.usesTableApi; - } - - return false; + return connector?.isDeprecated ?? false; }; diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index 4832ffe5b2eaf..baf32fd30d74b 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -521,6 +521,7 @@ describe('utils', () => { apiUrl: 'https://elastic.jira.com', }, isPreconfigured: false, + isDeprecated: false, }; it('creates an external incident', async () => { diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index d1bf39b575ab5..0c5f95189ae90 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -210,6 +210,17 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) password: 'somepassword', }, }, + 'my-deprecated-servicenow-default': { + actionTypeId: '.servicenow', + name: 'ServiceNow#xyz', + config: { + apiUrl: 'https://ven04334.service-now.com', + }, + secrets: { + username: 'elastic_integration', + password: 'somepassword', + }, + }, 'custom-system-abc-connector': { actionTypeId: 'system-abc-action-type', name: 'SystemABC', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts index 103ae5abd3071..69f618c804eb1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts @@ -95,6 +95,14 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'ServiceNow#xyz', referenced_by_count: 0, }, + { + connector_type_id: '.servicenow', + id: 'my-deprecated-servicenow-default', + is_preconfigured: true, + is_deprecated: true, + name: 'ServiceNow#xyz', + referenced_by_count: 0, + }, { id: 'my-slack1', is_preconfigured: true, @@ -222,6 +230,14 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'ServiceNow#xyz', referenced_by_count: 0, }, + { + connector_type_id: '.servicenow', + id: 'my-deprecated-servicenow-default', + is_preconfigured: true, + is_deprecated: true, + name: 'ServiceNow#xyz', + referenced_by_count: 0, + }, { id: 'my-slack1', is_preconfigured: true, @@ -313,6 +329,14 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'ServiceNow#xyz', referenced_by_count: 0, }, + { + connector_type_id: '.servicenow', + id: 'my-deprecated-servicenow-default', + is_preconfigured: true, + is_deprecated: true, + name: 'ServiceNow#xyz', + referenced_by_count: 0, + }, { id: 'my-slack1', is_preconfigured: true, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/actions_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/actions_telemetry.ts index b187b9e9f9759..b1e77b98b792d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/actions_telemetry.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/actions_telemetry.ts @@ -188,7 +188,7 @@ export default function createActionsTelemetryTests({ getService }: FtrProviderC const telemetry = JSON.parse(taskState!); // total number of connectors - expect(telemetry.count_total).to.equal(18); + expect(telemetry.count_total).to.equal(19); // total number of active connectors (used by a rule) expect(telemetry.count_active_total).to.equal(7); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts index d5d5109b6e738..6d923452faac5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts @@ -98,6 +98,18 @@ export default function getActionTests({ getService }: FtrProviderContext) { connector_type_id: '.servicenow', name: 'ServiceNow#xyz', }); + + await supertest + .get( + `${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/my-deprecated-servicenow-default` + ) + .expect(200, { + id: 'my-deprecated-servicenow-default', + is_preconfigured: true, + is_deprecated: true, + connector_type_id: '.servicenow', + name: 'ServiceNow#xyz', + }); }); describe('legacy', () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index 54a0e6e10a198..0632f48ed6e8d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -83,6 +83,14 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'ServiceNow#xyz', referenced_by_count: 0, }, + { + connector_type_id: '.servicenow', + id: 'my-deprecated-servicenow-default', + is_preconfigured: true, + is_deprecated: true, + name: 'ServiceNow#xyz', + referenced_by_count: 0, + }, { id: 'my-slack1', is_preconfigured: true, @@ -162,6 +170,14 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'ServiceNow#xyz', referenced_by_count: 0, }, + { + connector_type_id: '.servicenow', + id: 'my-deprecated-servicenow-default', + is_preconfigured: true, + is_deprecated: true, + name: 'ServiceNow#xyz', + referenced_by_count: 0, + }, { id: 'my-slack1', is_preconfigured: true, @@ -254,6 +270,14 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'ServiceNow#xyz', referencedByCount: 0, }, + { + actionTypeId: '.servicenow', + id: 'my-deprecated-servicenow-default', + isPreconfigured: true, + isDeprecated: true, + name: 'ServiceNow#xyz', + referencedByCount: 0, + }, { id: 'my-slack1', isPreconfigured: true, diff --git a/x-pack/test/cases_api_integration/common/config.ts b/x-pack/test/cases_api_integration/common/config.ts index 89dd19ae74897..a20dd300a4e6e 100644 --- a/x-pack/test/cases_api_integration/common/config.ts +++ b/x-pack/test/cases_api_integration/common/config.ts @@ -144,6 +144,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) actionTypeId: '.servicenow', config: { apiUrl: 'https://example.com', + usesTableApi: false, }, secrets: { username: 'elastic', From aa4c389ed2839d18ab008dd00a7a89c8f4080d74 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Fri, 20 May 2022 12:24:38 +0200 Subject: [PATCH 029/120] [Fleet] Changes to agent upgrade modal to allow for rolling upgrades (#132421) * [Fleet] Changes to agent upgrade modal to allow for rolling upgrades * Update the onSubmit logic and handle case with single agent * Fix check * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Add option to upgrade immediately; minor fixes * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * Add callout in modal for 400 errors * Linter fixes * Fix i18n error * Address code review comments Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../fleet/common/types/rest_spec/agent.ts | 1 + .../components/actions_menu.tsx | 1 - .../components/bulk_actions.tsx | 7 +- .../sections/agents/agent_list_page/index.tsx | 1 - .../agent_upgrade_modal/constants.tsx | 32 +++ .../components/agent_upgrade_modal/index.tsx | 187 ++++++++++++++---- .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 9 files changed, 184 insertions(+), 51 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/constants.tsx diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index 7a8b7b918c1e3..886730d38f831 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -89,6 +89,7 @@ export interface PostBulkAgentUpgradeRequest { agents: string[] | string; source_uri?: string; version: string; + rollout_duration_seconds?: number; }; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx index 44e87d7fb4e63..239afe6c7e330 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx @@ -70,7 +70,6 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ { setIsUpgradeModalOpen(false); refreshAgent(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx index a2515b51814ee..e27c647e25f70 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx @@ -24,7 +24,6 @@ import { AgentUnenrollAgentModal, AgentUpgradeAgentModal, } from '../../components'; -import { useKibanaVersion } from '../../../../hooks'; import type { SelectionMode } from './types'; @@ -48,11 +47,10 @@ export const AgentBulkActions: React.FunctionComponent = ({ selectedAgents, refreshAgents, }) => { - const kibanaVersion = useKibanaVersion(); // Bulk actions menu states const [isMenuOpen, setIsMenuOpen] = useState(false); const closeMenu = () => setIsMenuOpen(false); - const openMenu = () => setIsMenuOpen(true); + const onClickMenu = () => setIsMenuOpen(!isMenuOpen); // Actions states const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(false); @@ -150,7 +148,6 @@ export const AgentBulkActions: React.FunctionComponent = ({ {isUpgradeModalOpen && ( { @@ -172,7 +169,7 @@ export const AgentBulkActions: React.FunctionComponent = ({ fill iconType="arrowDown" iconSide="right" - onClick={openMenu} + onClick={onClickMenu} data-test-subj="agentBulkActionsButton" > = () => { fetchData(); refreshUpgrades(); }} - version={kibanaVersion} /> )} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/constants.tsx new file mode 100644 index 0000000000000..b5d8cd8f4d72d --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/constants.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// Available versions for the upgrade of the Elastic Agent +// These versions are only intended to be used as a fallback +// in the event that the updated versions cannot be retrieved from the endpoint + +export const FALLBACK_VERSIONS = [ + '8.2.0', + '8.1.3', + '8.1.2', + '8.1.1', + '8.1.0', + '8.0.1', + '8.0.0', + '7.9.3', + '7.9.2', + '7.9.1', + '7.9.0', + '7.8.1', + '7.8.0', + '7.17.3', + '7.17.2', + '7.17.1', + '7.17.0', +]; + +export const MAINTAINANCE_VALUES = [1, 2, 4, 8, 12, 24, 48]; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx index 72ca7a5b80fd7..2122abb5e2785 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx @@ -7,34 +7,89 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiConfirmModal, EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiConfirmModal, + EuiComboBox, + EuiFormRow, + EuiSpacer, + EuiToolTip, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import type { EuiComboBoxOptionOption } from '@elastic/eui'; + import type { Agent } from '../../../../types'; import { sendPostAgentUpgrade, sendPostBulkAgentUpgrade, useStartServices, + useKibanaVersion, } from '../../../../hooks'; +import { FALLBACK_VERSIONS, MAINTAINANCE_VALUES } from './constants'; + interface Props { onClose: () => void; agents: Agent[] | string; agentCount: number; - version: string; } +const getVersion = (version: Array>) => version[0].value as string; + export const AgentUpgradeAgentModal: React.FunctionComponent = ({ onClose, agents, agentCount, - version, }) => { const { notifications } = useStartServices(); + const kibanaVersion = useKibanaVersion(); const [isSubmitting, setIsSubmitting] = useState(false); + const [errors, setErrors] = useState(); + const isSingleAgent = Array.isArray(agents) && agents.length === 1; + const isSmallBatch = Array.isArray(agents) && agents.length > 1 && agents.length <= 10; const isAllAgents = agents === ''; + + const fallbackVersions = [kibanaVersion].concat(FALLBACK_VERSIONS); + const fallbackOptions: Array> = fallbackVersions.map( + (option) => ({ + label: option, + value: option, + }) + ); + const maintainanceWindows = isSmallBatch ? [0].concat(MAINTAINANCE_VALUES) : MAINTAINANCE_VALUES; + const maintainanceOptions: Array> = maintainanceWindows.map( + (option) => ({ + label: + option === 0 + ? i18n.translate('xpack.fleet.upgradeAgents.noMaintainanceWindowOption', { + defaultMessage: 'Immediately', + }) + : i18n.translate('xpack.fleet.upgradeAgents.hourLabel', { + defaultMessage: '{option} {count, plural, one {hour} other {hours}}', + values: { option, count: option === 1 }, + }), + value: option === 0 ? 0 : option * 3600, + }) + ); + const [selectedVersion, setSelectedVersion] = useState([fallbackOptions[0]]); + const [selectedMantainanceWindow, setSelectedMantainanceWindow] = useState([ + maintainanceOptions[0], + ]); + async function onSubmit() { + const version = getVersion(selectedVersion); + const rolloutOptions = + selectedMantainanceWindow.length > 0 && (selectedMantainanceWindow[0]?.value as number) > 0 + ? { + rollout_duration_seconds: selectedMantainanceWindow[0].value, + } + : {}; + try { setIsSubmitting(true); const { data, error } = isSingleAgent @@ -42,10 +97,14 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ version, }) : await sendPostBulkAgentUpgrade({ - agents: Array.isArray(agents) ? agents.map((agent) => agent.id) : agents, version, + agents: Array.isArray(agents) ? agents.map((agent) => agent.id) : agents, + ...rolloutOptions, }); if (error) { + if (error?.statusCode === 400) { + setErrors(error?.message); + } throw error; } @@ -114,39 +173,20 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ - - {isSingleAgent ? ( - - ) : ( - - )} - - - - } - tooltipContent={ - - } + <> + {isSingleAgent ? ( + + ) : ( + - - + )} + } onCancel={onClose} onConfirm={onSubmit} @@ -179,17 +219,88 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ defaultMessage="This action will upgrade the agent running on '{hostName}' to version {version}. This action can not be undone. Are you sure you wish to continue?" values={{ hostName: ((agents[0] as Agent).local_metadata.host as any).hostname, - version, + version: getVersion(selectedVersion), }} /> ) : ( )}

+ + + >) => { + setSelectedVersion(selected); + }} + /> + + + {!isSingleAgent ? ( + + + {i18n.translate('xpack.fleet.upgradeAgents.maintainanceAvailableLabel', { + defaultMessage: 'Maintainance window available', + })} + + + + + + + + + } + fullWidth + > + >) => { + setSelectedMantainanceWindow(selected); + }} + /> + + ) : null} + {errors ? ( + <> + + + ) : null}
); }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f211cc9fede8e..8bd7308a27a70 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -13071,8 +13071,6 @@ "xpack.fleet.upgradeAgents.cancelButtonLabel": "Annuler", "xpack.fleet.upgradeAgents.confirmMultipleButtonLabel": "Mettre à niveau {count, plural, one {l'agent} other {{count} agents} =true {tous les agents sélectionnés}}", "xpack.fleet.upgradeAgents.confirmSingleButtonLabel": "Mettre à niveau l'agent", - "xpack.fleet.upgradeAgents.experimentalLabel": "Expérimental", - "xpack.fleet.upgradeAgents.experimentalLabelTooltip": "Une modification ou une suppression de la mise à niveau de l'agent peut intervenir dans une version ultérieure. La mise à niveau n'est pas soumise à l'accord de niveau de service du support technique.", "xpack.fleet.upgradeAgents.fatalErrorNotificationTitle": "Erreur lors de la mise à niveau de {count, plural, one {l'agent} other {{count} agents} =true {tous les agents sélectionnés}}", "xpack.fleet.upgradeAgents.successMultiNotificationTitle": "{isMixed, select, true {{success} agents sur {total}} other {{isAllAgents, select, true {Tous les agents sélectionnés} other {{success}} }}} mis à niveau", "xpack.fleet.upgradeAgents.successSingleNotificationTitle": "{count} agent mis à niveau", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index eec41bfb71c81..12300057ca7ff 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13178,8 +13178,6 @@ "xpack.fleet.upgradeAgents.cancelButtonLabel": "キャンセル", "xpack.fleet.upgradeAgents.confirmMultipleButtonLabel": "{count, plural, other {{count}個のエージェント} =true {すべての選択されたエージェント}}をアップグレード", "xpack.fleet.upgradeAgents.confirmSingleButtonLabel": "エージェントをアップグレード", - "xpack.fleet.upgradeAgents.experimentalLabel": "実験的", - "xpack.fleet.upgradeAgents.experimentalLabelTooltip": "アップグレードエージェントは今後のリリースで変更または削除される可能性があり、SLA のサポート対象になりません。", "xpack.fleet.upgradeAgents.fatalErrorNotificationTitle": "{count, plural, other {{count}個のエージェント} =true {すべての選択されたエージェント}}のアップグレードエラー", "xpack.fleet.upgradeAgents.successMultiNotificationTitle": "{isMixed, select, true {{success}/{total}個の} other {{isAllAgents, select, true {すべての選択された} other {{success}} }}}エージェントをアップグレードしました", "xpack.fleet.upgradeAgents.successSingleNotificationTitle": "{count}個のエージェントをアップグレードしました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2d7566bdd8c87..5953802b0a0a5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13202,8 +13202,6 @@ "xpack.fleet.upgradeAgents.cancelButtonLabel": "取消", "xpack.fleet.upgradeAgents.confirmMultipleButtonLabel": "升级{count, plural, one {代理} other { {count} 个代理} =true {所有选定代理}}", "xpack.fleet.upgradeAgents.confirmSingleButtonLabel": "升级代理", - "xpack.fleet.upgradeAgents.experimentalLabel": "实验性", - "xpack.fleet.upgradeAgents.experimentalLabelTooltip": "在未来的版本中可能会更改或移除升级代理,其不受支持 SLA 的约束。", "xpack.fleet.upgradeAgents.fatalErrorNotificationTitle": "升级{count, plural, one {代理} other { {count} 个代理} =true {所有选定代理}}时出错", "xpack.fleet.upgradeAgents.successMultiNotificationTitle": "已升级{isMixed, select, true { {success} 个(共 {total} 个)} other {{isAllAgents, select, true {所有选定} other { {success} 个} }}}代理", "xpack.fleet.upgradeAgents.successSingleNotificationTitle": "已升级 {count} 个代理", From 57d783a8c7806a525b926bcab6916a6afda889d2 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 20 May 2022 12:42:07 +0200 Subject: [PATCH 030/120] add tooltip and change icon (#132581) --- .../datatable_visualization/visualization.tsx | 9 +++++---- .../config_panel/color_indicator.tsx | 11 +++++++++++ .../shared_components/collapse_setting.tsx | 19 +++++++++++++++++-- x-pack/plugins/lens/public/types.ts | 2 +- .../public/xy_visualization/visualization.tsx | 2 +- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index d42af9aa3932c..12c5dafb5d942 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -203,10 +203,11 @@ export const getDatatableVisualization = ({ ) .map((accessor) => ({ columnId: accessor, - triggerIcon: - columnMap[accessor].hidden || columnMap[accessor].collapseFn - ? 'invisible' - : undefined, + triggerIcon: columnMap[accessor].hidden + ? 'invisible' + : columnMap[accessor].collapseFn + ? 'aggregate' + : undefined, })), supportsMoreColumns: true, filterOperations: (op) => op.isBucketed, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx index b8a5819d45532..b12f50a7b35a0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx @@ -59,6 +59,17 @@ export function ColorIndicator({ })} /> )} + {accessorConfig.triggerIcon === 'aggregate' && ( + + )} {accessorConfig.triggerIcon === 'colorBy' && ( +
+ {i18n.translate('xpack.lens.collapse.label', { defaultMessage: 'Collapse by' })} + {''} + + + + } display="columnCompressed" fullWidth > diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 1f2ee1266ddb7..1ffc300542b09 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -557,7 +557,7 @@ export type VisualizationDimensionEditorProps = VisualizationConfig export interface AccessorConfig { columnId: string; - triggerIcon?: 'color' | 'disabled' | 'colorBy' | 'none' | 'invisible'; + triggerIcon?: 'color' | 'disabled' | 'colorBy' | 'none' | 'invisible' | 'aggregate'; color?: string; palette?: string[] | Array<{ color: string; stop: number }>; } diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 096c395b31eaf..b35247f4d9d97 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -276,7 +276,7 @@ export const getXyVisualization = ({ ? [ { columnId: dataLayer.splitAccessor, - triggerIcon: dataLayer.collapseFn ? ('invisible' as const) : ('colorBy' as const), + triggerIcon: dataLayer.collapseFn ? ('aggregate' as const) : ('colorBy' as const), palette: dataLayer.collapseFn ? undefined : paletteService From b3aee1740ba63fdc83af0f9ce98246858c8fb929 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 20 May 2022 12:56:03 +0200 Subject: [PATCH 031/120] [ML] Disable AIOps UI/APIs. (#132589) This disables the UI and APIs for Explain log rate spikes in the ML plugin since it will not be part of 8.3. Once 8.3 has been branched off, we can reenable it in main. This also adds a check to the API integration tests to run the tests only when the hard coded feature flag is set to true. --- x-pack/plugins/aiops/common/index.ts | 2 +- x-pack/test/api_integration/apis/aiops/index.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/aiops/common/index.ts b/x-pack/plugins/aiops/common/index.ts index 0f4835d67ecc7..162fa9f1af624 100755 --- a/x-pack/plugins/aiops/common/index.ts +++ b/x-pack/plugins/aiops/common/index.ts @@ -19,4 +19,4 @@ export const PLUGIN_NAME = 'AIOps'; * This is an internal hard coded feature flag so we can easily turn on/off the * "Explain log rate spikes UI" during development until the first release. */ -export const AIOPS_ENABLED = true; +export const AIOPS_ENABLED = false; diff --git a/x-pack/test/api_integration/apis/aiops/index.ts b/x-pack/test/api_integration/apis/aiops/index.ts index d2aacc454b567..8d6b6ea13399f 100644 --- a/x-pack/test/api_integration/apis/aiops/index.ts +++ b/x-pack/test/api_integration/apis/aiops/index.ts @@ -5,13 +5,17 @@ * 2.0. */ +import { AIOPS_ENABLED } from '@kbn/aiops-plugin/common'; + import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('AIOps', function () { this.tags(['ml']); - loadTestFile(require.resolve('./example_stream')); - loadTestFile(require.resolve('./explain_log_rate_spikes')); + if (AIOPS_ENABLED) { + loadTestFile(require.resolve('./example_stream')); + loadTestFile(require.resolve('./explain_log_rate_spikes')); + } }); } From 3b7c7e81ff633eab2cd8c1da412ed88af7344e71 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Fri, 20 May 2022 13:08:20 +0200 Subject: [PATCH 032/120] Update user risk dashboard name (#132441) --- .../public/users/pages/navigation/user_risk_tab_body.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx index bb1f73765bf59..1684297fd236d 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx @@ -27,7 +27,7 @@ const StyledEuiFlexGroup = styled(EuiFlexGroup)` margin-top: ${({ theme }) => theme.eui.paddingSizes.l}; `; -const RISKY_USERS_DASHBOARD_TITLE = 'User Risk Score (Start Here)'; +const RISKY_USERS_DASHBOARD_TITLE = 'Current Risk Score For Users'; const UserRiskTabBodyComponent: React.FC< Pick & { From f0cb40af75a0848068d1775427762cdcab093ef6 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Fri, 20 May 2022 05:32:02 -0600 Subject: [PATCH 033/120] [ML] Data Frame Analytics: replace custom types with estypes (#132443) * replace common data_frame_analytics types from server with esclient types * remove unused ts error ignore commments * remove generic analyis type and move types to common dir * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * move types to commont folder Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../ml/common/types/data_frame_analytics.ts | 155 +++++++++--------- .../common/analytics.test.ts | 3 + .../data_frame_analytics/common/analytics.ts | 97 +---------- .../data_frame_analytics/common/fields.ts | 2 +- .../data_frame_analytics/common/index.ts | 8 +- .../analysis_fields_table.tsx | 2 +- .../configuration_step_form.tsx | 2 +- .../components/shared/fetch_explain_data.ts | 12 +- .../column_data.tsx | 2 +- .../evaluate_panel.tsx | 2 +- .../get_roc_curve_chart_vega_lite_spec.tsx | 2 +- .../use_confusion_matrix.ts | 8 +- .../use_roc_curve.ts | 4 +- .../exploration_page_wrapper.tsx | 2 +- .../use_exploration_results.ts | 2 +- .../outlier_exploration/use_outlier_data.ts | 6 +- .../regression_exploration/evaluate_panel.tsx | 2 +- .../action_edit/edit_action_flyout.tsx | 10 +- .../analytics_list/expanded_row.tsx | 6 +- .../components/analytics_list/use_columns.tsx | 1 + .../use_create_analytics_form/reducer.ts | 1 + .../use_create_analytics_form/state.test.ts | 1 + .../hooks/use_create_analytics_form/state.ts | 4 +- .../use_create_analytics_form.ts | 4 +- .../analytics_service/get_analytics.test.ts | 2 + .../ml_api_service/data_frame_analytics.ts | 6 +- .../data_frame_analytics/analytics_manager.ts | 1 - .../models/data_frame_analytics/validation.ts | 30 +++- .../plugins/ml/server/saved_objects/checks.ts | 2 +- .../ml/stack_management_jobs/export_jobs.ts | 3 - 30 files changed, 155 insertions(+), 227 deletions(-) diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index 92c0c1d06ef93..3d7dda658a0ba 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -7,11 +7,9 @@ import Boom from '@hapi/boom'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { RuntimeMappings } from './fields'; import { EsErrorBody } from '../util/errors'; import { ANALYSIS_CONFIG_TYPE } from '../constants/data_frame_analytics'; -import { DATA_FRAME_TASK_STATE } from '../constants/data_frame_analytics'; export interface DeleteDataFrameAnalyticsWithIndexStatus { success: boolean; @@ -57,69 +55,90 @@ export interface ClassificationAnalysis { classification: Classification; } -interface GenericAnalysis { - [key: string]: Record; +export type AnalysisConfig = estypes.MlDataframeAnalysisContainer; +export interface DataFrameAnalyticsConfig + extends Omit { + analyzed_fields?: estypes.MlDataframeAnalysisAnalyzedFields; } -export type AnalysisConfig = - | OutlierAnalysis - | RegressionAnalysis - | ClassificationAnalysis - | GenericAnalysis; - -export interface DataFrameAnalyticsConfig { - id: DataFrameAnalyticsId; +export interface UpdateDataFrameAnalyticsConfig { + allow_lazy_start?: string; description?: string; - dest: { - index: IndexName; - results_field: string; - }; - source: { - index: IndexName | IndexName[]; - query?: estypes.QueryDslQueryContainer; - runtime_mappings?: RuntimeMappings; - }; - analysis: AnalysisConfig; - analyzed_fields?: { - includes?: string[]; - excludes?: string[]; - }; - model_memory_limit: string; + model_memory_limit?: string; max_num_threads?: number; - create_time: number; - version: string; - allow_lazy_start?: boolean; } export type DataFrameAnalysisConfigType = typeof ANALYSIS_CONFIG_TYPE[keyof typeof ANALYSIS_CONFIG_TYPE]; -export type DataFrameTaskStateType = - typeof DATA_FRAME_TASK_STATE[keyof typeof DATA_FRAME_TASK_STATE]; +export type DataFrameTaskStateType = estypes.MlDataframeState | 'analyzing' | 'reindexing'; + +export interface DataFrameAnalyticsStats extends Omit { + failure_reason?: string; + state: DataFrameTaskStateType; +} + +export type DfAnalyticsExplainResponse = estypes.MlExplainDataFrameAnalyticsResponse; + +export interface PredictedClass { + predicted_class: string; + count: number; +} +export interface ConfusionMatrix { + actual_class: string; + actual_class_doc_count: number; + predicted_classes: PredictedClass[]; + other_predicted_class_doc_count: number; +} + +export interface RocCurveItem { + fpr: number; + threshold: number; + tpr: number; +} -interface ProgressSection { - phase: string; - progress_percent: number; +interface EvalClass { + class_name: string; + value: number; +} +export interface ClassificationEvaluateResponse { + classification: { + multiclass_confusion_matrix?: { + confusion_matrix: ConfusionMatrix[]; + }; + recall?: { + classes: EvalClass[]; + avg_recall: number; + }; + accuracy?: { + classes: EvalClass[]; + overall_accuracy: number; + }; + auc_roc?: { + curve?: RocCurveItem[]; + value: number; + }; + }; } -export interface DataFrameAnalyticsStats { - assignment_explanation?: string; - id: DataFrameAnalyticsId; - memory_usage?: { - timestamp?: string; - peak_usage_bytes: number; - status: string; +export interface EvaluateMetrics { + classification: { + accuracy?: object; + recall?: object; + multiclass_confusion_matrix?: object; + auc_roc?: { include_curve: boolean; class_name: string }; }; - node?: { - attributes: Record; - ephemeral_id: string; - id: string; - name: string; - transport_address: string; + regression: { + r_squared: object; + mse: object; + msle: object; + huber: object; }; - progress: ProgressSection[]; - failure_reason?: string; - state: DataFrameTaskStateType; +} + +export interface FieldSelectionItem + extends Omit { + mapping_types?: string[]; } export interface AnalyticsMapNodeElement { @@ -146,30 +165,14 @@ export interface AnalyticsMapReturnType { error: null | any; } -export interface FeatureProcessor { - frequency_encoding: { - feature_name: string; - field: string; - frequency_map: Record; - }; - multi_encoding: { - processors: any[]; - }; - n_gram_encoding: { - feature_prefix?: string; - field: string; - length?: number; - n_grams: number[]; - start?: number; - }; - one_hot_encoding: { - field: string; - hot_map: string; - }; - target_mean_encoding: { - default_value: number; - feature_name: string; - field: string; - target_map: Record; +export type FeatureProcessor = estypes.MlDataframeAnalysisFeatureProcessor; + +export interface TrackTotalHitsSearchResponse { + hits: { + total: { + value: number; + relation: string; + }; + hits: any[]; }; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts index 0cd4d190ebbbd..aa83ce0a1f4ad 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts @@ -13,15 +13,18 @@ describe('Data Frame Analytics: Analytics utils', () => { expect(getAnalysisType(outlierAnalysis)).toBe('outlier_detection'); const regressionAnalysis = { regression: {} }; + // @ts-expect-error incomplete regression analysis expect(getAnalysisType(regressionAnalysis)).toBe('regression'); // test against a job type that does not exist yet. const otherAnalysis = { other: {} }; + // @ts-expect-error unkown analysis type expect(getAnalysisType(otherAnalysis)).toBe('other'); // if the analysis object has a shape that is not just a single property, // the job type will be returned as 'unknown'. const unknownAnalysis = { outlier_detection: {}, regression: {} }; + // @ts-expect-error unkown analysis type expect(getAnalysisType(unknownAnalysis)).toBe('unknown'); }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index c2c2563c5ba7c..064416cd722d1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -12,6 +12,11 @@ import { cloneDeep } from 'lodash'; import { ml } from '../../services/ml_api_service'; import { Dictionary } from '../../../../common/types/common'; import { extractErrorMessage } from '../../../../common/util/errors'; +import { + ClassificationEvaluateResponse, + EvaluateMetrics, + TrackTotalHitsSearchResponse, +} from '../../../../common/types/data_frame_analytics'; import { SavedSearchQuery } from '../../contexts/ml'; import { AnalysisConfig, @@ -106,23 +111,6 @@ export enum INDEX_STATUS { ERROR, } -export interface FieldSelectionItem { - name: string; - mappings_types?: string[]; - is_included: boolean; - is_required: boolean; - feature_type?: string; - reason?: string; -} - -export interface DfAnalyticsExplainResponse { - field_selection?: FieldSelectionItem[]; - memory_estimation: { - expected_memory_without_disk: string; - expected_memory_with_disk: string; - }; -} - export interface Eval { mse: number | string; msle: number | string; @@ -148,49 +136,6 @@ export interface RegressionEvaluateResponse { }; } -export interface PredictedClass { - predicted_class: string; - count: number; -} - -export interface ConfusionMatrix { - actual_class: string; - actual_class_doc_count: number; - predicted_classes: PredictedClass[]; - other_predicted_class_doc_count: number; -} - -export interface RocCurveItem { - fpr: number; - threshold: number; - tpr: number; -} - -interface EvalClass { - class_name: string; - value: number; -} - -export interface ClassificationEvaluateResponse { - classification: { - multiclass_confusion_matrix?: { - confusion_matrix: ConfusionMatrix[]; - }; - recall?: { - classes: EvalClass[]; - avg_recall: number; - }; - accuracy?: { - classes: EvalClass[]; - overall_accuracy: number; - }; - auc_roc?: { - curve?: RocCurveItem[]; - value: number; - }; - }; -} - interface LoadEvaluateResult { success: boolean; eval: RegressionEvaluateResponse | ClassificationEvaluateResponse | null; @@ -279,13 +224,6 @@ export const isClassificationEvaluateResponse = ( ); }; -export interface UpdateDataFrameAnalyticsConfig { - allow_lazy_start?: string; - description?: string; - model_memory_limit?: string; - max_num_threads?: number; -} - export enum REFRESH_ANALYTICS_LIST_STATE { ERROR = 'error', IDLE = 'idle', @@ -451,21 +389,6 @@ export enum REGRESSION_STATS { HUBER = 'huber', } -interface EvaluateMetrics { - classification: { - accuracy?: object; - recall?: object; - multiclass_confusion_matrix?: object; - auc_roc?: { include_curve: boolean; class_name: string }; - }; - regression: { - r_squared: object; - mse: object; - msle: object; - huber: object; - }; -} - interface LoadEvalDataConfig { isTraining?: boolean; index: string; @@ -548,16 +471,6 @@ export const loadEvalData = async ({ } }; -interface TrackTotalHitsSearchResponse { - hits: { - total: { - value: number; - relation: string; - }; - hits: any[]; - }; -} - interface LoadDocsCountConfig { ignoreDefaultQuery?: boolean; isTraining?: boolean; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index 89c05643f0dc8..3ab82daa6b1f3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -96,7 +96,7 @@ export const sortExplorationResultsFields = ( if (isClassificationAnalysis(jobConfig.analysis) || isRegressionAnalysis(jobConfig.analysis)) { const dependentVariable = getDependentVar(jobConfig.analysis); - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); + const predictedField = getPredictedFieldName(resultsField!, jobConfig.analysis, true); if (a === `${resultsField}.is_training`) { return -1; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 2fb0daa1ed45e..f47b5b66f4944 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -5,13 +5,7 @@ * 2.0. */ -export type { - UpdateDataFrameAnalyticsConfig, - IndexPattern, - RegressionEvaluateResponse, - Eval, - SearchQuery, -} from './analytics'; +export type { IndexPattern, RegressionEvaluateResponse, Eval, SearchQuery } from './analytics'; export { getAnalysisType, getDependentVar, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx index d98940588f48f..f4a9eb0d5c0a8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx @@ -12,7 +12,7 @@ import { isEqual } from 'lodash'; import { LEFT_ALIGNMENT, SortableProperties } from '@elastic/eui/lib/services'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { FieldSelectionItem } from '../../../../common/analytics'; +import { FieldSelectionItem } from '../../../../../../../common/types/data_frame_analytics'; // @ts-ignore could not find declaration file import { CustomSelectionTable } from '../../../../../components/custom_selection_table'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index b4f55bcae0947..758fd01a133c6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -28,10 +28,10 @@ import { ANALYSIS_CONFIG_TYPE, TRAINING_PERCENT_MIN, TRAINING_PERCENT_MAX, - FieldSelectionItem, } from '../../../../common/analytics'; import { getScatterplotMatrixLegendType } from '../../../../common/get_scatterplot_matrix_legend_type'; import { RuntimeMappings as RuntimeMappingsType } from '../../../../../../../common/types/fields'; +import { FieldSelectionItem } from '../../../../../../../common/types/data_frame_analytics'; import { isRuntimeMappings, isRuntimeField, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts index 7c83b0af15107..ca334a58b36c2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts @@ -7,19 +7,15 @@ import { ml } from '../../../../../services/ml_api_service'; import { extractErrorProperties } from '../../../../../../../common/util/errors'; -import { DfAnalyticsExplainResponse, FieldSelectionItem } from '../../../../common/analytics'; +import { + DfAnalyticsExplainResponse, + FieldSelectionItem, +} from '../../../../../../../common/types/data_frame_analytics'; import { getJobConfigFromFormState, State, } from '../../../analytics_management/hooks/use_create_analytics_form/state'; -export interface FetchExplainDataReturnType { - success: boolean; - expectedMemory: string; - fieldSelection: FieldSelectionItem[]; - errorMessage: string; -} - export const fetchExplainData = async (formState: State['form']) => { const jobConfig = getJobConfigFromFormState(formState); let errorMessage = ''; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx index 31b7db66f81ae..c983511f80393 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx @@ -14,7 +14,7 @@ import { EuiPopover, EuiText, } from '@elastic/eui'; -import { ConfusionMatrix } from '../../../../common/analytics'; +import { ConfusionMatrix } from '../../../../../../../common/types/data_frame_analytics'; const COL_INITIAL_WIDTH = 165; // in pixels diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 97ab582832b64..8ba780a3e512a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -119,7 +119,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se columns.map(({ id }: { id: string }) => id) ); - const resultsField = jobConfig.dest.results_field; + const resultsField = jobConfig.dest.results_field!; const isTraining = isTrainingFilter(searchQuery, resultsField); const { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx index 3ca1f65cf2ecc..e3f92c36507c6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx @@ -16,7 +16,7 @@ import { i18n } from '@kbn/i18n'; import { LEGEND_TYPES } from '../../../../../components/vega_chart/common'; -import { RocCurveItem } from '../../../../common/analytics'; +import { RocCurveItem } from '../../../../../../../common/types/data_frame_analytics'; const GRAY = euiPaletteGray(1)[0]; const BASELINE = 'baseline'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts index 2a75acf823e88..c51f5bf3e9665 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts @@ -9,13 +9,15 @@ import { useState, useEffect } from 'react'; import { isClassificationEvaluateResponse, - ClassificationEvaluateResponse, - ConfusionMatrix, ResultsSearchQuery, ANALYSIS_CONFIG_TYPE, ClassificationMetricItem, } from '../../../../common/analytics'; import { isKeywordAndTextType } from '../../../../common/fields'; +import { + ClassificationEvaluateResponse, + ConfusionMatrix, +} from '../../../../../../../common/types/data_frame_analytics'; import { getDependentVar, @@ -78,7 +80,7 @@ export const useConfusionMatrix = ( let requiresKeyword = false; const dependentVariable = getDependentVar(jobConfig.analysis); - const resultsField = jobConfig.dest.results_field; + const resultsField = jobConfig.dest.results_field!; const isTraining = isTrainingFilter(searchQuery, resultsField); try { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_roc_curve.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_roc_curve.ts index 20521258cd374..f83f9f9f31e0f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_roc_curve.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_roc_curve.ts @@ -10,10 +10,10 @@ import { useState, useEffect } from 'react'; import { isClassificationEvaluateResponse, ResultsSearchQuery, - RocCurveItem, ANALYSIS_CONFIG_TYPE, } from '../../../../common/analytics'; import { isKeywordAndTextType } from '../../../../common/fields'; +import { RocCurveItem } from '../../../../../../../common/types/data_frame_analytics'; import { getDependentVar, @@ -58,7 +58,7 @@ export const useRocCurve = ( setIsLoading(true); const dependentVariable = getDependentVar(jobConfig.analysis); - const resultsField = jobConfig.dest.results_field; + const resultsField = jobConfig.dest.results_field!; const newRocCurveData: RocCurveDataRow[] = []; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx index 48477acfe7be8..17453dd87b0d0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -170,7 +170,7 @@ export const ExplorationPageWrapper: FC = ({ indexPattern={indexPattern} setSearchQuery={searchQueryUpdateHandler} query={query} - filters={getFilters(jobConfig.dest.results_field)} + filters={getFilters(jobConfig.dest.results_field!)} /> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts index c0590fd80a5d5..593ef5465d196 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -57,7 +57,7 @@ export const useExplorationResults = ( const columns: EuiDataGridColumn[] = []; if (jobConfig !== undefined) { - const resultsField = jobConfig.dest.results_field; + const resultsField = jobConfig.dest.results_field!; const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); columns.push( ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => 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 45653209cdb8a..920023c23a2bd 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 @@ -55,7 +55,7 @@ export const useOutlierData = ( const resultsField = jobConfig.dest.results_field; const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); newColumns.push( - ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField!).sort((a: any, b: any) => sortExplorationResultsFields(a.id, b.id, jobConfig) ) ); @@ -135,7 +135,9 @@ export const useOutlierData = ( const colorRange = useColorRange( COLOR_RANGE.BLUE, COLOR_RANGE_SCALE.INFLUENCER, - jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, dataGrid.tableItems) : 1 + jobConfig !== undefined + ? getFeatureCount(jobConfig.dest.results_field!, dataGrid.tableItems) + : 1 ); const renderCellValue = useRenderCellValue( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 6d5417db24607..1249b736960d8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -75,7 +75,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) const dependentVariable = getDependentVar(jobConfig.analysis); const predictionFieldName = getPredictionFieldName(jobConfig.analysis); // default is 'ml' - const resultsField = jobConfig.dest.results_field; + const resultsField = jobConfig.dest.results_field ?? 'ml'; const loadGeneralizationData = async (ignoreDefaultQuery: boolean = true) => { setIsLoadingGeneralization(true); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx index 766f1bda64d5e..3b8d3ed5460ff 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx @@ -34,10 +34,8 @@ import { MemoryInputValidatorResult, } from '../../../../../../../common/util/validators'; import { DATA_FRAME_TASK_STATE } from '../analytics_list/common'; -import { - useRefreshAnalyticsList, - UpdateDataFrameAnalyticsConfig, -} from '../../../../common/analytics'; +import { useRefreshAnalyticsList } from '../../../../common/analytics'; +import { UpdateDataFrameAnalyticsConfig } from '../../../../../../../common/types/data_frame_analytics'; import { EditAction } from './use_edit_action'; @@ -51,7 +49,9 @@ export const EditActionFlyout: FC> = ({ closeFlyout, item } const [allowLazyStart, setAllowLazyStart] = useState(initialAllowLazyStart); const [description, setDescription] = useState(config.description || ''); - const [modelMemoryLimit, setModelMemoryLimit] = useState(config.model_memory_limit); + const [modelMemoryLimit, setModelMemoryLimit] = useState( + config.model_memory_limit + ); const [mmlValidationError, setMmlValidationError] = useState(); const [maxNumThreads, setMaxNumThreads] = useState(config.max_num_threads); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index 3f7072fba4040..2d072d1aecc1f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -75,7 +75,7 @@ export const ExpandedRow: FC = ({ item }) => { const dependentVariable = getDependentVar(item.config.analysis); const predictionFieldName = getPredictionFieldName(item.config.analysis); // default is 'ml' - const resultsField = item.config.dest.results_field; + const resultsField = item.config.dest.results_field ?? 'ml'; const jobIsCompleted = isCompletedAnalyticsJob(item.stats); const isRegressionJob = isRegressionAnalysis(item.config.analysis); const analysisType = getAnalysisType(item.config.analysis); @@ -232,8 +232,8 @@ export const ExpandedRow: FC = ({ item }) => { moment(item.config.create_time).unix() * 1000 ), }, - { title: 'model_memory_limit', description: item.config.model_memory_limit }, - { title: 'version', description: item.config.version }, + { title: 'model_memory_limit', description: item.config.model_memory_limit ?? '' }, + { title: 'version', description: item.config.version ?? '' }, ], position: 'left', dataTestSubj: 'mlAnalyticsTableRowDetailsSection stats', diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index 3077f0fb38726..efa1f58ecddc0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -43,6 +43,7 @@ enum TASK_STATE_COLOR { started = 'primary', starting = 'primary', stopped = 'hollow', + stopping = 'hollow', } export const getTaskStateBadge = ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 5559e7db2d631..58a471b4e7246 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -193,6 +193,7 @@ export const validateAdvancedEditor = (state: State): State => { dependentVariableEmpty = dependentVariableName === ''; if ( !dependentVariableEmpty && + Array.isArray(analyzedFields) && analyzedFields.length > 0 && !analyzedFields.includes(dependentVariableName) ) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts index c51ccf1e20d8d..c27137fca9519 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts @@ -82,6 +82,7 @@ describe('useCreateAnalyticsForm', () => { expect(jobConfig?.dest?.index).toBe('the-destination-index'); expect(jobConfig?.source?.index).toBe('the-source-index'); expect(jobConfig?.analyzed_fields?.includes).toStrictEqual([]); + // @ts-ignore property 'excludes' does not exist expect(typeof jobConfig?.analyzed_fields?.excludes).toBe('undefined'); // test the conversion of comma-separated Kibana index patterns to ES array based index patterns diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 0b2cb8fcfc716..ca54c552f8ebf 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -370,7 +370,9 @@ export function getFormStateFromJobConfig( runtimeMappings: analyticsJobConfig.source.runtime_mappings, modelMemoryLimit: analyticsJobConfig.model_memory_limit, maxNumThreads: analyticsJobConfig.max_num_threads, - includes: analyticsJobConfig.analyzed_fields?.includes ?? [], + includes: Array.isArray(analyticsJobConfig.analyzed_fields?.includes) + ? analyticsJobConfig.analyzed_fields?.includes + : [], jobConfigQuery: analyticsJobConfig.source.query || defaultSearchQuery, }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 41a8ae4eeba92..cddc4fcd092dc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -333,8 +333,8 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { dispatch({ type: ACTION.SWITCH_TO_FORM }); }; - const setEstimatedModelMemoryLimit = (value: State['estimatedModelMemoryLimit']) => { - dispatch({ type: ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT, value }); + const setEstimatedModelMemoryLimit = (value: State['estimatedModelMemoryLimit'] | undefined) => { + dispatch({ type: ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT, value: value ?? '' }); }; const setJobClone = async (cloneJob: DeepReadonly) => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts index 1c2598477064f..e0324a261e57d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts @@ -15,6 +15,7 @@ describe('get_analytics', () => { const mockResponse: GetDataFrameAnalyticsStatsResponseOk = { count: 2, data_frame_analytics: [ + // @ts-expect-error test response missing expected properties { id: 'outlier-cloudwatch', state: DATA_FRAME_TASK_STATE.STOPPED, @@ -37,6 +38,7 @@ describe('get_analytics', () => { }, ], }, + // @ts-expect-error test response missing expected properties { id: 'reg-gallery', state: DATA_FRAME_TASK_STATE.FAILED, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index e4deb90d81073..479f8c50ae035 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -10,12 +10,10 @@ import { http } from '../http_service'; import { basePath } from '.'; import type { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import type { ValidateAnalyticsJobResponse } from '../../../../common/constants/validation'; -import type { - DataFrameAnalyticsConfig, - UpdateDataFrameAnalyticsConfig, -} from '../../data_frame_analytics/common'; +import type { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common'; import type { DeepPartial } from '../../../../common/types/common'; import type { NewJobCapsResponse } from '../../../../common/types/fields'; +import type { UpdateDataFrameAnalyticsConfig } from '../../../../common/types/data_frame_analytics'; import type { JobMessage } from '../../../../common/types/audit_message'; import type { DeleteDataFrameAnalyticsWithIndexStatus, diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts index 894354a0113fc..d4076a7cf496a 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -78,7 +78,6 @@ export class AnalyticsManager { async setJobStats() { try { const jobStats = await this.getAnalyticsStats(); - // @ts-expect-error @elastic-elasticsearch Data frame types incomplete this.jobStats = jobStats; } catch (error) { // eslint-disable-next-line diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts index 9cd8b67be2a6d..517f3cadf3b18 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts @@ -239,14 +239,16 @@ async function getValidationCheckMessages( let analysisFieldsEmpty = false; const fieldLimit = - analyzedFields.length <= MINIMUM_NUM_FIELD_FOR_CHECK + Array.isArray(analyzedFields) && analyzedFields.length <= MINIMUM_NUM_FIELD_FOR_CHECK ? analyzedFields.length : MINIMUM_NUM_FIELD_FOR_CHECK; - let aggs = analyzedFields.slice(0, fieldLimit).reduce((acc, curr) => { - acc[curr] = { missing: { field: curr } }; - return acc; - }, {} as any); + let aggs = Array.isArray(analyzedFields) + ? analyzedFields.slice(0, fieldLimit).reduce((acc, curr) => { + acc[curr] = { missing: { field: curr } }; + return acc; + }, {} as any) + : {}; if (depVar !== '') { const depVarAgg = { @@ -344,10 +346,18 @@ async function getValidationCheckMessages( ); messages.push(...regressionAndClassificationMessages); - if (analyzedFields.length && analyzedFields.length > INCLUDED_FIELDS_THRESHOLD) { + if ( + Array.isArray(analyzedFields) && + analyzedFields.length && + analyzedFields.length > INCLUDED_FIELDS_THRESHOLD + ) { analysisFieldsNumHigh = true; } else { - if (analysisType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && analyzedFields.length < 1) { + if ( + analysisType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && + Array.isArray(analyzedFields) && + analyzedFields.length < 1 + ) { lowFieldCountWarningMessage.text = i18n.translate( 'xpack.ml.models.dfaValidation.messages.lowFieldCountOutlierWarningText', { @@ -358,6 +368,7 @@ async function getValidationCheckMessages( messages.push(lowFieldCountWarningMessage); } else if ( analysisType !== ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && + Array.isArray(analyzedFields) && analyzedFields.length < 2 ) { lowFieldCountWarningMessage.text = i18n.translate( @@ -446,9 +457,12 @@ export async function validateAnalyticsJob( client: IScopedClusterClient, job: DataFrameAnalyticsConfig ) { + const includedFields = ( + Array.isArray(job?.analyzed_fields?.includes) ? job?.analyzed_fields?.includes : [] + ) as string[]; const messages = await getValidationCheckMessages( client.asCurrentUser, - job?.analyzed_fields?.includes || [], + includedFields, job.analysis, job.source ); diff --git a/x-pack/plugins/ml/server/saved_objects/checks.ts b/x-pack/plugins/ml/server/saved_objects/checks.ts index 93b68ea3fd990..a5cb560d324d2 100644 --- a/x-pack/plugins/ml/server/saved_objects/checks.ts +++ b/x-pack/plugins/ml/server/saved_objects/checks.ts @@ -131,7 +131,7 @@ export function checksFactory( ); const dfaJobsCreateTimeMap = dfaJobs.data_frame_analytics.reduce((acc, cur) => { - acc.set(cur.id, cur.create_time); + acc.set(cur.id, cur.create_time!); return acc; }, new Map()); diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts index c43cf74e3048c..69ecc7f446b58 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts @@ -169,7 +169,6 @@ const testADJobs: Array<{ job: Job; datafeed: Datafeed }> = [ ]; const testDFAJobs: DataFrameAnalyticsConfig[] = [ - // @ts-expect-error not full interface { id: `bm_1_1`, description: @@ -198,7 +197,6 @@ const testDFAJobs: DataFrameAnalyticsConfig[] = [ model_memory_limit: '60mb', allow_lazy_start: false, }, - // @ts-expect-error not full interface { id: `ihp_1_2`, description: 'This is the job description', @@ -221,7 +219,6 @@ const testDFAJobs: DataFrameAnalyticsConfig[] = [ }, model_memory_limit: '5mb', }, - // @ts-expect-error not full interface { id: `egs_1_3`, description: 'This is the job description', From 92ac7f925545e088de8466e39d77a284ab9ffd67 Mon Sep 17 00:00:00 2001 From: Katrin Freihofner Date: Fri, 20 May 2022 13:51:51 +0200 Subject: [PATCH 034/120] adds small styling updates to header panels (#132596) --- .../public/pages/rule_details/index.tsx | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx index 99000a91671b8..5cc12452e57e1 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/index.tsx @@ -301,15 +301,15 @@ export function RuleDetailsPage() { : [], }} > - + {/* Left side of Rule Summary */} - + @@ -318,7 +318,7 @@ export function RuleDetailsPage() { - + {i18n.translate('xpack.observability.ruleDetails.lastRun', { @@ -330,11 +330,7 @@ export function RuleDetailsPage() { itemValue={moment(rule.executionStatus.lastExecutionDate).fromNow()} /> - - - - - + {i18n.translate('xpack.observability.ruleDetails.alerts', { @@ -376,8 +372,6 @@ export function RuleDetailsPage() { /> )} - - @@ -385,7 +379,7 @@ export function RuleDetailsPage() { {/* Right side of Rule Summary */} - + @@ -401,7 +395,7 @@ export function RuleDetailsPage() { )} - + @@ -416,9 +410,9 @@ export function RuleDetailsPage() { /> - + - + {i18n.translate('xpack.observability.ruleDetails.description', { defaultMessage: 'Description', @@ -429,7 +423,7 @@ export function RuleDetailsPage() { /> - + @@ -449,8 +443,6 @@ export function RuleDetailsPage() { - - @@ -463,7 +455,7 @@ export function RuleDetailsPage() { - + @@ -474,7 +466,7 @@ export function RuleDetailsPage() { - + {i18n.translate('xpack.observability.ruleDetails.actions', { From 1c2eb9f03da58875360ed1f65a6a0bad33e52c6e Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 20 May 2022 13:59:56 +0100 Subject: [PATCH 035/120] [Security Solution] New Side nav integrating links config (#132210) * Update navigation landing pages to use appLinks config * align app links changes * link configs refactor to use updater$ * navigation panel categories * test and type fixes * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * types changes * shared style change moved to a separate PR * use old deep links * minor changes after ux meeting * add links filtering * remove duplicated categories * temporary increase of plugin size limit * swap management links order * improve performance closing nav panel * test updated * host isolation page filterd and some improvements * remove async from plugin start * move links register from start to mount * restore size limits * Fix use_show_timeline unit tests Co-authored-by: Pablo Neves Machado Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../public/app/deep_links/index.ts | 39 +- .../app/home/template_wrapper/index.tsx | 25 +- .../public/app/translations.ts | 2 +- .../security_solution/public/cases/links.ts | 12 +- .../components/navigation/nav_links.test.ts | 51 +- .../common/components/navigation/nav_links.ts | 34 +- .../security_side_nav/icons/launch.tsx | 25 + .../navigation/security_side_nav/index.ts | 8 + .../security_side_nav.test.tsx | 256 +++++++++ .../security_side_nav/security_side_nav.tsx | 156 ++++++ .../solution_grouped_nav.test.tsx | 4 +- .../solution_grouped_nav.tsx | 253 +++++---- .../solution_grouped_nav_item.tsx | 188 ------- .../solution_grouped_nav_panel.test.tsx | 17 +- .../solution_grouped_nav_panel.tsx | 123 ++++- .../navigation/solution_grouped_nav/types.ts | 32 ++ .../common/components/navigation/types.ts | 18 +- .../__snapshots__/index.test.tsx.snap | 2 +- .../use_primary_navigation.tsx | 6 +- .../public/common/links/app_links.ts | 60 +- .../public/common/links/index.tsx | 1 + .../public/common/links/links.test.ts | 520 ++++++------------ .../public/common/links/links.ts | 313 ++++++----- .../public/common/links/types.ts | 108 ++-- .../utils/timeline/use_show_timeline.test.tsx | 22 + .../public/detections/links.ts | 7 +- .../security_solution/public/hosts/links.ts | 1 - .../components/landing_links_icons.test.tsx | 3 +- .../components/landing_links_icons.tsx | 2 +- .../components/landing_links_images.test.tsx | 3 +- .../components/landing_links_images.tsx | 2 +- .../public/landing_pages/constants.ts | 36 -- .../public/landing_pages/links.ts | 52 ++ .../landing_pages/pages/manage.test.tsx | 172 +++++- .../public/landing_pages/pages/manage.tsx | 81 +-- .../public/management/links.ts | 60 +- .../public/overview/links.ts | 28 +- .../security_solution/public/plugin.tsx | 100 +++- .../public/timelines/links.ts | 7 +- .../security_solution/server/ui_settings.ts | 2 +- 40 files changed, 1710 insertions(+), 1121 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/icons/launch.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_item.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts delete mode 100644 x-pack/plugins/security_solution/public/landing_pages/constants.ts create mode 100644 x-pack/plugins/security_solution/public/landing_pages/links.ts diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index 550ec608a76cb..6598e0dc29426 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -10,7 +10,8 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; import { LicenseType } from '@kbn/licensing-plugin/common/types'; import { getCasesDeepLinks } from '@kbn/cases-plugin/public'; -import { AppDeepLink, AppNavLinkStatus, Capabilities } from '@kbn/core/public'; +import { AppDeepLink, AppNavLinkStatus, AppUpdater, Capabilities } from '@kbn/core/public'; +import { Subject } from 'rxjs'; import { SecurityPageName } from '../types'; import { OVERVIEW, @@ -63,6 +64,8 @@ import { RULES_CREATE_PATH, } from '../../../common/constants'; import { ExperimentalFeatures } from '../../../common/experimental_features'; +import { subscribeAppLinks } from '../../common/links'; +import { AppLinkItems } from '../../common/links/types'; const FEATURE = { general: `${SERVER_APP_ID}.show`, @@ -553,3 +556,37 @@ export function isPremiumLicense(licenseType?: LicenseType): boolean { licenseType === 'trial' ); } + +/** + * New deep links code starts here. + * All the code above will be removed once the appLinks migration is over. + * The code below manages the new implementation using the unified appLinks. + */ + +const formatDeepLinks = (appLinks: AppLinkItems): AppDeepLink[] => + appLinks.map((appLink) => ({ + id: appLink.id, + path: appLink.path, + title: appLink.title, + navLinkStatus: appLink.globalNavEnabled ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, + searchable: !appLink.globalSearchDisabled, + ...(appLink.globalSearchKeywords != null ? { keywords: appLink.globalSearchKeywords } : {}), + ...(appLink.globalNavOrder != null ? { order: appLink.globalNavOrder } : {}), + ...(appLink.links && appLink.links?.length + ? { + deepLinks: formatDeepLinks(appLink.links), + } + : {}), + })); + +/** + * Registers any change in appLinks to be updated in app deepLinks + */ +export const registerDeepLinksUpdater = (appUpdater$: Subject) => { + subscribeAppLinks((appLinks) => { + appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // needed to prevent main security link to switch to visible after update + deepLinks: formatDeepLinks(appLinks), + })); + }); +}; diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx index 3b436d2bdefc1..8d7d9daad550d 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -26,6 +26,7 @@ import { gutterTimeline } from '../../../common/lib/helpers'; import { useKibana } from '../../../common/lib/kibana'; import { useShowPagesWithEmptyView } from '../../../common/utils/empty_view/use_show_pages_with_empty_view'; import { useIsPolicySettingsBarVisible } from '../../../management/pages/policy/view/policy_hooks'; +import { useIsGroupedNavigationEnabled } from '../../../common/components/navigation/helpers'; const NO_DATA_PAGE_MAX_WIDTH = 950; @@ -44,8 +45,7 @@ const NO_DATA_PAGE_TEMPLATE_PROPS = { */ const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<{ $isShowingTimelineOverlay?: boolean; - $isTimelineBottomBarVisible?: boolean; - $isPolicySettingsVisible?: boolean; + $addBottomPadding?: boolean; }>` .${BOTTOM_BAR_CLASSNAME} { animation: 'none !important'; // disable the default bottom bar slide animation @@ -63,19 +63,8 @@ const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<{ } // If the bottom bar is visible add padding to the navigation - ${({ $isTimelineBottomBarVisible }) => - $isTimelineBottomBarVisible && - ` - @media (min-width: 768px) { - .kbnPageTemplateSolutionNav { - padding-bottom: ${gutterTimeline}; - } - } - `} - - // If the policy settings bottom bar is visible add padding to the navigation - ${({ $isPolicySettingsVisible }) => - $isPolicySettingsVisible && + ${({ $addBottomPadding }) => + $addBottomPadding && ` @media (min-width: 768px) { .kbnPageTemplateSolutionNav { @@ -98,6 +87,9 @@ export const SecuritySolutionTemplateWrapper: React.FC getTimelineShowStatus(state, TimelineId.active) ); + const isGroupedNavEnabled = useIsGroupedNavigationEnabled(); + const addBottomPadding = + isTimelineBottomBarVisible || isPolicySettingsVisible || isGroupedNavEnabled; const userHasSecuritySolutionVisible = useKibana().services.application.capabilities.siem.show; const showEmptyState = useShowPagesWithEmptyView(); @@ -117,9 +109,8 @@ export const SecuritySolutionTemplateWrapper: React.FC diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 9857e7160a209..354ba438ff52a 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -23,7 +23,7 @@ export const HOSTS = i18n.translate('xpack.securitySolution.navigation.hosts', { }); export const GETTING_STARTED = i18n.translate('xpack.securitySolution.navigation.gettingStarted', { - defaultMessage: 'Getting started', + defaultMessage: 'Get started', }); export const THREAT_HUNTING = i18n.translate('xpack.securitySolution.navigation.threatHunting', { diff --git a/x-pack/plugins/security_solution/public/cases/links.ts b/x-pack/plugins/security_solution/public/cases/links.ts index 9ed7a1f3980a6..bafaee6baa583 100644 --- a/x-pack/plugins/security_solution/public/cases/links.ts +++ b/x-pack/plugins/security_solution/public/cases/links.ts @@ -6,8 +6,8 @@ */ import { getCasesDeepLinks } from '@kbn/cases-plugin/public'; -import { CASES_PATH, SecurityPageName } from '../../common/constants'; -import { FEATURE, LinkItem } from '../common/links/types'; +import { CASES_FEATURE_ID, CASES_PATH, SecurityPageName } from '../../common/constants'; +import { LinkItem } from '../common/links/types'; export const getCasesLinkItems = (): LinkItem => { const casesLinks = getCasesDeepLinks({ @@ -16,15 +16,17 @@ export const getCasesLinkItems = (): LinkItem => { [SecurityPageName.case]: { globalNavEnabled: true, globalNavOrder: 9006, - features: [FEATURE.casesRead], + capabilities: [`${CASES_FEATURE_ID}.read_cases`], }, [SecurityPageName.caseConfigure]: { - features: [FEATURE.casesCrud], + capabilities: [`${CASES_FEATURE_ID}.crud_cases`], licenseType: 'gold', + sideNavDisabled: true, hideTimeline: true, }, [SecurityPageName.caseCreate]: { - features: [FEATURE.casesCrud], + capabilities: [`${CASES_FEATURE_ID}.crud_cases`], + sideNavDisabled: true, hideTimeline: true, }, }, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.test.ts index ff7aa7581fc4b..41b62e8589854 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.test.ts @@ -7,11 +7,12 @@ import { renderHook } from '@testing-library/react-hooks'; import { SecurityPageName } from '../../../app/types'; -import { NavLinkItem } from '../../links/types'; +import { AppLinkItems } from '../../links'; import { TestProviders } from '../../mock'; import { useAppNavLinks, useAppRootNavLink } from './nav_links'; +import { NavLinkItem } from './types'; -const mockNavLinks = [ +const mockNavLinks: AppLinkItems = [ { description: 'description', id: SecurityPageName.administration, @@ -22,6 +23,10 @@ const mockNavLinks = [ links: [], path: '/path_2', title: 'title 2', + sideNavDisabled: true, + landingIcon: 'someicon', + landingImage: 'someimage', + skipUrlState: true, }, ], path: '/path', @@ -30,7 +35,7 @@ const mockNavLinks = [ ]; jest.mock('../../links', () => ({ - getNavLinkItems: () => mockNavLinks, + useAppLinks: () => mockNavLinks, })); const renderUseAppNavLinks = () => @@ -44,11 +49,47 @@ const renderUseAppRootNavLink = (id: SecurityPageName) => describe('useAppNavLinks', () => { it('should return all nav links', () => { const { result } = renderUseAppNavLinks(); - expect(result.current).toEqual(mockNavLinks); + expect(result.current).toMatchInlineSnapshot(` + Array [ + Object { + "description": "description", + "id": "administration", + "links": Array [ + Object { + "description": "description 2", + "disabled": true, + "icon": "someicon", + "id": "endpoints", + "image": "someimage", + "skipUrlState": true, + "title": "title 2", + }, + ], + "title": "title", + }, + ] + `); }); it('should return a root nav links', () => { const { result } = renderUseAppRootNavLink(SecurityPageName.administration); - expect(result.current).toEqual(mockNavLinks[0]); + expect(result.current).toMatchInlineSnapshot(` + Object { + "description": "description", + "id": "administration", + "links": Array [ + Object { + "description": "description 2", + "disabled": true, + "icon": "someicon", + "id": "endpoints", + "image": "someimage", + "skipUrlState": true, + "title": "title 2", + }, + ], + "title": "title", + } + `); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts b/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts index efdf72a1f7926..db8b5788b04d6 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts @@ -5,21 +5,35 @@ * 2.0. */ -import { useKibana } from '../../lib/kibana'; -import { useEnableExperimental } from '../../hooks/use_experimental_features'; -import { useLicense } from '../../hooks/use_license'; -import { getNavLinkItems } from '../../links'; +import { useMemo } from 'react'; +import { useAppLinks } from '../../links'; import type { SecurityPageName } from '../../../app/types'; -import type { NavLinkItem } from '../../links/types'; +import { NavLinkItem } from './types'; +import { AppLinkItems } from '../../links/types'; export const useAppNavLinks = (): NavLinkItem[] => { - const license = useLicense(); - const enableExperimental = useEnableExperimental(); - const capabilities = useKibana().services.application.capabilities; - - return getNavLinkItems({ enableExperimental, license, capabilities }); + const appLinks = useAppLinks(); + const navLinks = useMemo(() => formatNavLinkItems(appLinks), [appLinks]); + return navLinks; }; export const useAppRootNavLink = (linkId: SecurityPageName): NavLinkItem | undefined => { return useAppNavLinks().find(({ id }) => id === linkId); }; + +const formatNavLinkItems = (appLinks: AppLinkItems): NavLinkItem[] => + appLinks.map((link) => ({ + id: link.id, + title: link.title, + ...(link.categories != null ? { categories: link.categories } : {}), + ...(link.description != null ? { description: link.description } : {}), + ...(link.sideNavDisabled === true ? { disabled: true } : {}), + ...(link.landingIcon != null ? { icon: link.landingIcon } : {}), + ...(link.landingImage != null ? { image: link.landingImage } : {}), + ...(link.skipUrlState != null ? { skipUrlState: link.skipUrlState } : {}), + ...(link.links && link.links.length + ? { + links: formatNavLinkItems(link.links), + } + : {}), + })); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/icons/launch.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/icons/launch.tsx new file mode 100644 index 0000000000000..de96338ef98e6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/icons/launch.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { SVGProps } from 'react'; + +export const EuiIconLaunch: React.FC> = ({ ...props }) => ( + + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/index.ts new file mode 100644 index 0000000000000..a2c866e604e16 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { SecuritySideNav } from './security_side_nav'; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx new file mode 100644 index 0000000000000..c0ebd0722f725 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { SecurityPageName } from '../../../../app/types'; +import { TestProviders } from '../../../mock'; +import { SecuritySideNav } from './security_side_nav'; +import { SolutionGroupedNavProps } from '../solution_grouped_nav/solution_grouped_nav'; +import { NavLinkItem } from '../types'; + +const manageNavLink: NavLinkItem = { + id: SecurityPageName.administration, + title: 'manage', + description: 'manage description', + categories: [{ label: 'test category', linkIds: [SecurityPageName.endpoints] }], + links: [ + { + id: SecurityPageName.endpoints, + title: 'title 2', + description: 'description 2', + }, + ], +}; +const alertsNavLink: NavLinkItem = { + id: SecurityPageName.alerts, + title: 'alerts', + description: 'alerts description', +}; + +const mockSolutionGroupedNav = jest.fn((_: SolutionGroupedNavProps) => <>); +jest.mock('../solution_grouped_nav', () => ({ + SolutionGroupedNav: (props: SolutionGroupedNavProps) => mockSolutionGroupedNav(props), +})); +const mockUseRouteSpy = jest.fn(() => [{ pageName: SecurityPageName.alerts }]); +jest.mock('../../../utils/route/use_route_spy', () => ({ + useRouteSpy: () => mockUseRouteSpy(), +})); + +const mockedUseCanSeeHostIsolationExceptionsMenu = jest.fn(); +jest.mock('../../../../management/pages/host_isolation_exceptions/view/hooks', () => ({ + useCanSeeHostIsolationExceptionsMenu: () => mockedUseCanSeeHostIsolationExceptionsMenu(), +})); +jest.mock('../../../links', () => ({ + getAncestorLinksInfo: (id: string) => [{ id }], +})); + +const mockUseAppNavLinks = jest.fn((): NavLinkItem[] => [alertsNavLink, manageNavLink]); +jest.mock('../nav_links', () => ({ + useAppNavLinks: () => mockUseAppNavLinks(), +})); +jest.mock('../../links', () => ({ + useGetSecuritySolutionLinkProps: + () => + ({ deepLinkId }: { deepLinkId: SecurityPageName }) => ({ + href: `/${deepLinkId}`, + }), +})); + +const renderNav = () => + render(, { + wrapper: TestProviders, + }); + +describe('SecuritySideNav', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render main items', () => { + mockUseAppNavLinks.mockReturnValueOnce([alertsNavLink]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith({ + selectedId: SecurityPageName.alerts, + items: [ + { + id: SecurityPageName.alerts, + label: 'alerts', + href: '/alerts', + }, + ], + footerItems: [], + }); + }); + + it('should render the loader if items are still empty', () => { + mockUseAppNavLinks.mockReturnValueOnce([]); + const result = renderNav(); + expect(result.getByTestId('sideNavLoader')).toBeInTheDocument(); + expect(mockSolutionGroupedNav).not.toHaveBeenCalled(); + }); + + it('should render with selected id', () => { + mockUseRouteSpy.mockReturnValueOnce([{ pageName: SecurityPageName.administration }]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith( + expect.objectContaining({ + selectedId: SecurityPageName.administration, + }) + ); + }); + + it('should render footer items', () => { + mockUseAppNavLinks.mockReturnValueOnce([manageNavLink]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith( + expect.objectContaining({ + items: [], + selectedId: SecurityPageName.alerts, + footerItems: [ + { + id: SecurityPageName.administration, + label: 'manage', + href: '/administration', + categories: manageNavLink.categories, + items: [ + { + id: SecurityPageName.endpoints, + label: 'title 2', + description: 'description 2', + href: '/endpoints', + }, + ], + }, + ], + }) + ); + }); + + it('should not render disabled items', () => { + mockUseAppNavLinks.mockReturnValueOnce([ + { ...alertsNavLink, disabled: true }, + { + ...manageNavLink, + links: [ + { + id: SecurityPageName.endpoints, + title: 'title 2', + description: 'description 2', + disabled: true, + }, + ], + }, + ]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith( + expect.objectContaining({ + items: [], + selectedId: SecurityPageName.alerts, + footerItems: [ + { + id: SecurityPageName.administration, + label: 'manage', + href: '/administration', + categories: manageNavLink.categories, + items: [], + }, + ], + }) + ); + }); + + it('should render hostIsolationExceptionsLink when useCanSeeHostIsolationExceptionsMenu is true', () => { + mockedUseCanSeeHostIsolationExceptionsMenu.mockReturnValue(true); + const hostIsolationExceptionsLink = { + id: SecurityPageName.hostIsolationExceptions, + title: 'test hostIsolationExceptions', + description: 'test description hostIsolationExceptions', + }; + + mockUseAppNavLinks.mockReturnValueOnce([ + { + ...manageNavLink, + links: [hostIsolationExceptionsLink], + }, + ]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith( + expect.objectContaining({ + items: [], + selectedId: SecurityPageName.alerts, + footerItems: [ + { + id: SecurityPageName.administration, + label: 'manage', + href: '/administration', + categories: manageNavLink.categories, + items: [ + { + id: hostIsolationExceptionsLink.id, + label: hostIsolationExceptionsLink.title, + description: hostIsolationExceptionsLink.description, + href: '/host_isolation_exceptions', + }, + ], + }, + ], + }) + ); + }); + + it('should not render hostIsolationExceptionsLink when useCanSeeHostIsolationExceptionsMenu is false', () => { + mockedUseCanSeeHostIsolationExceptionsMenu.mockReturnValue(false); + const hostIsolationExceptionsLink = { + id: SecurityPageName.hostIsolationExceptions, + title: 'test hostIsolationExceptions', + description: 'test description hostIsolationExceptions', + }; + + mockUseAppNavLinks.mockReturnValueOnce([ + { + ...manageNavLink, + links: [hostIsolationExceptionsLink], + }, + ]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith( + expect.objectContaining({ + items: [], + selectedId: SecurityPageName.alerts, + footerItems: [ + { + id: SecurityPageName.administration, + label: 'manage', + href: '/administration', + categories: manageNavLink.categories, + items: [], + }, + ], + }) + ); + }); + + it('should render custom item', () => { + mockUseAppNavLinks.mockReturnValueOnce([ + { id: SecurityPageName.landing, title: 'get started' }, + ]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith( + expect.objectContaining({ + items: [], + selectedId: SecurityPageName.alerts, + footerItems: [ + { + id: SecurityPageName.landing, + render: expect.any(Function), + }, + ], + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx new file mode 100644 index 0000000000000..b9173270e381e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useCallback } from 'react'; +import { EuiHorizontalRule, EuiListGroupItem, EuiLoadingSpinner } from '@elastic/eui'; +import { SecurityPageName } from '../../../../app/types'; +import { getAncestorLinksInfo } from '../../../links'; +import { useRouteSpy } from '../../../utils/route/use_route_spy'; +import { SecuritySolutionLinkAnchor, useGetSecuritySolutionLinkProps } from '../../links'; +import { useAppNavLinks } from '../nav_links'; +import { SolutionGroupedNav } from '../solution_grouped_nav'; +import { CustomSideNavItem, DefaultSideNavItem, SideNavItem } from '../solution_grouped_nav/types'; +import { NavLinkItem } from '../types'; +import { EuiIconLaunch } from './icons/launch'; +import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks'; + +const isFooterNavItem = (id: SecurityPageName) => + id === SecurityPageName.landing || id === SecurityPageName.administration; + +type FormatSideNavItems = (navItems: NavLinkItem) => SideNavItem; + +/** + * Renders the navigation item for "Get Started" custom link + */ +const GetStartedCustomLinkComponent: React.FC<{ + isSelected: boolean; + title: string; +}> = ({ isSelected, title }) => ( + + + + +); +const GetStartedCustomLink = React.memo(GetStartedCustomLinkComponent); + +/** + * Returns a function to format generic `NavLinkItem` array to the `SideNavItem` type + */ +const useFormatSideNavItem = (): FormatSideNavItems => { + const hideHostIsolationExceptions = !useCanSeeHostIsolationExceptionsMenu(); + const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); // adds href and onClick props + + const formatSideNavItem: FormatSideNavItems = useCallback( + (navLinkItem) => { + const formatDefaultItem = (navItem: NavLinkItem): DefaultSideNavItem => ({ + id: navItem.id, + label: navItem.title, + ...getSecuritySolutionLinkProps({ deepLinkId: navItem.id }), + ...(navItem.categories && navItem.categories.length > 0 + ? { categories: navItem.categories } + : {}), + ...(navItem.links && navItem.links.length > 0 + ? { + items: navItem.links + .filter( + (link) => + !link.disabled && + !( + link.id === SecurityPageName.hostIsolationExceptions && + hideHostIsolationExceptions + ) + ) + .map((panelNavItem) => ({ + id: panelNavItem.id, + label: panelNavItem.title, + description: panelNavItem.description, + ...getSecuritySolutionLinkProps({ deepLinkId: panelNavItem.id }), + })), + } + : {}), + }); + + const formatGetStartedItem = (navItem: NavLinkItem): CustomSideNavItem => ({ + id: navItem.id, + render: (isSelected) => ( + + ), + }); + + if (navLinkItem.id === SecurityPageName.landing) { + return formatGetStartedItem(navLinkItem); + } + return formatDefaultItem(navLinkItem); + }, + [getSecuritySolutionLinkProps, hideHostIsolationExceptions] + ); + + return formatSideNavItem; +}; + +/** + * Returns the formatted `items` and `footerItems` to be rendered in the navigation + */ +const useSideNavItems = () => { + const appNavLinks = useAppNavLinks(); + const formatSideNavItem = useFormatSideNavItem(); + + const sideNavItems = useMemo(() => { + const mainNavItems: SideNavItem[] = []; + const footerNavItems: SideNavItem[] = []; + appNavLinks.forEach((appNavLink) => { + if (appNavLink.disabled) { + return; + } + + if (isFooterNavItem(appNavLink.id)) { + footerNavItems.push(formatSideNavItem(appNavLink)); + } else { + mainNavItems.push(formatSideNavItem(appNavLink)); + } + }); + return [mainNavItems, footerNavItems]; + }, [appNavLinks, formatSideNavItem]); + + return sideNavItems; +}; + +const useSelectedId = (): SecurityPageName => { + const [{ pageName }] = useRouteSpy(); + const selectedId = useMemo(() => { + const [rootLinkInfo] = getAncestorLinksInfo(pageName as SecurityPageName); + return rootLinkInfo?.id ?? ''; + }, [pageName]); + + return selectedId; +}; + +/** + * Main security navigation component. + * It takes the links to render from the generic application `links` configs. + */ +export const SecuritySideNav: React.FC = () => { + const [items, footerItems] = useSideNavItems(); + const selectedId = useSelectedId(); + + if (items.length === 0 && footerItems.length === 0) { + return ; + } + + return ; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.test.tsx index f141264bd97e4..e41b566bbc7c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.test.tsx @@ -9,15 +9,15 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { SecurityPageName } from '../../../../app/types'; import { TestProviders } from '../../../mock'; -import { NavItem } from './solution_grouped_nav_item'; import { SolutionGroupedNav, SolutionGroupedNavProps } from './solution_grouped_nav'; +import { SideNavItem } from './types'; const mockUseShowTimeline = jest.fn((): [boolean] => [false]); jest.mock('../../../utils/timeline/use_show_timeline', () => ({ useShowTimeline: () => mockUseShowTimeline(), })); -const mockItems: NavItem[] = [ +const mockItems: SideNavItem[] = [ { id: SecurityPageName.dashboardsLanding, label: 'Dashboards', diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx index fcfcc9d6b1b4b..073723b80f518 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx @@ -15,22 +15,38 @@ import { } from '@elastic/eui'; import classNames from 'classnames'; -import { SolutionGroupedNavPanel } from './solution_grouped_nav_panel'; +import { SolutionNavPanel } from './solution_grouped_nav_panel'; import { EuiListGroupItemStyled } from './solution_grouped_nav.styles'; -import { - isCustomNavItem, - isDefaultNavItem, - NavItem, - PortalNavItem, -} from './solution_grouped_nav_item'; +import { DefaultSideNavItem, SideNavItem, isCustomItem, isDefaultItem } from './types'; import { EuiIconSpaces } from './icons/spaces'; +import type { LinkCategories } from '../../../links'; export interface SolutionGroupedNavProps { - items: NavItem[]; + items: SideNavItem[]; + selectedId: string; + footerItems?: SideNavItem[]; +} +export interface SolutionNavItemsProps { + items: SideNavItem[]; selectedId: string; - footerItems?: NavItem[]; + activePanelNavId: ActivePanelNav; + isMobileSize: boolean; + navItemsById: NavItemsById; + onOpenPanelNav: (id: string) => void; } -type ActivePortalNav = string | null; +export interface SolutionNavItemProps { + item: SideNavItem; + isSelected: boolean; + isActive: boolean; + hasPanelNav: boolean; + onOpenPanelNav: (id: string) => void; +} + +type ActivePanelNav = string | null; +type NavItemsById = Record< + string, + { title: string; panelItems: DefaultSideNavItem[]; categories?: LinkCategories } +>; export const SolutionGroupedNavComponent: React.FC = ({ items, @@ -39,41 +55,40 @@ export const SolutionGroupedNavComponent: React.FC = ({ }) => { const isMobileSize = useIsWithinBreakpoints(['xs', 's']); - const [activePortalNavId, setActivePortalNavId] = useState(null); - const activePortalNavIdRef = useRef(null); + const [activePanelNavId, setActivePanelNavId] = useState(null); + const activePanelNavIdRef = useRef(null); - const openPortalNav = (navId: string) => { - activePortalNavIdRef.current = navId; - setActivePortalNavId(navId); + const openPanelNav = (id: string) => { + activePanelNavIdRef.current = id; + setActivePanelNavId(id); }; - const closePortalNav = () => { - activePortalNavIdRef.current = null; - setActivePortalNavId(null); - }; + const onClosePanelNav = useCallback(() => { + activePanelNavIdRef.current = null; + setActivePanelNavId(null); + }, []); - const onClosePortalNav = useCallback(() => { - const currentPortalNavId = activePortalNavIdRef.current; + const onOutsidePanelClick = useCallback(() => { + const currentPanelNavId = activePanelNavIdRef.current; setTimeout(() => { // This event is triggered on outside click. // Closing the side nav at the end of event loop to make sure it - // closes also if the active "nav group" button has been clicked (toggle), - // but it does not close if any some other "nav group" open button has been clicked. - if (activePortalNavIdRef.current === currentPortalNavId) { - closePortalNav(); + // closes also if the active panel button has been clicked (toggle), + // but it does not close if any any other panel open button has been clicked. + if (activePanelNavIdRef.current === currentPanelNavId) { + onClosePanelNav(); } }); - }, []); + }, [onClosePanelNav]); - const navItemsById = useMemo( + const navItemsById = useMemo( () => - [...items, ...footerItems].reduce< - Record - >((acc, navItem) => { - if (isDefaultNavItem(navItem) && navItem.items && navItem.items.length > 0) { + [...items, ...footerItems].reduce((acc, navItem) => { + if (isDefaultItem(navItem) && navItem.items && navItem.items.length > 0) { acc[navItem.id] = { title: navItem.label, - subItems: navItem.items, + panelItems: navItem.items, + categories: navItem.categories, }; } return acc; @@ -82,67 +97,20 @@ export const SolutionGroupedNavComponent: React.FC = ({ ); const portalNav = useMemo(() => { - if (activePortalNavId == null || !navItemsById[activePortalNavId]) { + if (activePanelNavId == null || !navItemsById[activePanelNavId]) { return null; } - const { subItems, title } = navItemsById[activePortalNavId]; - return ; - }, [activePortalNavId, navItemsById, onClosePortalNav]); - - const renderNavItem = useCallback( - (navItem: NavItem) => { - if (isCustomNavItem(navItem)) { - return {navItem.render()}; - } - const { id, href, label, onClick } = navItem; - const isActive = activePortalNavId === id; - const isCurrentNav = selectedId === id; - - const itemClassNames = classNames('solutionGroupedNavItem', { - 'solutionGroupedNavItem--isActive': isActive, - 'solutionGroupedNavItem--isPrimary': isCurrentNav, - }); - const buttonClassNames = classNames('solutionGroupedNavItemButton'); - - return ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - - { - ev.preventDefault(); - ev.stopPropagation(); - openPortalNav(id); - }, - iconType: EuiIconSpaces, - iconSize: 'm', - 'aria-label': 'Toggle group nav', - 'data-test-subj': `groupedNavItemButton-${id}`, - alwaysShow: true, - }, - } - : {})} - /> - - ); - }, - [activePortalNavId, isMobileSize, navItemsById, selectedId] - ); + const { panelItems, title, categories } = navItemsById[activePanelNavId]; + return ( + + ); + }, [activePanelNavId, navItemsById, onClosePanelNav, onOutsidePanelClick]); return ( <> @@ -150,10 +118,28 @@ export const SolutionGroupedNavComponent: React.FC = ({ - {items.map(renderNavItem)} + + + - {footerItems.map(renderNavItem)} + + + @@ -163,5 +149,84 @@ export const SolutionGroupedNavComponent: React.FC = ({ ); }; - export const SolutionGroupedNav = React.memo(SolutionGroupedNavComponent); + +const SolutionNavItems: React.FC = ({ + items, + selectedId, + activePanelNavId, + isMobileSize, + navItemsById, + onOpenPanelNav, +}) => ( + <> + {items.map((item) => ( + + ))} + +); + +const SolutionNavItemComponent: React.FC = ({ + item, + isSelected, + isActive, + hasPanelNav, + onOpenPanelNav, +}) => { + if (isCustomItem(item)) { + return {item.render(isSelected)}; + } + const { id, href, label, onClick } = item; + + const itemClassNames = classNames('solutionGroupedNavItem', { + 'solutionGroupedNavItem--isActive': isActive, + 'solutionGroupedNavItem--isPrimary': isSelected, + }); + const buttonClassNames = classNames('solutionGroupedNavItemButton'); + + const onButtonClick: React.MouseEventHandler = (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + onOpenPanelNav(id); + }; + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + + + + ); +}; +const SolutionNavItem = React.memo(SolutionNavItemComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_item.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_item.tsx deleted file mode 100644 index df7e08ad46f95..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_item.tsx +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { useGetSecuritySolutionLinkProps } from '../../links'; -import { SecurityPageName } from '../../../../../common/constants'; - -export type NavItemCategories = Array<{ label: string; itemIds: string[] }>; -export interface DefaultNavItem { - id: string; - label: string; - href: string; - onClick?: React.MouseEventHandler; - items?: PortalNavItem[]; - categories?: NavItemCategories; -} - -export interface CustomNavItem { - id: string; - render: () => React.ReactNode; -} - -export type NavItem = DefaultNavItem | CustomNavItem; - -export interface PortalNavItem { - id: string; - label: string; - href: string; - onClick?: React.MouseEventHandler; - description?: string; -} - -export const isCustomNavItem = (navItem: NavItem): navItem is CustomNavItem => 'render' in navItem; -export const isDefaultNavItem = (navItem: NavItem): navItem is DefaultNavItem => - !isCustomNavItem(navItem); - -export const useNavItems: () => NavItem[] = () => { - const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); - return [ - { - id: SecurityPageName.dashboardsLanding, - label: 'Dashboards', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.dashboardsLanding }), - items: [ - { - id: 'overview', - label: 'Overview', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.overview }), - }, - { - id: 'detection_response', - label: 'Detection & Response', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.detectionAndResponse }), - }, - // TODO: add the cloudPostureFindings to the config here - // { - // id: SecurityPageName.cloudPostureFindings, - // label: 'Cloud Posture Findings', - // description: 'The description goes here', - // ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.cloudPostureFindings }), - // }, - ], - }, - { - id: SecurityPageName.alerts, - label: 'Alerts', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.alerts }), - }, - { - id: SecurityPageName.timelines, - label: 'Timelines', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.timelines }), - }, - { - id: SecurityPageName.case, - label: 'Cases', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.case }), - }, - { - id: SecurityPageName.threatHuntingLanding, - label: 'Threat Hunting', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.threatHuntingLanding }), - items: [ - { - id: SecurityPageName.hosts, - label: 'Hosts', - description: - 'Computer or other device that communicates with other hosts on a network. Hosts on a network include clients and servers -- that send or receive data, services or applications.', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.hosts }), - }, - { - id: SecurityPageName.network, - label: 'Network', - description: - 'The action or process of interacting with others to exchange information and develop professional or social contacts.', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.network }), - }, - { - id: SecurityPageName.users, - label: 'Users', - description: 'Sudo commands dashboard from the Logs System integration.', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.users }), - }, - ], - }, - // TODO: implement footer and move management - { - id: SecurityPageName.administration, - label: 'Manage', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.administration }), - categories: [ - { label: 'SIEM', itemIds: [SecurityPageName.rules, SecurityPageName.exceptions] }, - { - label: 'ENDPOINTS', - itemIds: [ - SecurityPageName.endpoints, - SecurityPageName.policies, - SecurityPageName.trustedApps, - SecurityPageName.eventFilters, - SecurityPageName.blocklist, - SecurityPageName.hostIsolationExceptions, - ], - }, - ], - items: [ - { - id: SecurityPageName.rules, - label: 'Rules', - description: 'The description here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.rules }), - }, - { - id: SecurityPageName.exceptions, - label: 'Exceptions', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.exceptions }), - }, - { - id: SecurityPageName.endpoints, - label: 'Endpoints', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.endpoints }), - }, - { - id: SecurityPageName.policies, - label: 'Policies', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.policies }), - }, - { - id: SecurityPageName.trustedApps, - label: 'Trusted applications', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.trustedApps }), - }, - { - id: SecurityPageName.eventFilters, - label: 'Event filters', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.eventFilters }), - }, - { - id: SecurityPageName.blocklist, - label: 'Blocklist', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.blocklist }), - }, - { - id: SecurityPageName.hostIsolationExceptions, - label: 'Host Isolation IP exceptions', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.hostIsolationExceptions }), - }, - ], - }, - ]; -}; - -export const useFooterNavItems: () => NavItem[] = () => { - // TODO: implement footer items - return []; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.test.tsx index 93d46c35d6bed..8215d9c0b9f40 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.test.tsx @@ -9,18 +9,15 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { SecurityPageName } from '../../../../app/types'; import { TestProviders } from '../../../mock'; -import { PortalNavItem } from './solution_grouped_nav_item'; -import { - SolutionGroupedNavPanel, - SolutionGroupedNavPanelProps, -} from './solution_grouped_nav_panel'; +import { SolutionNavPanel, SolutionNavPanelProps } from './solution_grouped_nav_panel'; +import { DefaultSideNavItem } from './types'; const mockUseShowTimeline = jest.fn((): [boolean] => [false]); jest.mock('../../../utils/timeline/use_show_timeline', () => ({ useShowTimeline: () => mockUseShowTimeline(), })); -const mockItems: PortalNavItem[] = [ +const mockItems: DefaultSideNavItem[] = [ { id: SecurityPageName.hosts, label: 'Hosts', @@ -37,14 +34,16 @@ const mockItems: PortalNavItem[] = [ const PANEL_TITLE = 'test title'; const mockOnClose = jest.fn(); -const renderNavPanel = (props: Partial = {}) => +const mockOnOutsideClick = jest.fn(); +const renderNavPanel = (props: Partial = {}) => render( <>
- , @@ -112,7 +111,7 @@ describe('SolutionGroupedNav', () => { const result = renderNavPanel(); result.getByTestId('outsideClickDummy').click(); waitFor(() => { - expect(mockOnClose).toHaveBeenCalled(); + expect(mockOnOutsideClick).toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx index c1615a97264eb..a418f666d2782 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx @@ -13,8 +13,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiFocusTrap, + EuiHorizontalRule, EuiOutsideClickDetector, EuiPortal, + EuiSpacer, EuiTitle, EuiWindowEvent, keys, @@ -22,18 +24,39 @@ import { } from '@elastic/eui'; import classNames from 'classnames'; import { EuiPanelStyled } from './solution_grouped_nav_panel.styles'; -import { PortalNavItem } from './solution_grouped_nav_item'; import { useShowTimeline } from '../../../utils/timeline/use_show_timeline'; +import type { DefaultSideNavItem } from './types'; +import type { LinkCategories } from '../../../links/types'; -export interface SolutionGroupedNavPanelProps { +export interface SolutionNavPanelProps { onClose: () => void; + onOutsideClick: () => void; title: string; - items: PortalNavItem[]; + items: DefaultSideNavItem[]; + categories?: LinkCategories; +} +export interface SolutionNavPanelCategoriesProps { + categories: LinkCategories; + items: DefaultSideNavItem[]; + onClose: () => void; +} +export interface SolutionNavPanelItemsProps { + items: DefaultSideNavItem[]; + onClose: () => void; +} +export interface SolutionNavPanelItemProps { + item: DefaultSideNavItem; + onClose: () => void; } -const SolutionGroupedNavPanelComponent: React.FC = ({ +/** + * Renders the side navigation panel for secondary links + */ +const SolutionNavPanelComponent: React.FC = ({ onClose, + onOutsideClick, title, + categories, items, }) => { const [hasTimelineBar] = useShowTimeline(); @@ -41,9 +64,7 @@ const SolutionGroupedNavPanelComponent: React.FC = const isTimelineVisible = hasTimelineBar && isLargerBreakpoint; const panelClasses = classNames('eui-yScroll'); - /** - * ESC key closes SideNav - */ + // ESC key closes PanelNav const onKeyDown = useCallback( (ev: KeyboardEvent) => { if (ev.key === keys.ESCAPE) { @@ -58,7 +79,7 @@ const SolutionGroupedNavPanelComponent: React.FC = - onClose()}> + = - {items.map(({ id, href, onClick, label, description }: PortalNavItem) => ( - - - { - onClose(); - if (onClick) { - onClick(ev); - } - }} - > - {label} - - - {description} - - ))} + {categories ? ( + + ) : ( + + )} @@ -105,5 +116,61 @@ const SolutionGroupedNavPanelComponent: React.FC = ); }; +export const SolutionNavPanel = React.memo(SolutionNavPanelComponent); + +const SolutionNavPanelCategories: React.FC = ({ + categories, + items, + onClose, +}) => { + const itemsMap = new Map(items.map((item) => [item.id, item])); + + return ( + <> + {categories.map(({ label, linkIds }) => { + const links = linkIds.reduce((acc, linkId) => { + const link = itemsMap.get(linkId); + if (link) { + acc.push(link); + } + return acc; + }, []); + + return ( + + +

{label}

+
+ + + +
+ ); + })} + + ); +}; -export const SolutionGroupedNavPanel = React.memo(SolutionGroupedNavPanelComponent); +const SolutionNavPanelItems: React.FC = ({ items, onClose }) => ( + <> + {items.map(({ id, href, onClick, label, description }) => ( + + + { + onClose(); + if (onClick) { + onClick(ev); + } + }} + > + {label} + + + {description} + + ))} + +); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts new file mode 100644 index 0000000000000..a16bad9126d09 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { SecurityPageName } from '../../../../app/types'; +import type { LinkCategories } from '../../../links/types'; + +export interface DefaultSideNavItem { + id: SecurityPageName; + label: string; + href: string; + onClick?: React.MouseEventHandler; + description?: string; + items?: DefaultSideNavItem[]; + categories?: LinkCategories; +} + +export interface CustomSideNavItem { + id: string; + render: (isSelected: boolean) => React.ReactNode; +} + +export type SideNavItem = DefaultSideNavItem | CustomSideNavItem; + +export const isCustomItem = (navItem: SideNavItem): navItem is CustomSideNavItem => + 'render' in navItem; +export const isDefaultItem = (navItem: SideNavItem): navItem is DefaultSideNavItem => + !isCustomItem(navItem); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts index 91edd1feea2da..85d504165484b 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts @@ -5,10 +5,12 @@ * 2.0. */ +import { IconType } from '@elastic/eui'; import { UrlStateType } from '../url_state/constants'; import { SecurityPageName } from '../../../app/types'; import { UrlState } from '../url_state/types'; import { SiemRouteType } from '../../utils/route/types'; +import { LinkCategories } from '../../links'; export interface TabNavigationComponentProps { pageName: string; @@ -76,10 +78,14 @@ export type GetUrlForApp = ( ) => string; export type NavigateToUrl = (url: string) => void; - -export interface NavigationCategory { - label: string; - linkIds: readonly SecurityPageName[]; +export interface NavLinkItem { + categories?: LinkCategories; + description?: string; + disabled?: boolean; + icon?: IconType; + id: SecurityPageName; + links?: NavLinkItem[]; + image?: string; + title: string; + skipUrlState?: boolean; } - -export type NavigationCategories = Readonly; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap index cadb9057ccbcc..d50b07ca56089 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap @@ -14,7 +14,7 @@ Object { "href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "get_started", "isSelected": false, - "name": "Getting started", + "name": "Get started", "onClick": [Function], }, Object { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx index 1dbcf929ed81f..1123fd50a53e6 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx @@ -11,9 +11,8 @@ import { i18n } from '@kbn/i18n'; import { KibanaPageTemplateProps } from '@kbn/shared-ux-components'; import { PrimaryNavigationProps } from './types'; import { usePrimaryNavigationItems } from './use_navigation_items'; -import { SolutionGroupedNav } from '../solution_grouped_nav'; -import { useNavItems } from '../solution_grouped_nav/solution_grouped_nav_item'; import { useIsGroupedNavigationEnabled } from '../helpers'; +import { SecuritySideNav } from '../security_side_nav'; const translatedNavTitle = i18n.translate('xpack.securitySolution.navigation.mainLabel', { defaultMessage: 'Security', @@ -48,7 +47,6 @@ export const usePrimaryNavigation = ({ // we do need navTabs in case the selectedTabId appears after initial load (ex. checking permissions for anomalies) }, [pageName, navTabs, mapLocationToTab, selectedTabId]); - const navLinkItems = useNavItems(); const navItems = usePrimaryNavigationItems({ navTabs, selectedTabId, @@ -65,7 +63,7 @@ export const usePrimaryNavigation = ({ icon: 'logoSecurity', ...(isGroupedNavigationEnabled ? { - children: , + children: , closeFlyoutButtonPosition: 'inside', } : { items: navItems }), diff --git a/x-pack/plugins/security_solution/public/common/links/app_links.ts b/x-pack/plugins/security_solution/public/common/links/app_links.ts index 1a78444012334..45a7ed373222f 100644 --- a/x-pack/plugins/security_solution/public/common/links/app_links.ts +++ b/x-pack/plugins/security_solution/public/common/links/app_links.ts @@ -4,48 +4,30 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { i18n } from '@kbn/i18n'; -import { SecurityPageName, THREAT_HUNTING_PATH } from '../../../common/constants'; -import { THREAT_HUNTING } from '../../app/translations'; -import { FEATURE, LinkItem, UserPermissions } from './types'; -import { links as hostsLinks } from '../../hosts/links'; +import { CoreStart } from '@kbn/core/public'; +import { AppLinkItems } from './types'; import { links as detectionLinks } from '../../detections/links'; -import { links as networkLinks } from '../../network/links'; -import { links as usersLinks } from '../../users/links'; import { links as timelinesLinks } from '../../timelines/links'; import { getCasesLinkItems } from '../../cases/links'; -import { links as managementLinks } from '../../management/links'; -import { gettingStartedLinks, dashboardsLandingLinks } from '../../overview/links'; +import { getManagementLinkItems } from '../../management/links'; +import { dashboardsLandingLinks, threatHuntingLandingLinks } from '../../landing_pages/links'; +import { gettingStartedLinks } from '../../overview/links'; +import { StartPlugins } from '../../types'; -export const appLinks: Readonly = Object.freeze([ - gettingStartedLinks, - dashboardsLandingLinks, - detectionLinks, - { - id: SecurityPageName.threatHuntingLanding, - title: THREAT_HUNTING, - path: THREAT_HUNTING_PATH, - globalNavEnabled: false, - features: [FEATURE.general], - globalSearchKeywords: [ - i18n.translate('xpack.securitySolution.appLinks.threatHunting', { - defaultMessage: 'Threat hunting', - }), - ], - links: [hostsLinks, networkLinks, usersLinks], - skipUrlState: true, - hideTimeline: true, - }, - timelinesLinks, - getCasesLinkItems(), - managementLinks, -]); +export const getAppLinks = async ( + core: CoreStart, + plugins: StartPlugins +): Promise => { + const managementLinks = await getManagementLinkItems(core, plugins); + const casesLinks = getCasesLinkItems(); -export const getAppLinks = async ({ - enableExperimental, - license, - capabilities, -}: UserPermissions) => { - // OLM team, implement async behavior here - return appLinks; + return Object.freeze([ + dashboardsLandingLinks, + detectionLinks, + timelinesLinks, + casesLinks, + threatHuntingLandingLinks, + gettingStartedLinks, + managementLinks, + ]); }; diff --git a/x-pack/plugins/security_solution/public/common/links/index.tsx b/x-pack/plugins/security_solution/public/common/links/index.tsx index 6d8e99cd416d2..e4e4de0b49430 100644 --- a/x-pack/plugins/security_solution/public/common/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/links/index.tsx @@ -6,3 +6,4 @@ */ export * from './links'; +export * from './types'; diff --git a/x-pack/plugins/security_solution/public/common/links/links.test.ts b/x-pack/plugins/security_solution/public/common/links/links.test.ts index b68ae3d863de3..896f9357077c8 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.test.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.test.ts @@ -5,399 +5,223 @@ * 2.0. */ +import { CASES_FEATURE_ID, SecurityPageName, SERVER_APP_ID } from '../../../common/constants'; +import { Capabilities } from '@kbn/core/types'; +import { mockGlobalState, TestProviders } from '../mock'; +import { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types'; +import { AppLinkItems } from './types'; +import { act, renderHook } from '@testing-library/react-hooks'; import { + useAppLinks, getAncestorLinksInfo, - getDeepLinks, - getInitialDeepLinks, getLinkInfo, - getNavLinkItems, needsUrlState, + updateAppLinks, + excludeAppLink, } from './links'; -import { CASES_FEATURE_ID, SecurityPageName, SERVER_APP_ID } from '../../../common/constants'; -import { Capabilities } from '@kbn/core/types'; -import { AppDeepLink } from '@kbn/core/public'; -import { mockGlobalState } from '../mock'; -import { NavLinkItem } from './types'; -import { LicenseType } from '@kbn/licensing-plugin/common/types'; -import { LicenseService } from '../../../common/license'; + +const defaultAppLinks: AppLinkItems = [ + { + id: SecurityPageName.hosts, + title: 'Hosts', + path: '/hosts', + links: [ + { + id: SecurityPageName.hostsAuthentications, + title: 'Authentications', + path: `/hosts/authentications`, + }, + { + id: SecurityPageName.hostsEvents, + title: 'Events', + path: `/hosts/events`, + skipUrlState: true, + }, + ], + }, +]; const mockExperimentalDefaults = mockGlobalState.app.enableExperimental; + const mockCapabilities = { [CASES_FEATURE_ID]: { read_cases: true, crud_cases: true }, [SERVER_APP_ID]: { show: true }, } as unknown as Capabilities; -const findDeepLink = (id: string, deepLinks: AppDeepLink[]): AppDeepLink | null => - deepLinks.reduce((deepLinkFound: AppDeepLink | null, deepLink) => { - if (deepLinkFound !== null) { - return deepLinkFound; - } - if (deepLink.id === id) { - return deepLink; - } - if (deepLink.deepLinks) { - return findDeepLink(id, deepLink.deepLinks); - } - return null; - }, null); - -const findNavLink = (id: SecurityPageName, navLinks: NavLinkItem[]): NavLinkItem | null => - navLinks.reduce((deepLinkFound: NavLinkItem | null, deepLink) => { - if (deepLinkFound !== null) { - return deepLinkFound; - } - if (deepLink.id === id) { - return deepLink; - } - if (deepLink.links) { - return findNavLink(id, deepLink.links); - } - return null; - }, null); - -// remove filter once new nav is live -const allPages = Object.values(SecurityPageName).filter( - (pageName) => - pageName !== SecurityPageName.explore && - pageName !== SecurityPageName.detections && - pageName !== SecurityPageName.investigate -); -const casesPages = [ - SecurityPageName.case, - SecurityPageName.caseConfigure, - SecurityPageName.caseCreate, -]; -const featureFlagPages = [ - SecurityPageName.detectionAndResponse, - SecurityPageName.hostsAuthentications, - SecurityPageName.hostsRisk, - SecurityPageName.usersRisk, -]; -const premiumPages = [ - SecurityPageName.caseConfigure, - SecurityPageName.hostsAnomalies, - SecurityPageName.networkAnomalies, - SecurityPageName.usersAnomalies, - SecurityPageName.detectionAndResponse, - SecurityPageName.hostsRisk, - SecurityPageName.usersRisk, -]; -const nonCasesPages = allPages.reduce( - (acc: SecurityPageName[], p) => - casesPages.includes(p) || featureFlagPages.includes(p) ? acc : [p, ...acc], - [] -); - const licenseBasicMock = jest.fn().mockImplementation((arg: LicenseType) => arg === 'basic'); const licensePremiumMock = jest.fn().mockReturnValue(true); const mockLicense = { - isAtLeast: licensePremiumMock, -} as unknown as LicenseService; - -const threatHuntingLinkInfo = { - features: ['siem.show'], - globalNavEnabled: false, - globalSearchKeywords: ['Threat hunting'], - id: 'threat_hunting', - path: '/threat_hunting', - title: 'Threat Hunting', - hideTimeline: true, - skipUrlState: true, -}; + hasAtLeast: licensePremiumMock, +} as unknown as ILicense; -const hostsLinkInfo = { - globalNavEnabled: true, - globalNavOrder: 9002, - globalSearchEnabled: true, - globalSearchKeywords: ['Hosts'], - id: 'hosts', - path: '/hosts', - title: 'Hosts', - landingImage: 'test-file-stub', - description: 'A comprehensive overview of all hosts and host-related security events.', -}; +const renderUseAppLinks = () => + renderHook<{}, AppLinkItems>(() => useAppLinks(), { wrapper: TestProviders }); -describe('security app link helpers', () => { +describe('Security app links', () => { beforeEach(() => { - mockLicense.isAtLeast = licensePremiumMock; - }); - describe('getInitialDeepLinks', () => { - it('should return all pages in the app', () => { - const links = getInitialDeepLinks(); - allPages.forEach((page) => expect(findDeepLink(page, links)).toBeTruthy()); - }); - }); - describe('getDeepLinks', () => { - it('basicLicense should return only basic links', async () => { - mockLicense.isAtLeast = licenseBasicMock; + mockLicense.hasAtLeast = licensePremiumMock; - const links = await getDeepLinks({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findDeepLink(SecurityPageName.hostsAnomalies, links)).toBeFalsy(); - allPages.forEach((page) => { - if (premiumPages.includes(page)) { - return expect(findDeepLink(page, links)).toBeFalsy(); - } - if (featureFlagPages.includes(page)) { - // ignore feature flag pages - return; - } - expect(findDeepLink(page, links)).toBeTruthy(); - }); - }); - it('platinumLicense should return all links', async () => { - const links = await getDeepLinks({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities: mockCapabilities, - }); - allPages.forEach((page) => { - if (premiumPages.includes(page) && !featureFlagPages.includes(page)) { - return expect(findDeepLink(page, links)).toBeTruthy(); - } - if (featureFlagPages.includes(page)) { - // ignore feature flag pages - return; - } - expect(findDeepLink(page, links)).toBeTruthy(); - }); - }); - it('hideWhenExperimentalKey hides entry when key = true', async () => { - const links = await getDeepLinks({ - enableExperimental: { ...mockExperimentalDefaults, usersEnabled: true }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findDeepLink(SecurityPageName.hostsAuthentications, links)).toBeFalsy(); - }); - it('hideWhenExperimentalKey shows entry when key = false', async () => { - const links = await getDeepLinks({ - enableExperimental: { ...mockExperimentalDefaults, usersEnabled: false }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findDeepLink(SecurityPageName.hostsAuthentications, links)).toBeTruthy(); - }); - it('experimentalKey shows entry when key = false', async () => { - const links = await getDeepLinks({ - enableExperimental: { - ...mockExperimentalDefaults, - riskyHostsEnabled: false, - riskyUsersEnabled: false, - detectionResponseEnabled: false, - }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findDeepLink(SecurityPageName.hostsRisk, links)).toBeFalsy(); - expect(findDeepLink(SecurityPageName.usersRisk, links)).toBeFalsy(); - expect(findDeepLink(SecurityPageName.detectionAndResponse, links)).toBeFalsy(); - }); - it('experimentalKey shows entry when key = true', async () => { - const links = await getDeepLinks({ - enableExperimental: { - ...mockExperimentalDefaults, - riskyHostsEnabled: true, - riskyUsersEnabled: true, - detectionResponseEnabled: true, - }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findDeepLink(SecurityPageName.hostsRisk, links)).toBeTruthy(); - expect(findDeepLink(SecurityPageName.usersRisk, links)).toBeTruthy(); - expect(findDeepLink(SecurityPageName.detectionAndResponse, links)).toBeTruthy(); + updateAppLinks(defaultAppLinks, { + capabilities: mockCapabilities, + experimentalFeatures: mockExperimentalDefaults, + license: mockLicense, }); + }); - it('Removes siem features when siem capabilities are false', async () => { - const capabilities = { - ...mockCapabilities, - [SERVER_APP_ID]: { show: false }, - } as unknown as Capabilities; - const links = await getDeepLinks({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities, - }); - nonCasesPages.forEach((page) => { - // investigate is active for both Cases and Timelines pages - if (page === SecurityPageName.investigate) { - return expect(findDeepLink(page, links)).toBeTruthy(); - } - return expect(findDeepLink(page, links)).toBeFalsy(); - }); - casesPages.forEach((page) => expect(findDeepLink(page, links)).toBeTruthy()); - }); - it('Removes cases features when cases capabilities are false', async () => { - const capabilities = { - ...mockCapabilities, - [CASES_FEATURE_ID]: { read_cases: false, crud_cases: false }, - } as unknown as Capabilities; - const links = await getDeepLinks({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities, - }); - nonCasesPages.forEach((page) => expect(findDeepLink(page, links)).toBeTruthy()); - casesPages.forEach((page) => expect(findDeepLink(page, links)).toBeFalsy()); + describe('useAppLinks', () => { + it('should return initial appLinks', () => { + const { result } = renderUseAppLinks(); + expect(result.current).toStrictEqual(defaultAppLinks); + }); + + it('should filter not allowed links', async () => { + const { result, waitForNextUpdate } = renderUseAppLinks(); + // this link should not be excluded, the test checks all conditions are passed + const networkLinkItem = { + id: SecurityPageName.network, + title: 'Network', + path: '/network', + capabilities: [`${CASES_FEATURE_ID}.read_cases`, `${SERVER_APP_ID}.show`], + experimentalKey: 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults, + hideWhenExperimentalKey: 'flagDisabled' as unknown as keyof typeof mockExperimentalDefaults, + licenseType: 'basic' as const, + }; + + await act(async () => { + updateAppLinks( + [ + { + ...networkLinkItem, + // all its links should be filtered for all different criteria + links: [ + { + id: SecurityPageName.networkExternalAlerts, + title: 'external alerts', + path: '/external_alerts', + experimentalKey: + 'flagDisabled' as unknown as keyof typeof mockExperimentalDefaults, + }, + { + id: SecurityPageName.networkDns, + title: 'dns', + path: '/dns', + hideWhenExperimentalKey: + 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults, + }, + { + id: SecurityPageName.networkAnomalies, + title: 'Anomalies', + path: '/anomalies', + capabilities: [ + `${CASES_FEATURE_ID}.read_cases`, + `${CASES_FEATURE_ID}.write_cases`, + ], + }, + { + id: SecurityPageName.networkHttp, + title: 'Http', + path: '/http', + licenseType: 'gold', + }, + ], + }, + { + // should be excluded by license with all its links + id: SecurityPageName.hosts, + title: 'Hosts', + path: '/hosts', + licenseType: 'platinum', + links: [ + { + id: SecurityPageName.hostsEvents, + title: 'Events', + path: '/events', + }, + ], + }, + ], + { + capabilities: { + ...mockCapabilities, + [CASES_FEATURE_ID]: { read_cases: false, crud_cases: false }, + }, + experimentalFeatures: { + flagEnabled: true, + flagDisabled: false, + } as unknown as typeof mockExperimentalDefaults, + license: { hasAtLeast: licenseBasicMock } as unknown as ILicense, + } + ); + await waitForNextUpdate(); + }); + + expect(result.current).toStrictEqual([networkLinkItem]); }); }); - describe('getNavLinkItems', () => { - it('basicLicense should return only basic links', () => { - mockLicense.isAtLeast = licenseBasicMock; - const links = getNavLinkItems({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findNavLink(SecurityPageName.hostsAnomalies, links)).toBeFalsy(); - allPages.forEach((page) => { - if (premiumPages.includes(page)) { - return expect(findNavLink(page, links)).toBeFalsy(); - } - if (featureFlagPages.includes(page)) { - // ignore feature flag pages - return; - } - expect(findNavLink(page, links)).toBeTruthy(); - }); - }); - it('platinumLicense should return all links', () => { - const links = getNavLinkItems({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities: mockCapabilities, - }); - allPages.forEach((page) => { - if (premiumPages.includes(page) && !featureFlagPages.includes(page)) { - return expect(findNavLink(page, links)).toBeTruthy(); - } - if (featureFlagPages.includes(page)) { - // ignore feature flag pages - return; - } - expect(findNavLink(page, links)).toBeTruthy(); - }); - }); - it('hideWhenExperimentalKey hides entry when key = true', () => { - const links = getNavLinkItems({ - enableExperimental: { ...mockExperimentalDefaults, usersEnabled: true }, - license: mockLicense, - capabilities: mockCapabilities, + describe('excludeAppLink', () => { + it('should exclude link from app links', async () => { + const { result, waitForNextUpdate } = renderUseAppLinks(); + await act(async () => { + excludeAppLink(SecurityPageName.hostsEvents); + await waitForNextUpdate(); }); - expect(findNavLink(SecurityPageName.hostsAuthentications, links)).toBeFalsy(); - }); - it('hideWhenExperimentalKey shows entry when key = false', () => { - const links = getNavLinkItems({ - enableExperimental: { ...mockExperimentalDefaults, usersEnabled: false }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findNavLink(SecurityPageName.hostsAuthentications, links)).toBeTruthy(); - }); - it('experimentalKey shows entry when key = false', () => { - const links = getNavLinkItems({ - enableExperimental: { - ...mockExperimentalDefaults, - riskyHostsEnabled: false, - riskyUsersEnabled: false, - detectionResponseEnabled: false, - }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findNavLink(SecurityPageName.hostsRisk, links)).toBeFalsy(); - expect(findNavLink(SecurityPageName.usersRisk, links)).toBeFalsy(); - expect(findNavLink(SecurityPageName.detectionAndResponse, links)).toBeFalsy(); - }); - it('experimentalKey shows entry when key = true', () => { - const links = getNavLinkItems({ - enableExperimental: { - ...mockExperimentalDefaults, - riskyHostsEnabled: true, - riskyUsersEnabled: true, - detectionResponseEnabled: true, + expect(result.current).toStrictEqual([ + { + id: SecurityPageName.hosts, + title: 'Hosts', + path: '/hosts', + links: [ + { + id: SecurityPageName.hostsAuthentications, + title: 'Authentications', + path: `/hosts/authentications`, + }, + ], }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findNavLink(SecurityPageName.hostsRisk, links)).toBeTruthy(); - expect(findNavLink(SecurityPageName.usersRisk, links)).toBeTruthy(); - expect(findNavLink(SecurityPageName.detectionAndResponse, links)).toBeTruthy(); - }); - - it('Removes siem features when siem capabilities are false', () => { - const capabilities = { - ...mockCapabilities, - [SERVER_APP_ID]: { show: false }, - } as unknown as Capabilities; - const links = getNavLinkItems({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities, - }); - nonCasesPages.forEach((page) => { - // investigate is active for both Cases and Timelines pages - if (page === SecurityPageName.investigate) { - return expect(findNavLink(page, links)).toBeTruthy(); - } - return expect(findNavLink(page, links)).toBeFalsy(); - }); - casesPages.forEach((page) => expect(findNavLink(page, links)).toBeTruthy()); - }); - it('Removes cases features when cases capabilities are false', () => { - const capabilities = { - ...mockCapabilities, - [CASES_FEATURE_ID]: { read_cases: false, crud_cases: false }, - } as unknown as Capabilities; - const links = getNavLinkItems({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities, - }); - nonCasesPages.forEach((page) => expect(findNavLink(page, links)).toBeTruthy()); - casesPages.forEach((page) => expect(findNavLink(page, links)).toBeFalsy()); + ]); }); }); describe('getAncestorLinksInfo', () => { - it('finds flattened links for hosts', () => { - const hierarchy = getAncestorLinksInfo(SecurityPageName.hosts); - expect(hierarchy).toEqual([threatHuntingLinkInfo, hostsLinkInfo]); - }); - it('finds flattened links for uncommonProcesses', () => { - const hierarchy = getAncestorLinksInfo(SecurityPageName.uncommonProcesses); - expect(hierarchy).toEqual([ - threatHuntingLinkInfo, - hostsLinkInfo, + it('should find ancestors flattened links', () => { + const hierarchy = getAncestorLinksInfo(SecurityPageName.hostsEvents); + expect(hierarchy).toStrictEqual([ { - id: 'uncommon_processes', - path: '/hosts/uncommonProcesses', - title: 'Uncommon Processes', + id: SecurityPageName.hosts, + path: '/hosts', + title: 'Hosts', + }, + { + id: SecurityPageName.hostsEvents, + path: '/hosts/events', + skipUrlState: true, + title: 'Events', }, ]); }); }); describe('needsUrlState', () => { - it('returns true when url state exists for page', () => { + it('should return true when url state exists for page', () => { const needsUrl = needsUrlState(SecurityPageName.hosts); expect(needsUrl).toEqual(true); }); - it('returns false when url state does not exist for page', () => { - const needsUrl = needsUrlState(SecurityPageName.landing); + it('should return false when url state does not exist for page', () => { + const needsUrl = needsUrlState(SecurityPageName.hostsEvents); expect(needsUrl).toEqual(false); }); }); describe('getLinkInfo', () => { - it('gets information for an individual link', () => { - const linkInfo = getLinkInfo(SecurityPageName.hosts); - expect(linkInfo).toEqual(hostsLinkInfo); + it('should get information for an individual link', () => { + const linkInfo = getLinkInfo(SecurityPageName.hostsEvents); + expect(linkInfo).toStrictEqual({ + id: SecurityPageName.hostsEvents, + path: '/hosts/events', + skipUrlState: true, + title: 'Events', + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/links/links.ts b/x-pack/plugins/security_solution/public/common/links/links.ts index 57965bdeba0c0..384861a9dc5e7 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.ts @@ -5,169 +5,120 @@ * 2.0. */ -import { AppDeepLink, AppNavLinkStatus, Capabilities } from '@kbn/core/public'; +import type { Capabilities } from '@kbn/core/public'; import { get } from 'lodash'; +import { useEffect, useState } from 'react'; +import { BehaviorSubject } from 'rxjs'; import { SecurityPageName } from '../../../common/constants'; -import { appLinks, getAppLinks } from './app_links'; -import { - Feature, +import type { + AppLinkItems, LinkInfo, LinkItem, - NavLinkItem, NormalizedLink, NormalizedLinks, - UserPermissions, + LinksPermissions, } from './types'; -const createDeepLink = (link: LinkItem, linkProps?: UserPermissions): AppDeepLink => ({ - id: link.id, - path: link.path, - title: link.title, - ...(link.links && link.links.length - ? { - deepLinks: reduceLinks({ - links: link.links, - linkProps, - formatFunction: createDeepLink, - }), - } - : {}), - ...(link.globalSearchKeywords != null ? { keywords: link.globalSearchKeywords } : {}), - ...(link.globalNavEnabled != null - ? { navLinkStatus: link.globalNavEnabled ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden } - : {}), - ...(link.globalNavOrder != null ? { order: link.globalNavOrder } : {}), - ...(link.globalSearchEnabled != null ? { searchable: link.globalSearchEnabled } : {}), +/** + * App links updater, it keeps the value of the app links in sync with all application. + * It can be updated using `updateAppLinks` or `excludeAppLink` + * Read it using `subscribeAppLinks` or `useAppLinks` hook. + */ +const appLinksUpdater$ = new BehaviorSubject<{ + links: AppLinkItems; + normalizedLinks: NormalizedLinks; +}>({ + links: [], // stores the appLinkItems recursive hierarchy + normalizedLinks: {}, // stores a flatten normalized object for direct id access }); -const createNavLinkItem = (link: LinkItem, linkProps?: UserPermissions): NavLinkItem => ({ - id: link.id, - path: link.path, - title: link.title, - ...(link.description != null ? { description: link.description } : {}), - ...(link.landingIcon != null ? { icon: link.landingIcon } : {}), - ...(link.landingImage != null ? { image: link.landingImage } : {}), - ...(link.links && link.links.length - ? { - links: reduceLinks({ - links: link.links, - linkProps, - formatFunction: createNavLinkItem, - }), - } - : {}), - ...(link.skipUrlState != null ? { skipUrlState: link.skipUrlState } : {}), -}); +const getAppLinksValue = (): AppLinkItems => appLinksUpdater$.getValue().links; +const getNormalizedLinksValue = (): NormalizedLinks => appLinksUpdater$.getValue().normalizedLinks; -const hasFeaturesCapability = ( - features: Feature[] | undefined, - capabilities: Capabilities -): boolean => { - if (!features) { - return true; - } - return features.some((featureKey) => get(capabilities, featureKey, false)); -}; +/** + * Subscribes to the updater to get the app links updates + */ +export const subscribeAppLinks = (onChange: (links: AppLinkItems) => void) => + appLinksUpdater$.subscribe(({ links }) => onChange(links)); -const isLinkAllowed = (link: LinkItem, linkProps?: UserPermissions) => - !( - linkProps != null && - // exclude link when license is basic and link is premium - ((linkProps.license && !linkProps.license.isAtLeast(link.licenseType ?? 'basic')) || - // exclude link when enableExperimental[hideWhenExperimentalKey] is enabled and link has hideWhenExperimentalKey - (link.hideWhenExperimentalKey != null && - linkProps.enableExperimental[link.hideWhenExperimentalKey]) || - // exclude link when enableExperimental[experimentalKey] is disabled and link has experimentalKey - (link.experimentalKey != null && !linkProps.enableExperimental[link.experimentalKey]) || - // exclude link when link is not part of enabled feature capabilities - (linkProps.capabilities != null && - !hasFeaturesCapability(link.features, linkProps.capabilities))) - ); - -export function reduceLinks({ - links, - linkProps, - formatFunction, -}: { - links: Readonly; - linkProps?: UserPermissions; - formatFunction: (link: LinkItem, linkProps?: UserPermissions) => T; -}): T[] { - return links.reduce( - (deepLinks: T[], link: LinkItem) => - isLinkAllowed(link, linkProps) ? [...deepLinks, formatFunction(link, linkProps)] : deepLinks, - [] - ); -} - -export const getInitialDeepLinks = (): AppDeepLink[] => { - return appLinks.map((link) => createDeepLink(link)); -}; +/** + * Hook to get the app links updated value + */ +export const useAppLinks = (): AppLinkItems => { + const [appLinks, setAppLinks] = useState(getAppLinksValue); -export const getDeepLinks = async ({ - enableExperimental, - license, - capabilities, -}: UserPermissions): Promise => { - const links = await getAppLinks({ enableExperimental, license, capabilities }); - return reduceLinks({ - links, - linkProps: { enableExperimental, license, capabilities }, - formatFunction: createDeepLink, - }); -}; + useEffect(() => { + const linksSubscription = subscribeAppLinks((newAppLinks) => { + setAppLinks(newAppLinks); + }); + return () => linksSubscription.unsubscribe(); + }, []); -export const getNavLinkItems = ({ - enableExperimental, - license, - capabilities, -}: UserPermissions): NavLinkItem[] => - reduceLinks({ - links: appLinks, - linkProps: { enableExperimental, license, capabilities }, - formatFunction: createNavLinkItem, - }); + return appLinks; +}; /** - * Recursive function to create the `NormalizedLinks` structure from a `LinkItem` array parameter + * Updates the app links applying the filter by permissions */ -const getNormalizedLinks = ( - currentLinks: Readonly, - parentId?: SecurityPageName -): NormalizedLinks => { - const result = currentLinks.reduce>( - (normalized, { links, ...currentLink }) => { - normalized[currentLink.id] = { - ...currentLink, - parentId, - }; - if (links && links.length > 0) { - Object.assign(normalized, getNormalizedLinks(links, currentLink.id)); - } - return normalized; - }, - {} - ); - return result as NormalizedLinks; +export const updateAppLinks = ( + appLinksToUpdate: AppLinkItems, + linksPermissions: LinksPermissions +) => { + const filteredAppLinks = getFilteredAppLinks(appLinksToUpdate, linksPermissions); + appLinksUpdater$.next({ + links: Object.freeze(filteredAppLinks), + normalizedLinks: Object.freeze(getNormalizedLinks(filteredAppLinks)), + }); }; /** - * Normalized indexed version of the global `links` array, referencing the parent by id, instead of having nested links children - */ -const normalizedLinks: Readonly = Object.freeze(getNormalizedLinks(appLinks)); -/** - * Returns the `NormalizedLink` from a link id parameter. - * The object reference is frozen to make sure it is not mutated by the caller. + * Excludes a link by id from the current app links + * @deprecated this function will not be needed when async link filtering is migrated to the main getAppLinks functions */ -const getNormalizedLink = (id: SecurityPageName): Readonly => - Object.freeze(normalizedLinks[id]); +export const excludeAppLink = (linkId: SecurityPageName) => { + const { links, normalizedLinks } = appLinksUpdater$.getValue(); + if (!normalizedLinks[linkId]) { + return; + } + + let found = false; + const excludeRec = (currentLinks: AppLinkItems): LinkItem[] => + currentLinks.reduce((acc, link) => { + if (!found) { + if (link.id === linkId) { + found = true; + return acc; + } + if (link.links) { + const excludedLinks = excludeRec(link.links); + if (excludedLinks.length > 0) { + acc.push({ ...link, links: excludedLinks }); + return acc; + } + } + } + acc.push(link); + return acc; + }, []); + + const excludedLinks = excludeRec(links); + + appLinksUpdater$.next({ + links: Object.freeze(excludedLinks), + normalizedLinks: Object.freeze(getNormalizedLinks(excludedLinks)), + }); +}; /** * Returns the `LinkInfo` from a link id parameter */ -export const getLinkInfo = (id: SecurityPageName): LinkInfo => { +export const getLinkInfo = (id: SecurityPageName): LinkInfo | undefined => { + const normalizedLink = getNormalizedLink(id); + if (!normalizedLink) { + return undefined; + } // discards the parentId and creates the linkInfo copy. - const { parentId, ...linkInfo } = getNormalizedLink(id); + const { parentId, ...linkInfo } = normalizedLink; return linkInfo; }; @@ -178,9 +129,14 @@ export const getAncestorLinksInfo = (id: SecurityPageName): LinkInfo[] => { const ancestors: LinkInfo[] = []; let currentId: SecurityPageName | undefined = id; while (currentId) { - const { parentId, ...linkInfo } = getNormalizedLink(currentId); - ancestors.push(linkInfo); - currentId = parentId; + const normalizedLink = getNormalizedLink(currentId); + if (normalizedLink) { + const { parentId, ...linkInfo } = normalizedLink; + ancestors.push(linkInfo); + currentId = parentId; + } else { + currentId = undefined; + } } return ancestors.reverse(); }; @@ -190,9 +146,82 @@ export const getAncestorLinksInfo = (id: SecurityPageName): LinkInfo[] => { * Defaults to `true` if the `skipUrlState` property of the `LinkItem` is `undefined`. */ export const needsUrlState = (id: SecurityPageName): boolean => { - return !getNormalizedLink(id).skipUrlState; + return !getNormalizedLink(id)?.skipUrlState; +}; + +// Internal functions + +/** + * Creates the `NormalizedLinks` structure from a `LinkItem` array + */ +const getNormalizedLinks = ( + currentLinks: AppLinkItems, + parentId?: SecurityPageName +): NormalizedLinks => { + return currentLinks.reduce((normalized, { links, ...currentLink }) => { + normalized[currentLink.id] = { + ...currentLink, + parentId, + }; + if (links && links.length > 0) { + Object.assign(normalized, getNormalizedLinks(links, currentLink.id)); + } + return normalized; + }, {}); +}; + +const getNormalizedLink = (id: SecurityPageName): Readonly | undefined => + getNormalizedLinksValue()[id]; + +const getFilteredAppLinks = ( + appLinkToFilter: AppLinkItems, + linksPermissions: LinksPermissions +): LinkItem[] => + appLinkToFilter.reduce((acc, { links, ...appLink }) => { + if (!isLinkAllowed(appLink, linksPermissions)) { + return acc; + } + if (links) { + const childrenLinks = getFilteredAppLinks(links, linksPermissions); + if (childrenLinks.length > 0) { + acc.push({ ...appLink, links: childrenLinks }); + } else { + acc.push(appLink); + } + } else { + acc.push(appLink); + } + return acc; + }, []); + +// It checks if the user has at least one of the link capabilities needed +const hasCapabilities = (linkCapabilities: string[], userCapabilities: Capabilities): boolean => + linkCapabilities.some((linkCapability) => get(userCapabilities, linkCapability, false)); + +const isLinkAllowed = ( + link: LinkItem, + { license, experimentalFeatures, capabilities }: LinksPermissions +) => { + const linkLicenseType = link.licenseType ?? 'basic'; + if (license) { + if (!license.hasAtLeast(linkLicenseType)) { + return false; + } + } else if (linkLicenseType !== 'basic') { + return false; + } + if (link.hideWhenExperimentalKey && experimentalFeatures[link.hideWhenExperimentalKey]) { + return false; + } + if (link.experimentalKey && !experimentalFeatures[link.experimentalKey]) { + return false; + } + if (link.capabilities && !hasCapabilities(link.capabilities, capabilities)) { + return false; + } + return true; }; export const getLinksWithHiddenTimeline = (): LinkInfo[] => { - return Object.values(normalizedLinks).filter((link) => link.hideTimeline); + return Object.values(getNormalizedLinksValue()).filter((link) => link.hideTimeline); }; diff --git a/x-pack/plugins/security_solution/public/common/links/types.ts b/x-pack/plugins/security_solution/public/common/links/types.ts index bfa87851306ff..323873cafc23c 100644 --- a/x-pack/plugins/security_solution/public/common/links/types.ts +++ b/x-pack/plugins/security_solution/public/common/links/types.ts @@ -6,43 +6,73 @@ */ import { Capabilities } from '@kbn/core/types'; -import { LicenseType } from '@kbn/licensing-plugin/common/types'; +import { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types'; import { IconType } from '@elastic/eui'; -import { LicenseService } from '../../../common/license'; import { ExperimentalFeatures } from '../../../common/experimental_features'; -import { CASES_FEATURE_ID, SecurityPageName, SERVER_APP_ID } from '../../../common/constants'; +import { SecurityPageName } from '../../../common/constants'; -export const FEATURE = { - general: `${SERVER_APP_ID}.show`, - casesRead: `${CASES_FEATURE_ID}.read_cases`, - casesCrud: `${CASES_FEATURE_ID}.crud_cases`, -}; - -export type Feature = Readonly; +/** + * Permissions related parameters needed for the links to be filtered + */ +export interface LinksPermissions { + capabilities: Capabilities; + experimentalFeatures: Readonly; + license?: ILicense; +} -export interface UserPermissions { - enableExperimental: ExperimentalFeatures; - license?: LicenseService; - capabilities?: Capabilities; +export interface LinkCategory { + label: string; + linkIds: readonly SecurityPageName[]; } +export type LinkCategories = Readonly; + export interface LinkItem { + /** + * The description of the link content + */ description?: string; - disabled?: boolean; // default false /** - * Displays deep link when feature flag is enabled. + * Experimental flag needed to enable the link */ experimentalKey?: keyof ExperimentalFeatures; - features?: Feature[]; /** - * Hides deep link when feature flag is enabled. + * Capabilities strings (using object dot notation) to enable the link. + * Uses "or" conditional, only one enabled capability is needed to activate the link + */ + capabilities?: string[]; + /** + * Categories to display in the navigation + */ + categories?: LinkCategories; + /** + * Enables link in the global navigation. Defaults to false. + */ + globalNavEnabled?: boolean; + /** + * Global navigation order number */ - globalNavEnabled?: boolean; // default false globalNavOrder?: number; - globalSearchEnabled?: boolean; + /** + * Disables link in the global search. Defaults to false. + */ + globalSearchDisabled?: boolean; + /** + * Keywords for the global search to search. + */ globalSearchKeywords?: string[]; + /** + * Experimental flag needed to disable the link. Opposite of experimentalKey + */ hideWhenExperimentalKey?: keyof ExperimentalFeatures; + /** + * Link id. Refers to a SecurityPageName + */ id: SecurityPageName; + /** + * Displays the "Beta" badge + */ + isBeta?: boolean; /** * Icon that is displayed on menu navigation landing page. * Only required for pages that are displayed inside a landing page. @@ -53,26 +83,38 @@ export interface LinkItem { * Only required for pages that are displayed inside a landing page. */ landingImage?: string; - isBeta?: boolean; + /** + * Minimum license required to enable the link + */ licenseType?: LicenseType; + /** + * Nested links + */ links?: LinkItem[]; + /** + * Link path relative to security root + */ path: string; - skipUrlState?: boolean; // defaults to false + /** + * Disables link in the side navigation. Defaults to false. + */ + sideNavDisabled?: boolean; + /** + * Disables the state query string in the URL. Defaults to false. + */ + skipUrlState?: boolean; + /** + * Disables the timeline call to action on the bottom of the page. Defaults to false. + */ hideTimeline?: boolean; // defaults to false + /** + * Title of the link + */ title: string; } -export interface NavLinkItem { - description?: string; - icon?: IconType; - id: SecurityPageName; - links?: NavLinkItem[]; - image?: string; - path: string; - title: string; - skipUrlState?: boolean; // default to false -} +export type AppLinkItems = Readonly; export type LinkInfo = Omit; export type NormalizedLink = LinkInfo & { parentId?: SecurityPageName }; -export type NormalizedLinks = Record; +export type NormalizedLinks = Partial>; diff --git a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx index 33a9f3a37a42f..ca9029c6c0939 100644 --- a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx @@ -6,7 +6,12 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; +import { coreMock } from '@kbn/core/public/mocks'; +import { allowedExperimentalValues } from '../../../../common/experimental_features'; +import { updateAppLinks } from '../../links'; +import { getAppLinks } from '../../links/app_links'; import { useShowTimeline } from './use_show_timeline'; +import { StartPlugins } from '../../../types'; const mockUseLocation = jest.fn().mockReturnValue({ pathname: '/overview' }); jest.mock('react-router-dom', () => { @@ -24,6 +29,23 @@ jest.mock('../../components/navigation/helpers', () => ({ })); describe('use show timeline', () => { + beforeAll(async () => { + // initialize all App links before running test + const appLinks = await getAppLinks(coreMock.createStart(), {} as StartPlugins); + updateAppLinks(appLinks, { + experimentalFeatures: allowedExperimentalValues, + capabilities: { + navLinks: {}, + management: {}, + catalogue: {}, + actions: { show: true, crud: true }, + siem: { + show: true, + crud: true, + }, + }, + }); + }); describe('useIsGroupedNavigationEnabled false', () => { beforeAll(() => { mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); diff --git a/x-pack/plugins/security_solution/public/detections/links.ts b/x-pack/plugins/security_solution/public/detections/links.ts index 1cfac62d80e6e..df9d32fcb57ed 100644 --- a/x-pack/plugins/security_solution/public/detections/links.ts +++ b/x-pack/plugins/security_solution/public/detections/links.ts @@ -5,21 +5,20 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import { ALERTS_PATH, SecurityPageName } from '../../common/constants'; +import { ALERTS_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants'; import { ALERTS } from '../app/translations'; -import { LinkItem, FEATURE } from '../common/links/types'; +import { LinkItem } from '../common/links/types'; export const links: LinkItem = { id: SecurityPageName.alerts, title: ALERTS, path: ALERTS_PATH, - features: [FEATURE.general], + capabilities: [`${SERVER_APP_ID}.show`], globalNavEnabled: true, globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.alerts', { defaultMessage: 'Alerts', }), ], - globalSearchEnabled: true, globalNavOrder: 9001, }; diff --git a/x-pack/plugins/security_solution/public/hosts/links.ts b/x-pack/plugins/security_solution/public/hosts/links.ts index d1bc26c5fb3f2..dcdeb73ac1219 100644 --- a/x-pack/plugins/security_solution/public/hosts/links.ts +++ b/x-pack/plugins/security_solution/public/hosts/links.ts @@ -24,7 +24,6 @@ export const links: LinkItem = { defaultMessage: 'Hosts', }), ], - globalSearchEnabled: true, globalNavOrder: 9002, links: [ { diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx index 81b72527500ad..57aee98af4e9d 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx @@ -8,7 +8,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { SecurityPageName } from '../../app/types'; -import { NavLinkItem } from '../../common/links/types'; +import { NavLinkItem } from '../../common/components/navigation/types'; import { TestProviders } from '../../common/mock'; import { LandingLinksIcons } from './landing_links_icons'; @@ -17,7 +17,6 @@ const DEFAULT_NAV_ITEM: NavLinkItem = { title: 'TEST LABEL', description: 'TEST DESCRIPTION', icon: 'myTestIcon', - path: '', }; const mockNavigateTo = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx index 04a3e20b1f178..b30d4f404b163 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx @@ -12,7 +12,7 @@ import { SecuritySolutionLinkAnchor, withSecuritySolutionLink, } from '../../common/components/links'; -import { NavLinkItem } from '../../common/links/types'; +import { NavLinkItem } from '../../common/components/navigation/types'; interface LandingLinksImagesProps { items: NavLinkItem[]; diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx index c44374852f29b..81881a3796f0b 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx @@ -8,7 +8,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { SecurityPageName } from '../../app/types'; -import { NavLinkItem } from '../../common/links/types'; +import { NavLinkItem } from '../../common/components/navigation/types'; import { TestProviders } from '../../common/mock'; import { LandingLinksImages } from './landing_links_images'; @@ -17,7 +17,6 @@ const DEFAULT_NAV_ITEM: NavLinkItem = { title: 'TEST LABEL', description: 'TEST DESCRIPTION', image: 'TEST_IMAGE.png', - path: '', }; jest.mock('../../common/lib/kibana/kibana_react', () => { diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx index 22bcc0f1aa251..4cf8db26bbe7a 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiPanel, EuiText, EuiTitle } from import React from 'react'; import styled from 'styled-components'; import { withSecuritySolutionLink } from '../../common/components/links'; -import { NavLinkItem } from '../../common/links/types'; +import { NavLinkItem } from '../../common/components/navigation/types'; interface LandingLinksImagesProps { items: NavLinkItem[]; diff --git a/x-pack/plugins/security_solution/public/landing_pages/constants.ts b/x-pack/plugins/security_solution/public/landing_pages/constants.ts deleted file mode 100644 index a6b72a5e7db4f..0000000000000 --- a/x-pack/plugins/security_solution/public/landing_pages/constants.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { SecurityPageName } from '../app/types'; - -export interface LandingNavGroup { - label: string; - itemIds: SecurityPageName[]; -} - -export const MANAGE_NAVIGATION_CATEGORIES: LandingNavGroup[] = [ - { - label: i18n.translate('xpack.securitySolution.landing.siemTitle', { - defaultMessage: 'SIEM', - }), - itemIds: [SecurityPageName.rules, SecurityPageName.exceptions], - }, - { - label: i18n.translate('xpack.securitySolution.landing.endpointsTitle', { - defaultMessage: 'ENDPOINTS', - }), - itemIds: [ - SecurityPageName.endpoints, - SecurityPageName.policies, - SecurityPageName.trustedApps, - SecurityPageName.eventFilters, - SecurityPageName.blocklist, - SecurityPageName.hostIsolationExceptions, - ], - }, -]; diff --git a/x-pack/plugins/security_solution/public/landing_pages/links.ts b/x-pack/plugins/security_solution/public/landing_pages/links.ts new file mode 100644 index 0000000000000..48cd31485ea7f --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/links.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { + DASHBOARDS_PATH, + SecurityPageName, + SERVER_APP_ID, + THREAT_HUNTING_PATH, +} from '../../common/constants'; +import { DASHBOARDS, THREAT_HUNTING } from '../app/translations'; +import { LinkItem } from '../common/links/types'; +import { overviewLinks, detectionResponseLinks } from '../overview/links'; +import { links as hostsLinks } from '../hosts/links'; +import { links as networkLinks } from '../network/links'; +import { links as usersLinks } from '../users/links'; + +export const dashboardsLandingLinks: LinkItem = { + id: SecurityPageName.dashboardsLanding, + title: DASHBOARDS, + path: DASHBOARDS_PATH, + globalNavEnabled: false, + capabilities: [`${SERVER_APP_ID}.show`], + globalSearchKeywords: [ + i18n.translate('xpack.securitySolution.appLinks.dashboards', { + defaultMessage: 'Dashboards', + }), + ], + links: [overviewLinks, detectionResponseLinks], + skipUrlState: true, + hideTimeline: true, +}; + +export const threatHuntingLandingLinks: LinkItem = { + id: SecurityPageName.threatHuntingLanding, + title: THREAT_HUNTING, + path: THREAT_HUNTING_PATH, + globalNavEnabled: false, + capabilities: [`${SERVER_APP_ID}.show`], + globalSearchKeywords: [ + i18n.translate('xpack.securitySolution.appLinks.threatHunting', { + defaultMessage: 'Threat hunting', + }), + ], + links: [hostsLinks, networkLinks, usersLinks], + skipUrlState: true, + hideTimeline: true, +}; diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx index 1955d56c0a151..a09db6ebf5eaa 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx @@ -9,53 +9,58 @@ import { render } from '@testing-library/react'; import React from 'react'; import { SecurityPageName } from '../../app/types'; import { TestProviders } from '../../common/mock'; -import { LandingCategories } from './manage'; -import { NavLinkItem } from '../../common/links/types'; +import { ManagementCategories } from './manage'; +import { NavLinkItem } from '../../common/components/navigation/types'; const RULES_ITEM_LABEL = 'elastic rules!'; const EXCEPTIONS_ITEM_LABEL = 'exceptional!'; +const CATEGORY_1_LABEL = 'first tests category'; +const CATEGORY_2_LABEL = 'second tests category'; -const mockAppManageLink: NavLinkItem = { +const defaultAppManageLink: NavLinkItem = { id: SecurityPageName.administration, - path: '', title: 'admin', + categories: [ + { + label: CATEGORY_1_LABEL, + linkIds: [SecurityPageName.rules], + }, + { + label: CATEGORY_2_LABEL, + linkIds: [SecurityPageName.exceptions], + }, + ], links: [ { id: SecurityPageName.rules, title: RULES_ITEM_LABEL, description: '', icon: 'testIcon1', - path: '', }, { id: SecurityPageName.exceptions, title: EXCEPTIONS_ITEM_LABEL, description: '', icon: 'testIcon2', - path: '', }, ], }; + +const mockedUseCanSeeHostIsolationExceptionsMenu = jest.fn(); +jest.mock('../../management/pages/host_isolation_exceptions/view/hooks', () => ({ + useCanSeeHostIsolationExceptionsMenu: () => mockedUseCanSeeHostIsolationExceptionsMenu(), +})); + +const mockAppManageLink = jest.fn(() => defaultAppManageLink); jest.mock('../../common/components/navigation/nav_links', () => ({ - useAppRootNavLink: jest.fn(() => mockAppManageLink), + useAppRootNavLink: () => mockAppManageLink(), })); -describe('LandingCategories', () => { - it('renders items', () => { +describe('ManagementCategories', () => { + it('should render items', () => { const { queryByText } = render( - + ); @@ -63,17 +68,19 @@ describe('LandingCategories', () => { expect(queryByText(EXCEPTIONS_ITEM_LABEL)).toBeInTheDocument(); }); - it('renders items in the same order as defined', () => { + it('should render items in the same order as defined', () => { + mockAppManageLink.mockReturnValueOnce({ + ...defaultAppManageLink, + categories: [ + { + label: '', + linkIds: [SecurityPageName.exceptions, SecurityPageName.rules], + }, + ], + }); const { queryAllByTestId } = render( - + ); @@ -82,4 +89,109 @@ describe('LandingCategories', () => { expect(renderedItems[0]).toHaveTextContent(EXCEPTIONS_ITEM_LABEL); expect(renderedItems[1]).toHaveTextContent(RULES_ITEM_LABEL); }); + + it('should not render category items filtered', () => { + mockAppManageLink.mockReturnValueOnce({ + ...defaultAppManageLink, + categories: [ + { + label: CATEGORY_1_LABEL, + linkIds: [SecurityPageName.rules, SecurityPageName.exceptions], + }, + ], + links: [ + { + id: SecurityPageName.rules, + title: RULES_ITEM_LABEL, + description: '', + icon: 'testIcon1', + }, + ], + }); + const { queryAllByTestId } = render( + + + + ); + + const renderedItems = queryAllByTestId('LandingItem'); + + expect(renderedItems).toHaveLength(1); + expect(renderedItems[0]).toHaveTextContent(RULES_ITEM_LABEL); + }); + + it('should not render category if all items filtered', () => { + mockAppManageLink.mockReturnValueOnce({ + ...defaultAppManageLink, + links: [], + }); + const { queryByText } = render( + + + + ); + + expect(queryByText(CATEGORY_1_LABEL)).not.toBeInTheDocument(); + expect(queryByText(CATEGORY_2_LABEL)).not.toBeInTheDocument(); + }); + + it('should not render hostIsolationExceptionsLink when useCanSeeHostIsolationExceptionsMenu is false', () => { + mockedUseCanSeeHostIsolationExceptionsMenu.mockReturnValueOnce(false); + mockAppManageLink.mockReturnValueOnce({ + ...defaultAppManageLink, + categories: [ + { + label: CATEGORY_1_LABEL, + linkIds: [SecurityPageName.hostIsolationExceptions], + }, + ], + links: [ + { + id: SecurityPageName.hostIsolationExceptions, + title: 'test hostIsolationExceptions title', + description: 'test hostIsolationExceptions description', + icon: 'testIcon1', + }, + ], + }); + const { queryByText } = render( + + + + ); + + expect(queryByText(CATEGORY_1_LABEL)).not.toBeInTheDocument(); + }); + + it('should render hostIsolationExceptionsLink when useCanSeeHostIsolationExceptionsMenu is true', () => { + const HOST_ISOLATION_EXCEPTIONS_ITEM_LABEL = 'test hostIsolationExceptions title'; + mockedUseCanSeeHostIsolationExceptionsMenu.mockReturnValueOnce(true); + mockAppManageLink.mockReturnValueOnce({ + ...defaultAppManageLink, + categories: [ + { + label: CATEGORY_1_LABEL, + linkIds: [SecurityPageName.hostIsolationExceptions], + }, + ], + links: [ + { + id: SecurityPageName.hostIsolationExceptions, + title: HOST_ISOLATION_EXCEPTIONS_ITEM_LABEL, + description: 'test hostIsolationExceptions description', + icon: 'testIcon1', + }, + ], + }); + const { queryAllByTestId } = render( + + + + ); + + const renderedItems = queryAllByTestId('LandingItem'); + + expect(renderedItems).toHaveLength(1); + expect(renderedItems[0]).toHaveTextContent(HOST_ISOLATION_EXCEPTIONS_ITEM_LABEL); + }); }); diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx index f0e6094d5113f..d484e5fe90a52 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx @@ -11,18 +11,18 @@ import styled from 'styled-components'; import { SecurityPageName } from '../../app/types'; import { HeaderPage } from '../../common/components/header_page'; import { useAppRootNavLink } from '../../common/components/navigation/nav_links'; -import { NavigationCategories } from '../../common/components/navigation/types'; +import { NavLinkItem } from '../../common/components/navigation/types'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { SpyRoute } from '../../common/utils/route/spy_routes'; -import { navigationCategories } from '../../management/links'; +import { useCanSeeHostIsolationExceptionsMenu } from '../../management/pages/host_isolation_exceptions/view/hooks'; import { LandingLinksIcons } from '../components/landing_links_icons'; import { MANAGE_PAGE_TITLE } from './translations'; export const ManageLandingPage = () => ( - - + + ); @@ -31,37 +31,52 @@ const StyledEuiHorizontalRule = styled(EuiHorizontalRule)` margin-bottom: ${({ theme }) => theme.eui.paddingSizes.l}; `; -const useGetManageNavLinks = () => { - const manageNavLinks = useAppRootNavLink(SecurityPageName.administration)?.links ?? []; +type ManagementCategories = Array<{ label: string; links: NavLinkItem[] }>; +const useManagementCategories = (): ManagementCategories => { + const hideHostIsolationExceptions = !useCanSeeHostIsolationExceptionsMenu(); + const { links = [], categories = [] } = useAppRootNavLink(SecurityPageName.administration) ?? {}; - const manageLinksById = Object.fromEntries(manageNavLinks.map((link) => [link.id, link])); - return (linkIds: readonly SecurityPageName[]) => linkIds.map((linkId) => manageLinksById[linkId]); + const manageLinksById = Object.fromEntries(links.map((link) => [link.id, link])); + + return categories.reduce((acc, { label, linkIds }) => { + const linksItem = linkIds.reduce((linksAcc, linkId) => { + if ( + manageLinksById[linkId] && + !(linkId === SecurityPageName.hostIsolationExceptions && hideHostIsolationExceptions) + ) { + linksAcc.push(manageLinksById[linkId]); + } + return linksAcc; + }, []); + if (linksItem.length > 0) { + acc.push({ label, links: linksItem }); + } + return acc; + }, []); }; -export const LandingCategories = React.memo( - ({ categories }: { categories: NavigationCategories }) => { - const getManageNavLinks = useGetManageNavLinks(); +export const ManagementCategories = () => { + const managementCategories = useManagementCategories(); - return ( - <> - {categories.map(({ label, linkIds }, index) => ( -
- {index > 0 && ( - <> - - - - )} - -

{label}

-
- - -
- ))} - - ); - } -); + return ( + <> + {managementCategories.map(({ label, links }, index) => ( +
+ {index > 0 && ( + <> + + + + )} + +

{label}

+
+ + +
+ ))} + + ); +}; -LandingCategories.displayName = 'LandingCategories'; +ManagementCategories.displayName = 'ManagementCategories'; diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index ee60274cbb83d..9316f92a0d0b8 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { BLOCKLIST_PATH, @@ -17,6 +18,7 @@ import { RULES_CREATE_PATH, RULES_PATH, SecurityPageName, + SERVER_APP_ID, TRUSTED_APPS_PATH, } from '../../common/constants'; import { @@ -31,8 +33,8 @@ import { RULES, TRUSTED_APPLICATIONS, } from '../app/translations'; -import { NavigationCategories } from '../common/components/navigation/types'; -import { FEATURE, LinkItem } from '../common/links/types'; +import { LinkItem } from '../common/links/types'; +import { StartPlugins } from '../types'; import { IconBlocklist } from './icons/blocklist'; import { IconEndpoints } from './icons/endpoints'; @@ -43,19 +45,42 @@ import { IconHostIsolation } from './icons/host_isolation'; import { IconSiemRules } from './icons/siem_rules'; import { IconTrustedApplications } from './icons/trusted_applications'; -export const links: LinkItem = { +const categories = [ + { + label: i18n.translate('xpack.securitySolution.appLinks.category.siem', { + defaultMessage: 'SIEM', + }), + linkIds: [SecurityPageName.rules, SecurityPageName.exceptions], + }, + { + label: i18n.translate('xpack.securitySolution.appLinks.category.endpoints', { + defaultMessage: 'ENDPOINTS', + }), + linkIds: [ + SecurityPageName.endpoints, + SecurityPageName.policies, + SecurityPageName.trustedApps, + SecurityPageName.eventFilters, + SecurityPageName.hostIsolationExceptions, + SecurityPageName.blocklist, + ], + }, +]; + +const links: LinkItem = { id: SecurityPageName.administration, title: MANAGE, path: MANAGE_PATH, skipUrlState: true, hideTimeline: true, globalNavEnabled: false, - features: [FEATURE.general], + capabilities: [`${SERVER_APP_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.manage', { defaultMessage: 'Manage', }), ], + categories, links: [ { id: SecurityPageName.rules, @@ -73,7 +98,6 @@ export const links: LinkItem = { defaultMessage: 'Rules', }), ], - globalSearchEnabled: true, links: [ { id: SecurityPageName.rulesCreate, @@ -99,7 +123,6 @@ export const links: LinkItem = { defaultMessage: 'Exception lists', }), ], - globalSearchEnabled: true, }, { id: SecurityPageName.endpoints, @@ -178,24 +201,7 @@ export const links: LinkItem = { ], }; -export const navigationCategories: NavigationCategories = [ - { - label: i18n.translate('xpack.securitySolution.appLinks.category.siem', { - defaultMessage: 'SIEM', - }), - linkIds: [SecurityPageName.rules, SecurityPageName.exceptions], - }, - { - label: i18n.translate('xpack.securitySolution.appLinks.category.endpoints', { - defaultMessage: 'ENDPOINTS', - }), - linkIds: [ - SecurityPageName.endpoints, - SecurityPageName.policies, - SecurityPageName.trustedApps, - SecurityPageName.eventFilters, - SecurityPageName.blocklist, - SecurityPageName.hostIsolationExceptions, - ], - }, -] as const; +export const getManagementLinkItems = async (core: CoreStart, plugins: StartPlugins) => { + // TODO: implement async logic to exclude links + return links; +}; diff --git a/x-pack/plugins/security_solution/public/overview/links.ts b/x-pack/plugins/security_solution/public/overview/links.ts index 9fd06b523347f..dbcc04b5c6d8e 100644 --- a/x-pack/plugins/security_solution/public/overview/links.ts +++ b/x-pack/plugins/security_solution/public/overview/links.ts @@ -7,14 +7,14 @@ import { i18n } from '@kbn/i18n'; import { - DASHBOARDS_PATH, DETECTION_RESPONSE_PATH, LANDING_PATH, OVERVIEW_PATH, SecurityPageName, + SERVER_APP_ID, } from '../../common/constants'; -import { DASHBOARDS, DETECTION_RESPONSE, GETTING_STARTED, OVERVIEW } from '../app/translations'; -import { FEATURE, LinkItem } from '../common/links/types'; +import { DETECTION_RESPONSE, GETTING_STARTED, OVERVIEW } from '../app/translations'; +import { LinkItem } from '../common/links/types'; import overviewPageImg from '../common/images/overview_page.png'; import detectionResponsePageImg from '../common/images/detection_response_page.png'; @@ -27,7 +27,7 @@ export const overviewLinks: LinkItem = { }), path: OVERVIEW_PATH, globalNavEnabled: true, - features: [FEATURE.general], + capabilities: [`${SERVER_APP_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.overview', { defaultMessage: 'Overview', @@ -41,7 +41,7 @@ export const gettingStartedLinks: LinkItem = { title: GETTING_STARTED, path: LANDING_PATH, globalNavEnabled: false, - features: [FEATURE.general], + capabilities: [`${SERVER_APP_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.getStarted', { defaultMessage: 'Getting started', @@ -62,26 +62,10 @@ export const detectionResponseLinks: LinkItem = { path: DETECTION_RESPONSE_PATH, globalNavEnabled: false, experimentalKey: 'detectionResponseEnabled', - features: [FEATURE.general], + capabilities: [`${SERVER_APP_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.detectionAndResponse', { defaultMessage: 'Detection & Response', }), ], }; - -export const dashboardsLandingLinks: LinkItem = { - id: SecurityPageName.dashboardsLanding, - title: DASHBOARDS, - path: DASHBOARDS_PATH, - globalNavEnabled: false, - features: [FEATURE.general], - globalSearchKeywords: [ - i18n.translate('xpack.securitySolution.appLinks.dashboards', { - defaultMessage: 'Dashboards', - }), - ], - links: [overviewLinks, detectionResponseLinks], - skipUrlState: true, - hideTimeline: true, -}; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 4b49c04f295a5..1716e08febd40 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -45,9 +45,11 @@ import { DETECTION_ENGINE_INDEX_URL, SERVER_APP_ID, SOURCERER_API_URL, + ENABLE_GROUPED_NAVIGATION, } from '../common/constants'; -import { getDeepLinks } from './app/deep_links'; +import { getDeepLinks, registerDeepLinksUpdater } from './app/deep_links'; +import { AppLinkItems, subscribeAppLinks, updateAppLinks } from './common/links'; import { getSubPluginRoutesByCapabilities, manageOldSiemRoutes } from './helpers'; import { SecurityAppStore } from './common/store/store'; import { licenseService } from './common/hooks/use_license'; @@ -140,7 +142,6 @@ export class Plugin implements IPlugin { // required to show the alert table inside cases const { alertsTableConfigurationRegistry } = plugins.triggersActionsUi; @@ -171,7 +172,15 @@ export class Plugin implements IPlugin { const [coreStart] = await core.getStartServices(); - manageOldSiemRoutes(coreStart); + + const subscription = subscribeAppLinks((links: AppLinkItems) => { + // It has to be called once after deep links are initialized + if (links.length > 0) { + manageOldSiemRoutes(coreStart); + subscription.unsubscribe(); + } + }); + return () => true; }, }); @@ -220,35 +229,65 @@ export class Plugin implements IPlugin { - if (currentLicense.type !== undefined) { - this.appUpdater$.next(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks( - this.experimentalFeatures, - currentLicense.type, - core.application.capabilities - ), - })); + + if (newNavEnabled) { + registerDeepLinksUpdater(this.appUpdater$); + } + + // Not using await to prevent blocking start execution + this.lazyApplicationLinks().then(({ getAppLinks }) => { + getAppLinks(core, plugins).then((appLinks) => { + if (licensing !== null) { + this.licensingSubscription = licensing.subscribe((currentLicense) => { + if (currentLicense.type !== undefined) { + updateAppLinks(appLinks, { + experimentalFeatures: this.experimentalFeatures, + license: currentLicense, + capabilities: core.application.capabilities, + }); + + if (!newNavEnabled) { + // TODO: remove block when nav flag no longer needed + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks( + this.experimentalFeatures, + currentLicense.type, + core.application.capabilities + ), + })); + } + } + }); + } else { + updateAppLinks(appLinks, { + experimentalFeatures: this.experimentalFeatures, + capabilities: core.application.capabilities, + }); + + if (!newNavEnabled) { + // TODO: remove block when nav flag no longer needed + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks( + this.experimentalFeatures, + undefined, + core.application.capabilities + ), + })); + } } }); - } else { - this.appUpdater$.next(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks( - this.experimentalFeatures, - undefined, - core.application.capabilities - ), - })); - } + }); return {}; } @@ -296,11 +335,22 @@ export class Plugin implements IPlugin Date: Fri, 20 May 2022 16:27:14 +0300 Subject: [PATCH 036/120] [XY] `pointsRadius`, `showPoints` and `lineWidth`. (#130391) * Added pointsRadius, showPoints and lineWidth. * Added tests. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../extended_data_layer.test.ts.snap | 6 ++ .../common_data_layer_args.ts | 12 +++ .../extended_data_layer.test.ts | 94 ++++++++++++------- .../extended_data_layer_fn.ts | 10 +- .../reference_line_layer.ts | 17 +--- .../reference_line_layer_fn.ts | 24 +++++ .../common/expression_functions/validate.ts | 51 ++++++++++ .../expression_functions/xy_vis.test.ts | 1 + .../common/expression_functions/xy_vis_fn.ts | 12 +++ .../expression_xy/common/i18n/index.tsx | 12 +++ .../common/types/expression_functions.ts | 8 +- .../public/components/xy_chart.test.tsx | 69 ++++++++++++++ .../public/helpers/data_layers.tsx | 56 ++++++++--- 13 files changed, 313 insertions(+), 59 deletions(-) create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer_fn.ts diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/extended_data_layer.test.ts.snap b/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/extended_data_layer.test.ts.snap index 68262f8a4f3de..9abd76c669b8f 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/extended_data_layer.test.ts.snap +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/extended_data_layer.test.ts.snap @@ -1,5 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`extendedDataLayerConfig throws the error if lineWidth is provided to the not line/area chart 1`] = `"\`lineWidth\` can be applied only for line or area charts"`; + exports[`extendedDataLayerConfig throws the error if markSizeAccessor doesn't have the corresponding column in the table 1`] = `"Provided column name or index is invalid: nonsense"`; exports[`extendedDataLayerConfig throws the error if markSizeAccessor is provided to the not line/area chart 1`] = `"\`markSizeAccessor\` can't be used. Dots are applied only for line or area charts"`; + +exports[`extendedDataLayerConfig throws the error if pointsRadius is provided to the not line/area chart 1`] = `"\`pointsRadius\` can be applied only for line or area charts"`; + +exports[`extendedDataLayerConfig throws the error if showPoints is provided to the not line/area chart 1`] = `"\`showPoints\` can be applied only for line or area charts"`; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts index f4543c5236ce2..c7f2da8ec1543 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts @@ -43,6 +43,18 @@ export const commonDataLayerArgs: Omit< default: false, help: strings.getIsHistogramHelp(), }, + lineWidth: { + types: ['number'], + help: strings.getLineWidthHelp(), + }, + showPoints: { + types: ['boolean'], + help: strings.getShowPointsHelp(), + }, + pointsRadius: { + types: ['number'], + help: strings.getPointsRadiusHelp(), + }, yConfig: { types: [Y_CONFIG], help: strings.getYConfigHelp(), diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.test.ts index 5b943b0790313..7f513168a8607 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.test.ts @@ -13,62 +13,92 @@ import { LayerTypes } from '../constants'; import { extendedDataLayerFunction } from './extended_data_layer'; describe('extendedDataLayerConfig', () => { + const args: ExtendedDataLayerArgs = { + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + splitAccessor: 'd', + xScaleType: 'linear', + isHistogram: false, + palette: mockPaletteOutput, + }; + test('produces the correct arguments', async () => { const { data } = sampleArgs(); - const args: ExtendedDataLayerArgs = { - seriesType: 'line', - xAccessor: 'c', - accessors: ['a', 'b'], - splitAccessor: 'd', - xScaleType: 'linear', - isHistogram: false, - palette: mockPaletteOutput, + const fullArgs: ExtendedDataLayerArgs = { + ...args, markSizeAccessor: 'b', + showPoints: true, + lineWidth: 10, + pointsRadius: 10, }; - const result = await extendedDataLayerFunction.fn(data, args, createMockExecutionContext()); + const result = await extendedDataLayerFunction.fn(data, fullArgs, createMockExecutionContext()); expect(result).toEqual({ type: 'extendedDataLayer', layerType: LayerTypes.DATA, - ...args, + ...fullArgs, table: data, }); }); test('throws the error if markSizeAccessor is provided to the not line/area chart', async () => { const { data } = sampleArgs(); - const args: ExtendedDataLayerArgs = { - seriesType: 'bar', - xAccessor: 'c', - accessors: ['a', 'b'], - splitAccessor: 'd', - xScaleType: 'linear', - isHistogram: false, - palette: mockPaletteOutput, - markSizeAccessor: 'b', - }; expect( - extendedDataLayerFunction.fn(data, args, createMockExecutionContext()) + extendedDataLayerFunction.fn( + data, + { ...args, seriesType: 'bar', markSizeAccessor: 'b' }, + createMockExecutionContext() + ) ).rejects.toThrowErrorMatchingSnapshot(); }); test("throws the error if markSizeAccessor doesn't have the corresponding column in the table", async () => { const { data } = sampleArgs(); - const args: ExtendedDataLayerArgs = { - seriesType: 'line', - xAccessor: 'c', - accessors: ['a', 'b'], - splitAccessor: 'd', - xScaleType: 'linear', - isHistogram: false, - palette: mockPaletteOutput, - markSizeAccessor: 'nonsense', - }; expect( - extendedDataLayerFunction.fn(data, args, createMockExecutionContext()) + extendedDataLayerFunction.fn( + data, + { ...args, markSizeAccessor: 'nonsense' }, + createMockExecutionContext() + ) + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + test('throws the error if lineWidth is provided to the not line/area chart', async () => { + const { data } = sampleArgs(); + expect( + extendedDataLayerFunction.fn( + data, + { ...args, seriesType: 'bar', lineWidth: 10 }, + createMockExecutionContext() + ) + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + test('throws the error if showPoints is provided to the not line/area chart', async () => { + const { data } = sampleArgs(); + + expect( + extendedDataLayerFunction.fn( + data, + { ...args, seriesType: 'bar', showPoints: true }, + createMockExecutionContext() + ) + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + test('throws the error if pointsRadius is provided to the not line/area chart', async () => { + const { data } = sampleArgs(); + + expect( + extendedDataLayerFunction.fn( + data, + { ...args, seriesType: 'bar', pointsRadius: 10 }, + createMockExecutionContext() + ) ).rejects.toThrowErrorMatchingSnapshot(); }); }); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer_fn.ts index 8e5019e065133..f45aea7e86d8d 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer_fn.ts @@ -10,7 +10,12 @@ import { validateAccessor } from '@kbn/visualizations-plugin/common/utils'; import { ExtendedDataLayerArgs, ExtendedDataLayerFn } from '../types'; import { EXTENDED_DATA_LAYER, LayerTypes } from '../constants'; import { getAccessors, normalizeTable } from '../helpers'; -import { validateMarkSizeForChartType } from './validate'; +import { + validateLineWidthForChartType, + validateMarkSizeForChartType, + validatePointsRadiusForChartType, + validateShowPointsForChartType, +} from './validate'; export const extendedDataLayerFn: ExtendedDataLayerFn['fn'] = async (data, args, context) => { const table = args.table ?? data; @@ -21,6 +26,9 @@ export const extendedDataLayerFn: ExtendedDataLayerFn['fn'] = async (data, args, accessors.accessors.forEach((accessor) => validateAccessor(accessor, table.columns)); validateMarkSizeForChartType(args.markSizeAccessor, args.seriesType); validateAccessor(args.markSizeAccessor, table.columns); + validateLineWidthForChartType(args.lineWidth, args.seriesType); + validateShowPointsForChartType(args.showPoints, args.seriesType); + validatePointsRadiusForChartType(args.pointsRadius, args.seriesType); const normalizedTable = normalizeTable(table, accessors.xAccessor); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts index 6b51edd2d209e..234001015d73a 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { validateAccessor } from '@kbn/visualizations-plugin/common/utils'; -import { LayerTypes, REFERENCE_LINE_LAYER, EXTENDED_Y_CONFIG } from '../constants'; +import { REFERENCE_LINE_LAYER, EXTENDED_Y_CONFIG } from '../constants'; import { ReferenceLineLayerFn } from '../types'; import { strings } from '../i18n'; @@ -41,16 +40,8 @@ export const referenceLineLayerFunction: ReferenceLineLayerFn = { help: strings.getLayerIdHelp(), }, }, - fn(input, args) { - const table = args.table ?? input; - const accessors = args.accessors ?? []; - accessors.forEach((accessor) => validateAccessor(accessor, table.columns)); - - return { - type: REFERENCE_LINE_LAYER, - ...args, - layerType: LayerTypes.REFERENCELINE, - table: args.table ?? input, - }; + async fn(input, args, context) { + const { referenceLineLayerFn } = await import('./reference_line_layer_fn'); + return await referenceLineLayerFn(input, args, context); }, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer_fn.ts new file mode 100644 index 0000000000000..8b6d1cc531447 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer_fn.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { validateAccessor } from '@kbn/visualizations-plugin/common/utils'; +import { LayerTypes, REFERENCE_LINE_LAYER } from '../constants'; +import { ReferenceLineLayerFn } from '../types'; + +export const referenceLineLayerFn: ReferenceLineLayerFn['fn'] = async (input, args, handlers) => { + const table = args.table ?? input; + const accessors = args.accessors ?? []; + accessors.forEach((accessor) => validateAccessor(accessor, table.columns)); + + return { + type: REFERENCE_LINE_LAYER, + ...args, + layerType: LayerTypes.REFERENCELINE, + table: args.table ?? input, + }; +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts index df7f9ee08632e..de01b149802b9 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts @@ -34,6 +34,27 @@ export const errors = { i18n.translate('expressionXY.reusable.function.xyVis.errors.markSizeLimitsError', { defaultMessage: 'Mark size ratio must be greater or equal to 1 and less or equal to 100', }), + lineWidthForNonLineOrAreaChartError: () => + i18n.translate( + 'expressionXY.reusable.function.xyVis.errors.lineWidthForNonLineOrAreaChartError', + { + defaultMessage: '`lineWidth` can be applied only for line or area charts', + } + ), + showPointsForNonLineOrAreaChartError: () => + i18n.translate( + 'expressionXY.reusable.function.xyVis.errors.showPointsForNonLineOrAreaChartError', + { + defaultMessage: '`showPoints` can be applied only for line or area charts', + } + ), + pointsRadiusForNonLineOrAreaChartError: () => + i18n.translate( + 'expressionXY.reusable.function.xyVis.errors.pointsRadiusForNonLineOrAreaChartError', + { + defaultMessage: '`pointsRadius` can be applied only for line or area charts', + } + ), markSizeRatioWithoutAccessor: () => i18n.translate('expressionXY.reusable.function.xyVis.errors.markSizeRatioWithoutAccessor', { defaultMessage: 'Mark size ratio can be applied only with `markSizeAccessor`', @@ -140,6 +161,9 @@ export const validateValueLabels = ( } }; +const isAreaOrLineChart = (seriesType: SeriesType) => + seriesType.includes('line') || seriesType.includes('area'); + export const validateAddTimeMarker = ( dataLayers: Array, addTimeMarker?: boolean @@ -164,6 +188,33 @@ export const validateMarkSizeRatioLimits = (markSizeRatio?: number) => { } }; +export const validateLineWidthForChartType = ( + lineWidth: number | undefined, + seriesType: SeriesType +) => { + if (lineWidth !== undefined && !isAreaOrLineChart(seriesType)) { + throw new Error(errors.lineWidthForNonLineOrAreaChartError()); + } +}; + +export const validateShowPointsForChartType = ( + showPoints: boolean | undefined, + seriesType: SeriesType +) => { + if (showPoints !== undefined && !isAreaOrLineChart(seriesType)) { + throw new Error(errors.showPointsForNonLineOrAreaChartError()); + } +}; + +export const validatePointsRadiusForChartType = ( + pointsRadius: number | undefined, + seriesType: SeriesType +) => { + if (pointsRadius !== undefined && !isAreaOrLineChart(seriesType)) { + throw new Error(errors.pointsRadiusForNonLineOrAreaChartError()); + } +}; + export const validateMarkSizeRatioWithAccessor = ( markSizeRatio: number | undefined, markSizeAccessor: ExpressionValueVisDimension | string | undefined diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts index 8a327ccca9e20..174ff908eeaa1 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts @@ -65,6 +65,7 @@ describe('xyVis', () => { ) ).rejects.toThrowErrorMatchingSnapshot(); }); + test('it should throw error if minTimeBarInterval is invalid', async () => { const { data, args } = sampleArgs(); const { layers, ...rest } = args; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index 4c25e3378d523..afe569a86f894 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -29,6 +29,9 @@ import { validateMinTimeBarInterval, validateMarkSizeForChartType, validateMarkSizeRatioWithAccessor, + validateShowPointsForChartType, + validateLineWidthForChartType, + validatePointsRadiusForChartType, } from './validate'; const createDataLayer = (args: XYArgs, table: Datatable): DataLayerConfigResult => { @@ -43,6 +46,9 @@ const createDataLayer = (args: XYArgs, table: Datatable): DataLayerConfigResult isHistogram: args.isHistogram, palette: args.palette, yConfig: args.yConfig, + showPoints: args.showPoints, + pointsRadius: args.pointsRadius, + lineWidth: args.lineWidth, layerType: LayerTypes.DATA, table: normalizedTable, ...accessors, @@ -68,6 +74,9 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { yConfig, palette, markSizeAccessor, + showPoints, + pointsRadius, + lineWidth, ...restArgs } = args; @@ -116,6 +125,9 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { validateValueLabels(args.valueLabels, hasBar, hasNotHistogramBars); validateMarkSizeRatioWithAccessor(args.markSizeRatio, dataLayers[0].markSizeAccessor); validateMarkSizeRatioLimits(args.markSizeRatio); + validateLineWidthForChartType(lineWidth, args.seriesType); + validateShowPointsForChartType(showPoints, args.seriesType); + validatePointsRadiusForChartType(pointsRadius, args.seriesType); return { type: 'render', diff --git a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx index ed2ef4a7a57ce..4f94d5805396d 100644 --- a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx @@ -181,6 +181,18 @@ export const strings = { i18n.translate('expressionXY.dataLayer.markSizeAccessor.help', { defaultMessage: 'Mark size accessor', }), + getLineWidthHelp: () => + i18n.translate('expressionXY.dataLayer.lineWidth.help', { + defaultMessage: 'Line width', + }), + getShowPointsHelp: () => + i18n.translate('expressionXY.dataLayer.showPoints.help', { + defaultMessage: 'Show points', + }), + getPointsRadiusHelp: () => + i18n.translate('expressionXY.dataLayer.pointsRadius.help', { + defaultMessage: 'Points radius', + }), getYConfigHelp: () => i18n.translate('expressionXY.dataLayer.yConfig.help', { defaultMessage: 'Additional configuration for y axes', diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index c0336fc67536f..05447607bc194 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -102,6 +102,9 @@ export interface DataLayerArgs { hide?: boolean; splitAccessor?: string | ExpressionValueVisDimension; markSizeAccessor?: string | ExpressionValueVisDimension; + lineWidth?: number; + showPoints?: boolean; + pointsRadius?: number; columnToLabel?: string; // Actually a JSON key-value pair xScaleType: XScaleType; isHistogram: boolean; @@ -121,6 +124,9 @@ export interface ExtendedDataLayerArgs { hide?: boolean; splitAccessor?: string; markSizeAccessor?: string; + lineWidth?: number; + showPoints?: boolean; + pointsRadius?: number; columnToLabel?: string; // Actually a JSON key-value pair xScaleType: XScaleType; isHistogram: boolean; @@ -416,7 +422,7 @@ export type ReferenceLineLayerFn = ExpressionFunctionDefinition< typeof REFERENCE_LINE_LAYER, Datatable, ReferenceLineLayerArgs, - ReferenceLineLayerConfigResult + Promise >; export type YConfigFn = ExpressionFunctionDefinition; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index 91e5ae8ad1484..f46213fe41ba3 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -722,6 +722,75 @@ describe('XYChart component', () => { expect(lineArea.prop('lineSeriesStyle')).toEqual(expectedSeriesStyle); }); + test('applies the line width to the chart', () => { + const { args } = sampleArgs(); + const lineWidthArg = { lineWidth: 10 }; + const component = shallow( + + ); + const dataLayers = component.find(DataLayers).dive(); + const lineArea = dataLayers.find(LineSeries).at(0); + const expectedSeriesStyle = expect.objectContaining({ + line: { strokeWidth: lineWidthArg.lineWidth }, + }); + + expect(lineArea.prop('areaSeriesStyle')).toEqual(expectedSeriesStyle); + expect(lineArea.prop('lineSeriesStyle')).toEqual(expectedSeriesStyle); + }); + + test('applies showPoints to the chart', () => { + const checkIfPointsVisibilityIsApplied = (showPoints: boolean) => { + const { args } = sampleArgs(); + const component = shallow( + + ); + const dataLayers = component.find(DataLayers).dive(); + const lineArea = dataLayers.find(LineSeries).at(0); + const expectedSeriesStyle = expect.objectContaining({ + point: expect.objectContaining({ + visible: showPoints, + }), + }); + expect(lineArea.prop('areaSeriesStyle')).toEqual(expectedSeriesStyle); + expect(lineArea.prop('lineSeriesStyle')).toEqual(expectedSeriesStyle); + }; + + checkIfPointsVisibilityIsApplied(true); + checkIfPointsVisibilityIsApplied(false); + }); + + test('applies point radius to the chart', () => { + const pointsRadius = 10; + const { args } = sampleArgs(); + const component = shallow( + + ); + const dataLayers = component.find(DataLayers).dive(); + const lineArea = dataLayers.find(LineSeries).at(0); + const expectedSeriesStyle = expect.objectContaining({ + point: expect.objectContaining({ + radius: pointsRadius, + }), + }); + expect(lineArea.prop('areaSeriesStyle')).toEqual(expectedSeriesStyle); + expect(lineArea.prop('lineSeriesStyle')).toEqual(expectedSeriesStyle); + }); + test('it renders bar', () => { const { args } = sampleArgs(); const component = shallow( diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx index 08761f633f851..34e5e36091ae1 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx @@ -8,6 +8,7 @@ import { AreaSeriesProps, + AreaSeriesStyle, BarSeriesProps, ColorVariant, LineSeriesProps, @@ -80,6 +81,14 @@ type GetColorFn = ( } ) => string | null; +type GetLineConfigFn = (config: { + xAccessor: string | undefined; + markSizeAccessor: string | undefined; + emphasizeFitting?: boolean; + showPoints?: boolean; + pointsRadius?: number; +}) => Partial; + export interface DatatableWithFormatInfo { table: Datatable; formattedColumns: Record; @@ -227,17 +236,26 @@ const getSeriesName: GetSeriesNameFn = ( return splitColumnId ? data.seriesKeys[0] : columnToLabelMap[data.seriesKeys[0]] ?? null; }; -const getPointConfig = ( - xAccessor: string | undefined, - markSizeAccessor: string | undefined, - emphasizeFitting?: boolean -) => ({ - visible: !xAccessor || markSizeAccessor !== undefined, - radius: xAccessor && !emphasizeFitting ? 5 : 0, +const getPointConfig: GetLineConfigFn = ({ + xAccessor, + markSizeAccessor, + emphasizeFitting, + showPoints, + pointsRadius, +}) => ({ + visible: showPoints !== undefined ? showPoints : !xAccessor || markSizeAccessor !== undefined, + radius: pointsRadius !== undefined ? pointsRadius : xAccessor && !emphasizeFitting ? 5 : 0, fill: markSizeAccessor ? ColorVariant.Series : undefined, }); -const getLineConfig = () => ({ visible: true, stroke: ColorVariant.Series, opacity: 1, dash: [] }); +const getFitLineConfig = () => ({ + visible: true, + stroke: ColorVariant.Series, + opacity: 1, + dash: [], +}); + +const getLineConfig = (strokeWidth?: number) => ({ strokeWidth }); const getColor: GetColorFn = ( { yAccessor, seriesKeys }, @@ -363,15 +381,29 @@ export const getSeriesProps: GetSeriesPropsFn = ({ stackMode: isPercentage ? StackMode.Percentage : undefined, timeZone, areaSeriesStyle: { - point: getPointConfig(xColumnId, markSizeColumnId, emphasizeFitting), + point: getPointConfig({ + xAccessor: xColumnId, + markSizeAccessor: markSizeColumnId, + emphasizeFitting, + showPoints: layer.showPoints, + pointsRadius: layer.pointsRadius, + }), ...(fillOpacity && { area: { opacity: fillOpacity } }), ...(emphasizeFitting && { - fit: { area: { opacity: fillOpacity || 0.5 }, line: getLineConfig() }, + fit: { area: { opacity: fillOpacity || 0.5 }, line: getFitLineConfig() }, }), + line: getLineConfig(layer.lineWidth), }, lineSeriesStyle: { - point: getPointConfig(xColumnId, markSizeColumnId, emphasizeFitting), - ...(emphasizeFitting && { fit: { line: getLineConfig() } }), + point: getPointConfig({ + xAccessor: xColumnId, + markSizeAccessor: markSizeColumnId, + emphasizeFitting, + showPoints: layer.showPoints, + pointsRadius: layer.pointsRadius, + }), + ...(emphasizeFitting && { fit: { line: getFitLineConfig() } }), + line: getLineConfig(layer.lineWidth), }, name(d) { return getSeriesName(d, { From 2e51140d9c297abfd6394d61bff85aa0b93d9006 Mon Sep 17 00:00:00 2001 From: Katerina Patticha Date: Fri, 20 May 2022 15:34:29 +0200 Subject: [PATCH 037/120] Show service group icon only when there are service groups (#131138) * Show service group icon when there are service groups * Fix fix errors * Remove additional request and display icon only for the service groups * Revert "Remove additional request and display icon only for the service groups" This reverts commit 7ff2bc97f48914a4487998e6e66370ad8beba506. * Add dependencies Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../templates/service_group_template.tsx | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/apm/public/components/routing/templates/service_group_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/service_group_template.tsx index bcf0b44814089..006b3cc67bd5e 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/service_group_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/service_group_template.tsx @@ -11,6 +11,7 @@ import { EuiFlexItem, EuiButtonIcon, EuiLoadingContent, + EuiLoadingSpinner, } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; @@ -19,7 +20,7 @@ import { KibanaPageTemplateProps, } from '@kbn/kibana-react-plugin/public'; import { enableServiceGroups } from '@kbn/observability-plugin/public'; -import { useFetcher } from '../../../hooks/use_fetcher'; +import { useFetcher, FETCH_STATUS } from '../../../hooks/use_fetcher'; import { ApmPluginStartDeps } from '../../../plugin'; import { useApmRouter } from '../../../hooks/use_apm_router'; import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; @@ -51,17 +52,29 @@ export function ServiceGroupTemplate({ query: { serviceGroup: serviceGroupId }, } = useAnyOfApmParams('/services', '/service-map'); - const { data } = useFetcher((callApmApi) => { - if (serviceGroupId) { - return callApmApi('GET /internal/apm/service-group', { - params: { query: { serviceGroup: serviceGroupId } }, - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const { data } = useFetcher( + (callApmApi) => { + if (serviceGroupId) { + return callApmApi('GET /internal/apm/service-group', { + params: { query: { serviceGroup: serviceGroupId } }, + }); + } + }, + [serviceGroupId] + ); + + const { data: serviceGroupsData, status: serviceGroupsStatus } = useFetcher( + (callApmApi) => { + if (!serviceGroupId && isServiceGroupsEnabled) { + return callApmApi('GET /internal/apm/service-groups'); + } + }, + [serviceGroupId, isServiceGroupsEnabled] + ); const serviceGroupName = data?.serviceGroup.groupName; const loadingServiceGroupName = !!serviceGroupId && !serviceGroupName; + const hasServiceGroups = !!serviceGroupsData?.serviceGroups.length; const serviceGroupsLink = router.link('/service-groups', { query: { ...query, serviceGroup: '' }, }); @@ -74,15 +87,22 @@ export function ServiceGroupTemplate({ justifyContent="flexStart" responsive={false} > - - - + {serviceGroupsStatus === FETCH_STATUS.LOADING && ( + + + + )} + {(serviceGroupId || hasServiceGroups) && ( + + + + )} {loadingServiceGroupName ? ( From 7c37eda9ed8dfc7dd50b506ee57315a0babd779a Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Fri, 20 May 2022 15:42:28 +0200 Subject: [PATCH 038/120] [Osquery] Fix pagination issue on Alert's Osquery Flyout (#132611) --- x-pack/plugins/osquery/public/results/results_table.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index 229714eaaed99..ae0baaea7f586 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -315,8 +315,11 @@ const ResultsTableComponent: React.FC = ({ id: 'timeline', width: 38, headerCellRender: () => null, - rowCellRender: (actionProps: EuiDataGridCellValueElementProps) => { - const eventId = data[actionProps.rowIndex]._id; + rowCellRender: (actionProps) => { + const { visibleRowIndex } = actionProps as EuiDataGridCellValueElementProps & { + visibleRowIndex: number; + }; + const eventId = data[visibleRowIndex]._id; return addToTimeline({ query: ['_id', eventId], isIcon: true }); }, From 1d8bc7ede1e6e9aa4415adabfdc457a629e5cf6e Mon Sep 17 00:00:00 2001 From: Shivindera Singh Date: Fri, 20 May 2022 15:53:00 +0200 Subject: [PATCH 039/120] hasData service - hit search api in case of an error with resolve api (#132618) --- src/plugins/data_views/public/index.ts | 1 + .../data_views/public/services/has_data.ts | 61 ++++++++++++++++--- src/plugins/data_views/public/types.ts | 4 ++ 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/plugins/data_views/public/index.ts b/src/plugins/data_views/public/index.ts index f6a0843babed6..5b14ca9d25030 100644 --- a/src/plugins/data_views/public/index.ts +++ b/src/plugins/data_views/public/index.ts @@ -57,6 +57,7 @@ export type { HasDataViewsResponse, IndicesResponse, IndicesResponseModified, + IndicesViaSearchResponse, } from './types'; // Export plugin after all other imports diff --git a/src/plugins/data_views/public/services/has_data.ts b/src/plugins/data_views/public/services/has_data.ts index 76f6b39ec4982..d10f6a3d446f8 100644 --- a/src/plugins/data_views/public/services/has_data.ts +++ b/src/plugins/data_views/public/services/has_data.ts @@ -8,7 +8,12 @@ import { CoreStart, HttpStart } from '@kbn/core/public'; import { DEFAULT_ASSETS_TO_IGNORE } from '../../common'; -import { HasDataViewsResponse, IndicesResponse, IndicesResponseModified } from '..'; +import { + HasDataViewsResponse, + IndicesResponse, + IndicesResponseModified, + IndicesViaSearchResponse, +} from '..'; export class HasData { private removeAliases = (source: IndicesResponseModified): boolean => !source.item.indices; @@ -77,6 +82,41 @@ export class HasData { return source; }; + private getIndicesViaSearch = async ({ + http, + pattern, + showAllIndices, + }: { + http: HttpStart; + pattern: string; + showAllIndices: boolean; + }): Promise => + http + .post(`/internal/search/ese`, { + body: JSON.stringify({ + params: { + ignore_unavailable: true, + expand_wildcards: showAllIndices ? 'all' : 'open', + index: pattern, + body: { + size: 0, // no hits + aggs: { + indices: { + terms: { + field: '_index', + size: 200, + }, + }, + }, + }, + }, + }), + }) + .then((resp) => { + return !!(resp && resp.total >= 0); + }) + .catch(() => false); + private getIndices = async ({ http, pattern, @@ -96,26 +136,29 @@ export class HasData { } else { return this.responseToItemArray(response); } - }) - .catch(() => []); + }); private checkLocalESData = (http: HttpStart): Promise => this.getIndices({ http, pattern: '*', showAllIndices: false, - }).then((dataSources: IndicesResponseModified[]) => { - return dataSources.some(this.isUserDataIndex); - }); + }) + .then((dataSources: IndicesResponseModified[]) => { + return dataSources.some(this.isUserDataIndex); + }) + .catch(() => this.getIndicesViaSearch({ http, pattern: '*', showAllIndices: false })); private checkRemoteESData = (http: HttpStart): Promise => this.getIndices({ http, pattern: '*:*', showAllIndices: false, - }).then((dataSources: IndicesResponseModified[]) => { - return !!dataSources.filter(this.removeAliases).length; - }); + }) + .then((dataSources: IndicesResponseModified[]) => { + return !!dataSources.filter(this.removeAliases).length; + }) + .catch(() => this.getIndicesViaSearch({ http, pattern: '*:*', showAllIndices: false })); // Data Views diff --git a/src/plugins/data_views/public/types.ts b/src/plugins/data_views/public/types.ts index 612f22335e72a..f2d34961ab6e0 100644 --- a/src/plugins/data_views/public/types.ts +++ b/src/plugins/data_views/public/types.ts @@ -56,6 +56,10 @@ export interface IndicesResponse { data_streams?: IndicesResponseItemDataStream[]; } +export interface IndicesViaSearchResponse { + total: number; +} + export interface HasDataViewsResponse { hasDataView: boolean; hasUserDataView: boolean; From d34408876a67c7158f972f9ec0e493fe4f9a4e7b Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 20 May 2022 08:06:25 -0600 Subject: [PATCH 040/120] [maps] Use label features from ES vector tile search API to fix multiple labels (#132080) * [maps] mvt labels * eslint * only request labels when needed * update vector tile integration tests for hasLabels parameter * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * fix tests * fix test * only add _mvt_label_position filter when vector tiles are from ES vector tile search API * review feedback * include hasLabels in source data * fix jest test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/maps/common/mvt_request_body.ts | 6 ++ .../layers/heatmap_layer/heatmap_layer.ts | 1 + .../mvt_vector_layer/mvt_source_data.test.ts | 57 +++++++++++++++++++ .../mvt_vector_layer/mvt_source_data.ts | 11 +++- .../mvt_vector_layer/mvt_vector_layer.tsx | 1 + .../layers/vector_layer/vector_layer.tsx | 4 ++ .../es_geo_grid_source.test.ts | 4 +- .../es_geo_grid_source/es_geo_grid_source.tsx | 7 ++- .../es_search_source/es_search_source.test.ts | 4 +- .../es_search_source/es_search_source.tsx | 7 ++- .../vector_source/mvt_vector_source.ts | 6 +- .../classes/styles/vector/style_util.ts | 4 +- .../classes/styles/vector/vector_style.tsx | 14 ++++- .../classes/util/mb_filter_expressions.ts | 23 +++++--- .../components/get_tile_request.test.ts | 6 +- .../components/get_tile_request.ts | 6 ++ x-pack/plugins/maps/server/mvt/mvt_routes.ts | 4 ++ .../apis/maps/get_grid_tile.js | 37 ++++++++++++ .../api_integration/apis/maps/get_tile.js | 47 +++++++++++++++ .../apps/maps/group4/mvt_geotile_grid.js | 1 + .../apps/maps/group4/mvt_scaling.js | 1 + 21 files changed, 229 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/maps/common/mvt_request_body.ts b/x-pack/plugins/maps/common/mvt_request_body.ts index e5517b23e0cba..c2d367f89fa8a 100644 --- a/x-pack/plugins/maps/common/mvt_request_body.ts +++ b/x-pack/plugins/maps/common/mvt_request_body.ts @@ -21,6 +21,7 @@ export function getAggsTileRequest({ encodedRequestBody, geometryFieldName, gridPrecision, + hasLabels, index, renderAs = RENDER_AS.POINT, x, @@ -30,6 +31,7 @@ export function getAggsTileRequest({ encodedRequestBody: string; geometryFieldName: string; gridPrecision: number; + hasLabels: boolean; index: string; renderAs: RENDER_AS; x: number; @@ -50,6 +52,7 @@ export function getAggsTileRequest({ aggs: requestBody.aggs, fields: requestBody.fields, runtime_mappings: requestBody.runtime_mappings, + with_labels: hasLabels, }, }; } @@ -57,6 +60,7 @@ export function getAggsTileRequest({ export function getHitsTileRequest({ encodedRequestBody, geometryFieldName, + hasLabels, index, x, y, @@ -64,6 +68,7 @@ export function getHitsTileRequest({ }: { encodedRequestBody: string; geometryFieldName: string; + hasLabels: boolean; index: string; x: number; y: number; @@ -86,6 +91,7 @@ export function getHitsTileRequest({ ), runtime_mappings: requestBody.runtime_mappings, track_total_hits: typeof requestBody.size === 'number' ? requestBody.size + 1 : false, + with_labels: hasLabels, }, }; } diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts index e796ecad332ca..ec9cec3a914ba 100644 --- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts @@ -87,6 +87,7 @@ export class HeatmapLayer extends AbstractLayer { async syncData(syncContext: DataRequestContext) { await syncMvtSourceData({ + hasLabels: false, layerId: this.getId(), layerName: await this.getDisplayName(), prevDataRequest: this.getSourceDataRequest(), diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts index 1f710879d9dd7..dae0f5343dcc9 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts @@ -52,6 +52,7 @@ describe('syncMvtSourceData', () => { const syncContext = new MockSyncContext({ dataFilters: {} }); await syncMvtSourceData({ + hasLabels: false, layerId: 'layer1', layerName: 'my layer', prevDataRequest: undefined, @@ -82,6 +83,7 @@ describe('syncMvtSourceData', () => { tileSourceLayer: 'aggs', tileUrl: 'https://example.com/{x}/{y}/{z}.pbf', refreshToken: '12345', + hasLabels: false, }); }); @@ -99,6 +101,7 @@ describe('syncMvtSourceData', () => { }; await syncMvtSourceData({ + hasLabels: false, layerId: 'layer1', layerName: 'my layer', prevDataRequest: { @@ -112,6 +115,7 @@ describe('syncMvtSourceData', () => { tileSourceLayer: 'aggs', tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', refreshToken: '12345', + hasLabels: false, }; }, } as unknown as DataRequest, @@ -142,6 +146,7 @@ describe('syncMvtSourceData', () => { }; await syncMvtSourceData({ + hasLabels: false, layerId: 'layer1', layerName: 'my layer', prevDataRequest: { @@ -155,6 +160,7 @@ describe('syncMvtSourceData', () => { tileSourceLayer: 'aggs', tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', refreshToken: '12345', + hasLabels: false, }; }, } as unknown as DataRequest, @@ -182,6 +188,7 @@ describe('syncMvtSourceData', () => { }; await syncMvtSourceData({ + hasLabels: false, layerId: 'layer1', layerName: 'my layer', prevDataRequest: { @@ -195,6 +202,7 @@ describe('syncMvtSourceData', () => { tileSourceLayer: 'aggs', tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', refreshToken: '12345', + hasLabels: false, }; }, } as unknown as DataRequest, @@ -230,6 +238,7 @@ describe('syncMvtSourceData', () => { }; await syncMvtSourceData({ + hasLabels: false, layerId: 'layer1', layerName: 'my layer', prevDataRequest: { @@ -243,6 +252,7 @@ describe('syncMvtSourceData', () => { tileSourceLayer: 'barfoo', // tileSourceLayer is different then mockSource tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', refreshToken: '12345', + hasLabels: false, }; }, } as unknown as DataRequest, @@ -270,6 +280,7 @@ describe('syncMvtSourceData', () => { }; await syncMvtSourceData({ + hasLabels: false, layerId: 'layer1', layerName: 'my layer', prevDataRequest: { @@ -283,6 +294,7 @@ describe('syncMvtSourceData', () => { tileSourceLayer: 'aggs', tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', refreshToken: '12345', + hasLabels: false, }; }, } as unknown as DataRequest, @@ -310,6 +322,7 @@ describe('syncMvtSourceData', () => { }; await syncMvtSourceData({ + hasLabels: false, layerId: 'layer1', layerName: 'my layer', prevDataRequest: { @@ -323,6 +336,49 @@ describe('syncMvtSourceData', () => { tileSourceLayer: 'aggs', tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', refreshToken: '12345', + hasLabels: false, + }; + }, + } as unknown as DataRequest, + requestMeta: { ...prevRequestMeta }, + source: mockSource, + syncContext, + }); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.startLoading); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.stopLoading); + }); + + test('Should re-sync when hasLabel state changes', async () => { + const syncContext = new MockSyncContext({ dataFilters: {} }); + const prevRequestMeta = { + ...syncContext.dataFilters, + applyGlobalQuery: true, + applyGlobalTime: true, + applyForceRefresh: true, + fieldNames: [], + sourceMeta: {}, + isForceRefresh: false, + isFeatureEditorOpenForLayer: false, + }; + + await syncMvtSourceData({ + hasLabels: true, + layerId: 'layer1', + layerName: 'my layer', + prevDataRequest: { + getMeta: () => { + return prevRequestMeta; + }, + getData: () => { + return { + tileMinZoom: 4, + tileMaxZoom: 14, + tileSourceLayer: 'aggs', + tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', + refreshToken: '12345', + hasLabels: false, }; }, } as unknown as DataRequest, @@ -340,6 +396,7 @@ describe('syncMvtSourceData', () => { const syncContext = new MockSyncContext({ dataFilters: {} }); await syncMvtSourceData({ + hasLabels: false, layerId: 'layer1', layerName: 'my layer', prevDataRequest: undefined, diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts index 76550090109a1..19ad39e41a238 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts @@ -20,9 +20,11 @@ export interface MvtSourceData { tileMaxZoom: number; tileUrl: string; refreshToken: string; + hasLabels: boolean; } export async function syncMvtSourceData({ + hasLabels, layerId, layerName, prevDataRequest, @@ -30,6 +32,7 @@ export async function syncMvtSourceData({ source, syncContext, }: { + hasLabels: boolean; layerId: string; layerName: string; prevDataRequest: DataRequest | undefined; @@ -56,7 +59,10 @@ export async function syncMvtSourceData({ }, }); const canSkip = - !syncContext.forceRefreshDueToDrawing && noChangesInSourceState && noChangesInSearchState; + !syncContext.forceRefreshDueToDrawing && + noChangesInSourceState && + noChangesInSearchState && + prevData.hasLabels === hasLabels; if (canSkip) { return; @@ -72,7 +78,7 @@ export async function syncMvtSourceData({ ? uuid() : prevData.refreshToken; - const tileUrl = await source.getTileUrl(requestMeta, refreshToken); + const tileUrl = await source.getTileUrl(requestMeta, refreshToken, hasLabels); if (source.isESSource()) { syncContext.inspectorAdapters.vectorTiles.addLayer(layerId, layerName, tileUrl); } @@ -82,6 +88,7 @@ export async function syncMvtSourceData({ tileMinZoom: source.getMinZoom(), tileMaxZoom: source.getMaxZoom(), refreshToken, + hasLabels, }; syncContext.stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, sourceData, {}); } catch (error) { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx index 462ea5b0cc8f1..7eaec94eac0a2 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx @@ -219,6 +219,7 @@ export class MvtVectorLayer extends AbstractVectorLayer { await this._syncSupportsFeatureEditing({ syncContext, source: this.getSource() }); await syncMvtSourceData({ + hasLabels: this.getCurrentStyle().hasLabels(), layerId: this.getId(), layerName: await this.getDisplayName(), prevDataRequest: this.getSourceDataRequest(), diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 82ca62c7f33df..73e036b105730 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -736,7 +736,10 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { } } + const isSourceGeoJson = !this.getSource().isMvt(); const filterExpr = getPointFilterExpression( + isSourceGeoJson, + this.getSource().isESSource(), this._getJoinFilterExpression(), timesliceMaskConfig ); @@ -843,6 +846,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { const isSourceGeoJson = !this.getSource().isMvt(); const filterExpr = getLabelFilterExpression( isSourceGeoJson, + this.getSource().isESSource(), this._getJoinFilterExpression(), timesliceMaskConfig ); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index b08b95a58a495..831dc90871dff 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -306,10 +306,10 @@ describe('ESGeoGridSource', () => { }); it('getTileUrl', async () => { - const tileUrl = await mvtGeogridSource.getTileUrl(vectorSourceRequestMeta, '1234'); + const tileUrl = await mvtGeogridSource.getTileUrl(vectorSourceRequestMeta, '1234', false); expect(tileUrl).toEqual( - "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&gridPrecision=8&requestBody=(foobar%3AES_DSL_PLACEHOLDER%2Cparams%3A('0'%3A('0'%3Aindex%2C'1'%3A(fields%3A()))%2C'1'%3A('0'%3Asize%2C'1'%3A0)%2C'2'%3A('0'%3Afilter%2C'1'%3A!())%2C'3'%3A('0'%3Aquery)%2C'4'%3A('0'%3Aindex%2C'1'%3A(fields%3A()))%2C'5'%3A('0'%3Aquery%2C'1'%3A(language%3AKQL%2Cquery%3A''))%2C'6'%3A('0'%3Aaggs%2C'1'%3A())))&renderAs=heatmap&token=1234" + "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&gridPrecision=8&hasLabels=false&requestBody=(foobar%3AES_DSL_PLACEHOLDER%2Cparams%3A('0'%3A('0'%3Aindex%2C'1'%3A(fields%3A()))%2C'1'%3A('0'%3Asize%2C'1'%3A0)%2C'2'%3A('0'%3Afilter%2C'1'%3A!())%2C'3'%3A('0'%3Aquery)%2C'4'%3A('0'%3Aindex%2C'1'%3A(fields%3A()))%2C'5'%3A('0'%3Aquery%2C'1'%3A(language%3AKQL%2Cquery%3A''))%2C'6'%3A('0'%3Aaggs%2C'1'%3A())))&renderAs=heatmap&token=1234" ); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx index 66a07804c0105..1680b1d2fb55c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx @@ -471,7 +471,11 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo return 'aggs'; } - async getTileUrl(searchFilters: VectorSourceRequestMeta, refreshToken: string): Promise { + async getTileUrl( + searchFilters: VectorSourceRequestMeta, + refreshToken: string, + hasLabels: boolean + ): Promise { const indexPattern = await this.getIndexPattern(); const searchSource = await this.makeSearchSource(searchFilters, 0); searchSource.setField('aggs', this.getValueAggsDsl(indexPattern)); @@ -484,6 +488,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo ?geometryFieldName=${this._descriptor.geoField}\ &index=${indexPattern.title}\ &gridPrecision=${this._getGeoGridPrecisionResolutionDelta()}\ +&hasLabels=${hasLabels}\ &requestBody=${encodeMvtResponseBody(searchSource.getSearchRequestBody())}\ &renderAs=${this._descriptor.requestType}\ &token=${refreshToken}`; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts index 2df2e119df30c..24470ae0fade7 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts @@ -114,9 +114,9 @@ describe('ESSearchSource', () => { geoField: geoFieldName, indexPatternId: 'ipId', }); - const tileUrl = await esSearchSource.getTileUrl(searchFilters, '1234'); + const tileUrl = await esSearchSource.getTileUrl(searchFilters, '1234', false); expect(tileUrl).toBe( - `rootdir/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar%3AES_DSL_PLACEHOLDER%2Cparams%3A('0'%3A('0'%3Aindex%2C'1'%3A(fields%3A()%2Ctitle%3A'foobar-title-*'))%2C'1'%3A('0'%3Asize%2C'1'%3A1000)%2C'2'%3A('0'%3Afilter%2C'1'%3A!())%2C'3'%3A('0'%3Aquery)%2C'4'%3A('0'%3Aindex%2C'1'%3A(fields%3A()%2Ctitle%3A'foobar-title-*'))%2C'5'%3A('0'%3Aquery%2C'1'%3A(language%3AKQL%2Cquery%3A'tooltipField%3A%20foobar'))%2C'6'%3A('0'%3AfieldsFromSource%2C'1'%3A!(tooltipField%2CstyleField))%2C'7'%3A('0'%3Asource%2C'1'%3A!(tooltipField%2CstyleField))))&token=1234` + `rootdir/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=foobar-title-*&hasLabels=false&requestBody=(foobar%3AES_DSL_PLACEHOLDER%2Cparams%3A('0'%3A('0'%3Aindex%2C'1'%3A(fields%3A()%2Ctitle%3A'foobar-title-*'))%2C'1'%3A('0'%3Asize%2C'1'%3A1000)%2C'2'%3A('0'%3Afilter%2C'1'%3A!())%2C'3'%3A('0'%3Aquery)%2C'4'%3A('0'%3Aindex%2C'1'%3A(fields%3A()%2Ctitle%3A'foobar-title-*'))%2C'5'%3A('0'%3Aquery%2C'1'%3A(language%3AKQL%2Cquery%3A'tooltipField%3A%20foobar'))%2C'6'%3A('0'%3AfieldsFromSource%2C'1'%3A!(tooltipField%2CstyleField))%2C'7'%3A('0'%3Asource%2C'1'%3A!(tooltipField%2CstyleField))))&token=1234` ); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 52b9675cdbb39..b8982042b2365 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -810,7 +810,11 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource return 'hits'; } - async getTileUrl(searchFilters: VectorSourceRequestMeta, refreshToken: string): Promise { + async getTileUrl( + searchFilters: VectorSourceRequestMeta, + refreshToken: string, + hasLabels: boolean + ): Promise { const indexPattern = await this.getIndexPattern(); const indexSettings = await loadIndexSettings(indexPattern.title); @@ -847,6 +851,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource return `${mvtUrlServicePath}\ ?geometryFieldName=${this._descriptor.geoField}\ &index=${indexPattern.title}\ +&hasLabels=${hasLabels}\ &requestBody=${encodeMvtResponseBody(searchSource.getSearchRequestBody())}\ &token=${refreshToken}`; } diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/mvt_vector_source.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/mvt_vector_source.ts index fca72af193ca3..c6f55436efc15 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/mvt_vector_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/mvt_vector_source.ts @@ -13,7 +13,11 @@ export interface IMvtVectorSource extends IVectorSource { * IMvtVectorSource.getTileUrl returns the tile source URL. * Append refreshToken as a URL parameter to force tile re-fetch on refresh (not required) */ - getTileUrl(searchFilters: VectorSourceRequestMeta, refreshToken: string): Promise; + getTileUrl( + searchFilters: VectorSourceRequestMeta, + refreshToken: string, + hasLabels: boolean + ): Promise; /* * Tile vector sources can contain multiple layers. For example, elasticsearch _mvt tiles contain the layers "hits", "aggs", and "meta". diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts b/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts index 5d4d5bc3ecbfb..905bc63fb078b 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts @@ -94,9 +94,9 @@ export function makeMbClampedNumberExpression({ ]; } -export function getHasLabel(label: StaticTextProperty | DynamicTextProperty) { +export function getHasLabel(label: StaticTextProperty | DynamicTextProperty): boolean { return label.isDynamic() ? label.isComplete() : (label as StaticTextProperty).getOptions().value != null && - (label as StaticTextProperty).getOptions().value.length; + (label as StaticTextProperty).getOptions().value.length > 0; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index d9a296031b5a1..7ce9673fdc10e 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -115,6 +115,12 @@ export interface IVectorStyle extends IStyle { mbMap: MbMap, mbSourceId: string ) => boolean; + + /* + * Returns true when "Label" style configuration is complete and map shows a label for layer features. + */ + hasLabels: () => boolean; + arePointsSymbolizedAsCircles: () => boolean; setMBPaintProperties: ({ alpha, @@ -674,14 +680,14 @@ export class VectorStyle implements IVectorStyle { } _getLegendDetailStyleProperties = () => { - const hasLabel = getHasLabel(this._labelStyleProperty); + const hasLabels = this.hasLabels(); return this.getDynamicPropertiesArray().filter((styleProperty) => { const styleName = styleProperty.getStyleName(); if ([VECTOR_STYLES.ICON_ORIENTATION, VECTOR_STYLES.LABEL_TEXT].includes(styleName)) { return false; } - if (!hasLabel && LABEL_STYLES.includes(styleName)) { + if (!hasLabels && LABEL_STYLES.includes(styleName)) { // do not render legend for label styles when there is no label return false; } @@ -768,6 +774,10 @@ export class VectorStyle implements IVectorStyle { return !this._symbolizeAsStyleProperty.isSymbolizedAsIcon(); } + hasLabels() { + return getHasLabel(this._labelStyleProperty); + } + setMBPaintProperties({ alpha, mbMap, diff --git a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts index 2f25dc84fe224..a86ca84901cd9 100644 --- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts +++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts @@ -55,7 +55,7 @@ export function getFillFilterExpression( ): FilterSpecification { return getFilterExpression( [ - // explicit EXCLUDE_CENTROID_FEATURES filter not needed. Centroids are points and are filtered out by geometry narrowing + // explicit "exclude centroid features" filter not needed. Label features are points and are filtered out by geometry narrowing [ 'any', ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], @@ -73,7 +73,7 @@ export function getLineFilterExpression( ): FilterSpecification { return getFilterExpression( [ - // explicit EXCLUDE_CENTROID_FEATURES filter not needed. Centroids are points and are filtered out by geometry narrowing + // explicit "exclude centroid features" filter not needed. Label features are points and are filtered out by geometry narrowing [ 'any', ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], @@ -94,18 +94,25 @@ const IS_POINT_FEATURE = [ ]; export function getPointFilterExpression( + isSourceGeoJson: boolean, + isESSource: boolean, joinFilter?: FilterSpecification, timesliceMaskConfig?: TimesliceMaskConfig ): FilterSpecification { - return getFilterExpression( - [EXCLUDE_CENTROID_FEATURES, IS_POINT_FEATURE], - joinFilter, - timesliceMaskConfig - ); + const filters: FilterSpecification[] = []; + if (isSourceGeoJson) { + filters.push(EXCLUDE_CENTROID_FEATURES); + } else if (!isSourceGeoJson && isESSource) { + filters.push(['!=', ['get', '_mvt_label_position'], true]); + } + filters.push(IS_POINT_FEATURE); + + return getFilterExpression(filters, joinFilter, timesliceMaskConfig); } export function getLabelFilterExpression( isSourceGeoJson: boolean, + isESSource: boolean, joinFilter?: FilterSpecification, timesliceMaskConfig?: TimesliceMaskConfig ): FilterSpecification { @@ -116,6 +123,8 @@ export function getLabelFilterExpression( // For GeoJSON sources, show label for centroid features or point/multi-point features only. // no explicit isCentroidFeature filter is needed, centroids are points and are included in the geometry filter. filters.push(IS_POINT_FEATURE); + } else if (!isSourceGeoJson && isESSource) { + filters.push(['==', ['get', '_mvt_label_position'], true]); } return getFilterExpression(filters, joinFilter, timesliceMaskConfig); diff --git a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.test.ts b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.test.ts index a45be3cf80ec0..4534c8047409d 100644 --- a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.test.ts +++ b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.test.ts @@ -11,7 +11,7 @@ test('Should return elasticsearch vector tile request for aggs tiles', () => { expect( getTileRequest({ layerId: '1', - tileUrl: `/pof/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=kibana_sample_data_logs&gridPrecision=8&requestBody=(_source%3A(excludes%3A!())%2Caggs%3A()%2Cfields%3A!((field%3A'%40timestamp'%2Cformat%3Adate_time)%2C(field%3Atimestamp%2Cformat%3Adate_time)%2C(field%3Autc_time%2Cformat%3Adate_time))%2Cquery%3A(bool%3A(filter%3A!((match_phrase%3A(machine.os.keyword%3Aios))%2C(range%3A(timestamp%3A(format%3Astrict_date_optional_time%2Cgte%3A'2022-04-22T16%3A46%3A00.744Z'%2Clte%3A'2022-04-29T16%3A46%3A05.345Z'))))%2Cmust%3A!()%2Cmust_not%3A!()%2Cshould%3A!()))%2Cruntime_mappings%3A(hour_of_day%3A(script%3A(source%3A'emit(doc%5B!'timestamp!'%5D.value.getHour())%3B')%2Ctype%3Along))%2Cscript_fields%3A()%2Csize%3A0%2Cstored_fields%3A!('*'))&renderAs=heatmap&token=e8bff005-ccea-464a-ae56-2061b4f8ce68`, + tileUrl: `/pof/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&hasLabels=false&index=kibana_sample_data_logs&gridPrecision=8&requestBody=(_source%3A(excludes%3A!())%2Caggs%3A()%2Cfields%3A!((field%3A'%40timestamp'%2Cformat%3Adate_time)%2C(field%3Atimestamp%2Cformat%3Adate_time)%2C(field%3Autc_time%2Cformat%3Adate_time))%2Cquery%3A(bool%3A(filter%3A!((match_phrase%3A(machine.os.keyword%3Aios))%2C(range%3A(timestamp%3A(format%3Astrict_date_optional_time%2Cgte%3A'2022-04-22T16%3A46%3A00.744Z'%2Clte%3A'2022-04-29T16%3A46%3A05.345Z'))))%2Cmust%3A!()%2Cmust_not%3A!()%2Cshould%3A!()))%2Cruntime_mappings%3A(hour_of_day%3A(script%3A(source%3A'emit(doc%5B!'timestamp!'%5D.value.getHour())%3B')%2Ctype%3Along))%2Cscript_fields%3A()%2Csize%3A0%2Cstored_fields%3A!('*'))&renderAs=heatmap&token=e8bff005-ccea-464a-ae56-2061b4f8ce68`, x: 3, y: 0, z: 2, @@ -71,6 +71,7 @@ test('Should return elasticsearch vector tile request for aggs tiles', () => { type: 'long', }, }, + with_labels: false, }, }); }); @@ -79,7 +80,7 @@ test('Should return elasticsearch vector tile request for hits tiles', () => { expect( getTileRequest({ layerId: '1', - tileUrl: `http://localhost:5601/pof/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=kibana_sample_data_logs&requestBody=(_source%3A!f%2Cdocvalue_fields%3A!()%2Cquery%3A(bool%3A(filter%3A!((range%3A(timestamp%3A(format%3Astrict_date_optional_time%2Cgte%3A%272022-04-22T16%3A46%3A00.744Z%27%2Clte%3A%272022-04-29T16%3A46%3A05.345Z%27))))%2Cmust%3A!()%2Cmust_not%3A!()%2Cshould%3A!()))%2Cruntime_mappings%3A(hour_of_day%3A(script%3A(source%3A%27emit(doc%5B!%27timestamp!%27%5D.value.getHour())%3B%27)%2Ctype%3Along))%2Cscript_fields%3A()%2Csize%3A10000%2Cstored_fields%3A!(geo.coordinates))&token=415049b6-bb0a-444a-a7b9-89717db5183c`, + tileUrl: `http://localhost:5601/pof/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&hasLabels=true&index=kibana_sample_data_logs&requestBody=(_source%3A!f%2Cdocvalue_fields%3A!()%2Cquery%3A(bool%3A(filter%3A!((range%3A(timestamp%3A(format%3Astrict_date_optional_time%2Cgte%3A%272022-04-22T16%3A46%3A00.744Z%27%2Clte%3A%272022-04-29T16%3A46%3A05.345Z%27))))%2Cmust%3A!()%2Cmust_not%3A!()%2Cshould%3A!()))%2Cruntime_mappings%3A(hour_of_day%3A(script%3A(source%3A%27emit(doc%5B!%27timestamp!%27%5D.value.getHour())%3B%27)%2Ctype%3Along))%2Cscript_fields%3A()%2Csize%3A10000%2Cstored_fields%3A!(geo.coordinates))&token=415049b6-bb0a-444a-a7b9-89717db5183c`, x: 0, y: 0, z: 2, @@ -118,6 +119,7 @@ test('Should return elasticsearch vector tile request for hits tiles', () => { }, }, track_total_hits: 10001, + with_labels: true, }, }); }); diff --git a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.ts b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.ts index f483dfda23409..c79ef7c64fdd1 100644 --- a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.ts +++ b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.ts @@ -35,11 +35,16 @@ export function getTileRequest(tileRequest: TileRequest): { path?: string; body? } const geometryFieldName = searchParams.get('geometryFieldName') as string; + const hasLabels = searchParams.has('hasLabels') + ? searchParams.get('hasLabels') === 'true' + : false; + if (tileRequest.tileUrl.includes(MVT_GETGRIDTILE_API_PATH)) { return getAggsTileRequest({ encodedRequestBody, geometryFieldName, gridPrecision: parseInt(searchParams.get('gridPrecision') as string, 10), + hasLabels, index, renderAs: searchParams.get('renderAs') as RENDER_AS, x: tileRequest.x, @@ -52,6 +57,7 @@ export function getTileRequest(tileRequest: TileRequest): { path?: string; body? return getHitsTileRequest({ encodedRequestBody, geometryFieldName, + hasLabels, index, x: tileRequest.x, y: tileRequest.y, diff --git a/x-pack/plugins/maps/server/mvt/mvt_routes.ts b/x-pack/plugins/maps/server/mvt/mvt_routes.ts index 8af26548b1d28..6fd7374fb69c1 100644 --- a/x-pack/plugins/maps/server/mvt/mvt_routes.ts +++ b/x-pack/plugins/maps/server/mvt/mvt_routes.ts @@ -44,6 +44,7 @@ export function initMVTRoutes({ }), query: schema.object({ geometryFieldName: schema.string(), + hasLabels: schema.boolean(), requestBody: schema.string(), index: schema.string(), token: schema.maybe(schema.string()), @@ -65,6 +66,7 @@ export function initMVTRoutes({ tileRequest = getHitsTileRequest({ encodedRequestBody: query.requestBody as string, geometryFieldName: query.geometryFieldName as string, + hasLabels: query.hasLabels as boolean, index: query.index as string, x, y, @@ -102,6 +104,7 @@ export function initMVTRoutes({ }), query: schema.object({ geometryFieldName: schema.string(), + hasLabels: schema.boolean(), requestBody: schema.string(), index: schema.string(), renderAs: schema.string(), @@ -126,6 +129,7 @@ export function initMVTRoutes({ encodedRequestBody: query.requestBody as string, geometryFieldName: query.geometryFieldName as string, gridPrecision: parseInt(query.gridPrecision, 10), + hasLabels: query.hasLabels as boolean, index: query.index as string, renderAs: query.renderAs as RENDER_AS, x, diff --git a/x-pack/test/api_integration/apis/maps/get_grid_tile.js b/x-pack/test/api_integration/apis/maps/get_grid_tile.js index 46fdda09ec476..26ba8c24ce71a 100644 --- a/x-pack/test/api_integration/apis/maps/get_grid_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_grid_tile.js @@ -9,12 +9,22 @@ import { VectorTile } from '@mapbox/vector-tile'; import Protobuf from 'pbf'; import expect from '@kbn/expect'; +function findFeature(layer, callbackFn) { + for (let i = 0; i < layer.length; i++) { + const feature = layer.feature(i); + if (callbackFn(feature)) { + return feature; + } + } +} + export default function ({ getService }) { const supertest = getService('supertest'); describe('getGridTile', () => { const URL = `/api/maps/mvt/getGridTile/3/2/3.pbf\ ?geometryFieldName=geo.coordinates\ +&hasLabels=false\ &index=logstash-*\ &gridPrecision=8\ &requestBody=(_source:(excludes:!()),aggs:(avg_of_bytes:(avg:(field:bytes))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))`; @@ -152,6 +162,33 @@ export default function ({ getService }) { ]); }); + it('should return vector tile containing label features when hasLabels is true', async () => { + const resp = await supertest + .get(URL.replace('hasLabels=false', 'hasLabels=true') + '&renderAs=hex') + .set('kbn-xsrf', 'kibana') + .responseType('blob') + .expect(200); + + const jsonTile = new VectorTile(new Protobuf(resp.body)); + const layer = jsonTile.layers.aggs; + expect(layer.length).to.be(2); + + const labelFeature = findFeature(layer, (feature) => { + return feature.properties._mvt_label_position === true; + }); + expect(labelFeature).not.to.be(undefined); + expect(labelFeature.type).to.be(1); + expect(labelFeature.extent).to.be(4096); + expect(labelFeature.id).to.be(undefined); + expect(labelFeature.properties).to.eql({ + _count: 1, + _key: '85264a33fffffff', + 'avg_of_bytes.value': 9252, + _mvt_label_position: true, + }); + expect(labelFeature.loadGeometry()).to.eql([[{ x: 93, y: 667 }]]); + }); + it('should return vector tile with meta layer', async () => { const resp = await supertest .get(URL + '&renderAs=point') diff --git a/x-pack/test/api_integration/apis/maps/get_tile.js b/x-pack/test/api_integration/apis/maps/get_tile.js index 09b8bf1d8b862..6803b5e404ab0 100644 --- a/x-pack/test/api_integration/apis/maps/get_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_tile.js @@ -27,6 +27,7 @@ export default function ({ getService }) { .get( `/api/maps/mvt/getTile/2/1/1.pbf\ ?geometryFieldName=geo.coordinates\ +&hasLabels=false\ &index=logstash-*\ &requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw,(field:'@timestamp',format:epoch_millis)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10000,stored_fields:!(bytes,geo.coordinates,machine.os.raw,'@timestamp'))` ) @@ -85,11 +86,57 @@ export default function ({ getService }) { ]); }); + it('should return ES vector tile containing label features when hasLabels is true', async () => { + const resp = await supertest + .get( + `/api/maps/mvt/getTile/2/1/1.pbf\ +?geometryFieldName=geo.coordinates\ +&hasLabels=true\ +&index=logstash-*\ +&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw,(field:'@timestamp',format:epoch_millis)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10000,stored_fields:!(bytes,geo.coordinates,machine.os.raw,'@timestamp'))` + ) + .set('kbn-xsrf', 'kibana') + .responseType('blob') + .expect(200); + + expect(resp.headers['content-encoding']).to.be('gzip'); + expect(resp.headers['content-disposition']).to.be('inline'); + expect(resp.headers['content-type']).to.be('application/x-protobuf'); + expect(resp.headers['cache-control']).to.be('public, max-age=3600'); + + const jsonTile = new VectorTile(new Protobuf(resp.body)); + const layer = jsonTile.layers.hits; + expect(layer.length).to.be(4); // 2 docs + 2 label features + + // Verify ES document + + const feature = findFeature(layer, (feature) => { + return ( + feature.properties._id === 'AU_x3_BsGFA8no6Qjjug' && + feature.properties._mvt_label_position === true + ); + }); + expect(feature).not.to.be(undefined); + expect(feature.type).to.be(1); + expect(feature.extent).to.be(4096); + expect(feature.id).to.be(undefined); + expect(feature.properties).to.eql({ + '@timestamp': '1442709961071', + _id: 'AU_x3_BsGFA8no6Qjjug', + _index: 'logstash-2015.09.20', + bytes: 9252, + 'machine.os.raw': 'ios', + _mvt_label_position: true, + }); + expect(feature.loadGeometry()).to.eql([[{ x: 44, y: 2382 }]]); + }); + it('should return error when index does not exist', async () => { await supertest .get( `/api/maps/mvt/getTile/2/1/1.pbf\ ?geometryFieldName=geo.coordinates\ +&hasLabels=false\ &index=notRealIndex\ &requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw,(field:'@timestamp',format:epoch_millis)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10000,stored_fields:!(bytes,geo.coordinates,machine.os.raw,'@timestamp'))` ) diff --git a/x-pack/test/functional/apps/maps/group4/mvt_geotile_grid.js b/x-pack/test/functional/apps/maps/group4/mvt_geotile_grid.js index 40dfa5ac8e571..66eb54278e580 100644 --- a/x-pack/test/functional/apps/maps/group4/mvt_geotile_grid.js +++ b/x-pack/test/functional/apps/maps/group4/mvt_geotile_grid.js @@ -45,6 +45,7 @@ export default function ({ getPageObjects, getService }) { expect(searchParams).to.eql({ geometryFieldName: 'geo.coordinates', + hasLabels: 'false', index: 'logstash-*', gridPrecision: 8, renderAs: 'grid', diff --git a/x-pack/test/functional/apps/maps/group4/mvt_scaling.js b/x-pack/test/functional/apps/maps/group4/mvt_scaling.js index 0f74752d01136..5f740e9137cdb 100644 --- a/x-pack/test/functional/apps/maps/group4/mvt_scaling.js +++ b/x-pack/test/functional/apps/maps/group4/mvt_scaling.js @@ -50,6 +50,7 @@ export default function ({ getPageObjects, getService }) { expect(searchParams).to.eql({ geometryFieldName: 'geometry', + hasLabels: 'false', index: 'geo_shapes*', requestBody: '(_source:!f,docvalue_fields:!(prop1),query:(bool:(filter:!(),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))', From bc31053dc9e5cca9bdf344f8690bf9a0e3c043ac Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Fri, 20 May 2022 17:09:20 +0300 Subject: [PATCH 041/120] [Discover][Alerting] Implement editing of dataView, query & filters (#131688) * [Discover] introduce params editing using unified search * [Discover] fix unit tests * [Discover] fix functional tests * [Discover] fix unit tests * [Discover] return test subject name * [Discover] fix alert functional test * Update x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx Co-authored-by: Julia Rechkunova * Update x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx Co-authored-by: Matthias Wilhelm * [Discover] hide filter panel options * [Discover] improve functional test * [Discover] apply suggestions * [Discover] change data view selector * [Discover] fix tests * [Discover] apply suggestions, fix lang mode toggler * [Discover] mote interface to types file, clean up diff * [Discover] fix saved query issue * Update x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts Co-authored-by: Matthias Wilhelm * [Discover] remove zIndex * [Discover] omit null searchType from esQuery completely, add isEsQueryAlert check for useSavedObjectReferences hook * [Discover] set searchType to esQuery when needed * [Discover] fix unit tests * Update x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts Co-authored-by: Matthias Wilhelm * Update x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts Co-authored-by: Matthias Wilhelm Co-authored-by: Julia Rechkunova Co-authored-by: Matthias Wilhelm --- src/plugins/data/public/mocks.ts | 1 + src/plugins/data/public/query/mocks.ts | 2 +- src/plugins/data_views/public/mocks.ts | 1 + .../components/top_nav/get_top_nav_links.tsx | 1 + .../top_nav/open_alerts_popover.tsx | 14 +- .../filter_bar/filter_item/filter_item.tsx | 14 - .../public/filter_bar/filter_view/index.tsx | 62 ++-- .../query_string_input/query_bar_top_row.tsx | 3 + .../public/search_bar/search_bar.tsx | 4 + x-pack/plugins/stack_alerts/kibana.json | 3 +- .../data_view_select_popover.test.tsx | 75 +++++ .../components/data_view_select_popover.tsx | 120 ++++++++ .../public/alert_types/es_query/constants.ts | 15 + .../expression/es_query_expression.tsx | 1 + .../es_query/expression/expression.tsx | 41 +-- .../expression/read_only_filter_items.tsx | 66 ----- .../search_source_expression.test.tsx | 133 +++++---- .../expression/search_source_expression.tsx | 219 ++++---------- .../search_source_expression_form.tsx | 269 ++++++++++++++++++ .../public/alert_types/es_query/types.ts | 20 +- .../public/alert_types/es_query/util.ts | 5 +- .../public/alert_types/es_query/validation.ts | 16 +- ...inment_alert_type_expression.test.tsx.snap | 3 + .../es_query/action_context.test.ts | 2 + .../alert_types/es_query/alert_type.test.ts | 9 + .../server/alert_types/es_query/alert_type.ts | 24 +- .../es_query/alert_type_params.test.ts | 1 + .../alert_types/es_query/alert_type_params.ts | 29 +- .../alert_types/es_query/executor.test.ts | 1 + .../server/alert_types/es_query/executor.ts | 7 +- .../server/alert_types/es_query/types.ts | 4 +- .../server/alert_types/es_query/util.ts | 12 + .../sections/rule_form/rule_form.tsx | 6 +- .../apps/discover/search_source_alert.ts | 31 +- 34 files changed, 799 insertions(+), 415 deletions(-) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/components/data_view_select_popover.test.tsx create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/components/data_view_select_popover.tsx delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/util.ts diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 27e365ce0cb37..e1b42b7c193e2 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -38,6 +38,7 @@ const createStartContract = (): Start => { }), get: jest.fn().mockReturnValue(Promise.resolve({})), clearCache: jest.fn(), + getIdsWithTitle: jest.fn(), } as unknown as DataViewsContract; return { diff --git a/src/plugins/data/public/query/mocks.ts b/src/plugins/data/public/query/mocks.ts index a2d73e5b5ce34..296a61afef2fd 100644 --- a/src/plugins/data/public/query/mocks.ts +++ b/src/plugins/data/public/query/mocks.ts @@ -32,7 +32,7 @@ const createStartContractMock = () => { addToQueryLog: jest.fn(), filterManager: createFilterManagerMock(), queryString: queryStringManagerMock.createStartContract(), - savedQueries: jest.fn() as any, + savedQueries: { getSavedQuery: jest.fn() } as any, state$: new Observable(), getState: jest.fn(), timefilter: timefilterServiceMock.createStartContract(), diff --git a/src/plugins/data_views/public/mocks.ts b/src/plugins/data_views/public/mocks.ts index 61713c9406c23..3767c93be10e6 100644 --- a/src/plugins/data_views/public/mocks.ts +++ b/src/plugins/data_views/public/mocks.ts @@ -28,6 +28,7 @@ const createStartContract = (): Start => { get: jest.fn().mockReturnValue(Promise.resolve({})), clearCache: jest.fn(), getCanSaveSync: jest.fn(), + getIdsWithTitle: jest.fn(), } as unknown as jest.Mocked; }; diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index f2ac0d2bfa060..ee35e10b6631a 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -74,6 +74,7 @@ export const getTopNavLinks = ({ anchorElement, searchSource: savedSearch.searchSource, services, + savedQueryId: state.appStateContainer.getState().savedQuery, }); }, testId: 'discoverAlertsButton', diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index d414919e567f9..71a0ef3df1b8c 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -26,9 +26,15 @@ interface AlertsPopoverProps { onClose: () => void; anchorElement: HTMLElement; searchSource: ISearchSource; + savedQueryId?: string; } -export function AlertsPopover({ searchSource, anchorElement, onClose }: AlertsPopoverProps) { +export function AlertsPopover({ + searchSource, + anchorElement, + savedQueryId, + onClose, +}: AlertsPopoverProps) { const dataView = searchSource.getField('index')!; const services = useDiscoverServices(); const { triggersActionsUi } = services; @@ -49,8 +55,9 @@ export function AlertsPopover({ searchSource, anchorElement, onClose }: AlertsPo return { searchType: 'searchSource', searchConfiguration: nextSearchSource.getSerializedFields(), + savedQueryId, }; - }, [searchSource, services]); + }, [savedQueryId, searchSource, services]); const SearchThresholdAlertFlyout = useMemo(() => { if (!alertFlyoutVisible) { @@ -156,11 +163,13 @@ export function openAlertsPopover({ anchorElement, searchSource, services, + savedQueryId, }: { I18nContext: I18nStart['Context']; anchorElement: HTMLElement; searchSource: ISearchSource; services: DiscoverServices; + savedQueryId?: string; }) { if (isOpen) { closeAlertsPopover(); @@ -177,6 +186,7 @@ export function openAlertsPopover({ onClose={closeAlertsPopover} anchorElement={anchorElement} searchSource={searchSource} + savedQueryId={savedQueryId} /> diff --git a/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx index 387b5e751ff44..847140fd8e272 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx @@ -42,7 +42,6 @@ export interface FilterItemProps { uiSettings: IUiSettingsClient; hiddenPanelOptions?: FilterPanelOption[]; timeRangeForSuggestionsOverride?: boolean; - readonly?: boolean; } type FilterPopoverProps = HTMLAttributes & EuiPopoverProps; @@ -364,7 +363,6 @@ export function FilterItem(props: FilterItemProps) { iconOnClick: handleIconClick, onClick: handleBadgeClick, 'data-test-subj': getDataTestSubj(valueLabelConfig), - readonly: props.readonly, }; const popoverProps: FilterPopoverProps = { @@ -379,18 +377,6 @@ export function FilterItem(props: FilterItemProps) { panelPaddingSize: 'none', }; - if (props.readonly) { - return ( - - - - ); - } - return ( {renderedComponent === 'menu' ? ( diff --git a/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx b/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx index d399bb0025a10..0e10766139820 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx @@ -19,7 +19,6 @@ interface Props { fieldLabel?: string; filterLabelStatus: FilterLabelStatus; errorMessage?: string; - readonly?: boolean; hideAlias?: boolean; [propName: string]: any; } @@ -32,7 +31,6 @@ export const FilterView: FC = ({ fieldLabel, errorMessage, filterLabelStatus, - readonly, hideAlias, ...rest }: Props) => { @@ -56,45 +54,29 @@ export const FilterView: FC = ({ })} ${title}`; } - const badgeProps: EuiBadgeProps = readonly - ? { - title, - color: 'hollow', - onClick, - onClickAriaLabel: i18n.translate( - 'unifiedSearch.filter.filterBar.filterItemReadOnlyBadgeAriaLabel', - { - defaultMessage: 'Filter entry', - } - ), - iconOnClick, + const badgeProps: EuiBadgeProps = { + title, + color: 'hollow', + iconType: 'cross', + iconSide: 'right', + closeButtonProps: { + // Removing tab focus on close button because the same option can be obtained through the context menu + // Also, we may want to add a `DEL` keyboard press functionality + tabIndex: -1, + }, + iconOnClick, + iconOnClickAriaLabel: i18n.translate( + 'unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel', + { + defaultMessage: 'Delete {filter}', + values: { filter: innerText }, } - : { - title, - color: 'hollow', - iconType: 'cross', - iconSide: 'right', - closeButtonProps: { - // Removing tab focus on close button because the same option can be obtained through the context menu - // Also, we may want to add a `DEL` keyboard press functionality - tabIndex: -1, - }, - iconOnClick, - iconOnClickAriaLabel: i18n.translate( - 'unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel', - { - defaultMessage: 'Delete {filter}', - values: { filter: innerText }, - } - ), - onClick, - onClickAriaLabel: i18n.translate( - 'unifiedSearch.filter.filterBar.filterItemBadgeAriaLabel', - { - defaultMessage: 'Filter actions', - } - ), - }; + ), + onClick, + onClickAriaLabel: i18n.translate('unifiedSearch.filter.filterBar.filterItemBadgeAriaLabel', { + defaultMessage: 'Filter actions', + }), + }; return ( diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index 0ad4756e9177b..d62a7f79c82de 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -43,6 +43,7 @@ import { shallowEqual } from '../utils/shallow_equal'; import { AddFilterPopover } from './add_filter_popover'; import { DataViewPicker, DataViewPickerProps } from '../dataview_picker'; import { FilterButtonGroup } from '../filter_bar/filter_button_group/filter_button_group'; +import type { SuggestionsListSize } from '../typeahead/suggestions_component'; import './query_bar.scss'; const SuperDatePicker = React.memo( @@ -88,6 +89,7 @@ export interface QueryBarTopRowProps { filterBar?: React.ReactNode; showDatePickerAsBadge?: boolean; showSubmitButton?: boolean; + suggestionsSize?: SuggestionsListSize; isScreenshotMode?: boolean; } @@ -483,6 +485,7 @@ export const QueryBarTopRow = React.memo( timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride} disableLanguageSwitcher={true} prepend={renderFilterMenuOnly() && renderFilterButtonGroup()} + size={props.suggestionsSize} /> )} diff --git a/src/plugins/unified_search/public/search_bar/search_bar.tsx b/src/plugins/unified_search/public/search_bar/search_bar.tsx index a6ca444612402..9d96ba936f708 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.tsx @@ -29,6 +29,7 @@ import { QueryBarMenu, QueryBarMenuProps } from '../query_string_input/query_bar import type { DataViewPickerProps } from '../dataview_picker'; import QueryBarTopRow from '../query_string_input/query_bar_top_row'; import { FilterBar, FilterItems } from '../filter_bar'; +import type { SuggestionsListSize } from '../typeahead/suggestions_component'; import { searchBarStyles } from './search_bar.styles'; export interface SearchBarInjectedDeps { @@ -88,6 +89,8 @@ export interface SearchBarOwnProps { fillSubmitButton?: boolean; dataViewPickerComponentProps?: DataViewPickerProps; showSubmitButton?: boolean; + // defines size of suggestions query popover + suggestionsSize?: SuggestionsListSize; isScreenshotMode?: boolean; } @@ -485,6 +488,7 @@ class SearchBarUI extends Component { dataViewPickerComponentProps={this.props.dataViewPickerComponentProps} showDatePickerAsBadge={this.shouldShowDatePickerAsBadge()} filterBar={filterBar} + suggestionsSize={this.props.suggestionsSize} isScreenshotMode={this.props.isScreenshotMode} />
diff --git a/x-pack/plugins/stack_alerts/kibana.json b/x-pack/plugins/stack_alerts/kibana.json index abafba8010fbc..ff436ef53fae7 100644 --- a/x-pack/plugins/stack_alerts/kibana.json +++ b/x-pack/plugins/stack_alerts/kibana.json @@ -14,7 +14,8 @@ "triggersActionsUi", "kibanaReact", "savedObjects", - "data" + "data", + "kibanaUtils" ], "configPath": ["xpack", "stack_alerts"], "requiredBundles": ["esUiShared"], diff --git a/x-pack/plugins/stack_alerts/public/alert_types/components/data_view_select_popover.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/components/data_view_select_popover.test.tsx new file mode 100644 index 0000000000000..94e6a6b0c0cd4 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/components/data_view_select_popover.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; +import { DataViewSelectPopover } from './data_view_select_popover'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { act } from 'react-dom/test-utils'; + +const props = { + onSelectDataView: () => {}, + initialDataViewTitle: 'kibana_sample_data_logs', + initialDataViewId: 'mock-data-logs-id', +}; + +const dataViewOptions = [ + { + id: 'mock-data-logs-id', + namespaces: ['default'], + title: 'kibana_sample_data_logs', + }, + { + id: 'mock-flyghts-id', + namespaces: ['default'], + title: 'kibana_sample_data_flights', + }, + { + id: 'mock-ecommerce-id', + namespaces: ['default'], + title: 'kibana_sample_data_ecommerce', + typeMeta: {}, + }, + { + id: 'mock-test-id', + namespaces: ['default'], + title: 'test', + typeMeta: {}, + }, +]; + +const mount = () => { + const dataViewsMock = dataViewPluginMocks.createStartContract(); + dataViewsMock.getIdsWithTitle.mockImplementation(() => Promise.resolve(dataViewOptions)); + + return { + wrapper: mountWithIntl( + + + + ), + dataViewsMock, + }; +}; + +describe('DataViewSelectPopover', () => { + test('renders properly', async () => { + const { wrapper, dataViewsMock } = mount(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(dataViewsMock.getIdsWithTitle).toHaveBeenCalled(); + expect(wrapper.find('[data-test-subj="selectDataViewExpression"]').exists()).toBeTruthy(); + + const getIdsWithTitleResult = await dataViewsMock.getIdsWithTitle.mock.results[0].value; + expect(getIdsWithTitleResult).toBe(dataViewOptions); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/components/data_view_select_popover.tsx b/x-pack/plugins/stack_alerts/public/alert_types/components/data_view_select_popover.tsx new file mode 100644 index 0000000000000..a62b640e0d8eb --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/components/data_view_select_popover.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonIcon, + EuiExpression, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPopover, + EuiPopoverTitle, +} from '@elastic/eui'; +import { DataViewsList } from '@kbn/unified-search-plugin/public'; +import { DataViewListItem } from '@kbn/data-views-plugin/public'; +import { useTriggersAndActionsUiDeps } from '../es_query/util'; + +interface DataViewSelectPopoverProps { + onSelectDataView: (newDataViewId: string) => void; + initialDataViewTitle: string; + initialDataViewId?: string; +} + +export const DataViewSelectPopover: React.FunctionComponent = ({ + onSelectDataView, + initialDataViewTitle, + initialDataViewId, +}) => { + const { data } = useTriggersAndActionsUiDeps(); + const [dataViewItems, setDataViewsItems] = useState(); + const [dataViewPopoverOpen, setDataViewPopoverOpen] = useState(false); + + const [selectedDataViewId, setSelectedDataViewId] = useState(initialDataViewId); + const [selectedTitle, setSelectedTitle] = useState(initialDataViewTitle); + + useEffect(() => { + const initDataViews = async () => { + const fetchedDataViewItems = await data.dataViews.getIdsWithTitle(); + setDataViewsItems(fetchedDataViewItems); + }; + initDataViews(); + }, [data.dataViews]); + + const closeDataViewPopover = useCallback(() => setDataViewPopoverOpen(false), []); + + if (!dataViewItems) { + return null; + } + + return ( + { + setDataViewPopoverOpen(true); + }} + isInvalid={!selectedTitle} + /> + } + isOpen={dataViewPopoverOpen} + closePopover={closeDataViewPopover} + ownFocus + anchorPosition="downLeft" + display="block" + > +
+ + + + {i18n.translate('xpack.stackAlerts.components.ui.alertParams.dataViewPopoverTitle', { + defaultMessage: 'Data view', + })} + + + + + + + + { + setSelectedDataViewId(newId); + const newTitle = dataViewItems?.find(({ id }) => id === newId)?.title; + if (newTitle) { + setSelectedTitle(newTitle); + } + + onSelectDataView(newId); + closeDataViewPopover(); + }} + currentDataViewId={selectedDataViewId} + /> + +
+
+ ); +}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/constants.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/constants.ts index bceb39ba08cf9..da85c878f3281 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/constants.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/constants.ts @@ -6,6 +6,7 @@ */ import { COMPARATORS } from '@kbn/triggers-actions-ui-plugin/public'; +import { ErrorKey } from './types'; export const DEFAULT_VALUES = { THRESHOLD_COMPARATOR: COMPARATORS.GREATER_THAN, @@ -19,3 +20,17 @@ export const DEFAULT_VALUES = { TIME_WINDOW_UNIT: 'm', THRESHOLD: [1000], }; + +export const EXPRESSION_ERRORS = { + index: new Array(), + size: new Array(), + timeField: new Array(), + threshold0: new Array(), + threshold1: new Array(), + esQuery: new Array(), + thresholdComparator: new Array(), + timeWindowSize: new Array(), + searchConfiguration: new Array(), +}; + +export const EXPRESSION_ERROR_KEYS = Object.keys(EXPRESSION_ERRORS) as ErrorKey[]; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx index 10b774648d735..afb45f90c6e52 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx @@ -83,6 +83,7 @@ export const EsQueryExpression = ({ thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR, size: size ?? DEFAULT_VALUES.SIZE, esQuery: esQuery ?? DEFAULT_VALUES.QUERY, + searchType: 'esQuery', }); const setParam = useCallback( diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx index df44a8923183c..3b5e978b999c8 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx @@ -5,29 +5,33 @@ * 2.0. */ -import React from 'react'; +import React, { memo, PropsWithChildren } from 'react'; import { i18n } from '@kbn/i18n'; +import deepEqual from 'fast-deep-equal'; import 'brace/theme/github'; import { EuiSpacer, EuiCallOut } from '@elastic/eui'; import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; -import { EsQueryAlertParams } from '../types'; -import { SearchSourceExpression } from './search_source_expression'; +import { ErrorKey, EsQueryAlertParams } from '../types'; +import { SearchSourceExpression, SearchSourceExpressionProps } from './search_source_expression'; import { EsQueryExpression } from './es_query_expression'; import { isSearchSourceAlert } from '../util'; +import { EXPRESSION_ERROR_KEYS } from '../constants'; -const expressionFieldsWithValidation = [ - 'index', - 'size', - 'timeField', - 'threshold0', - 'threshold1', - 'timeWindowSize', - 'searchType', - 'esQuery', - 'searchConfiguration', -]; +function areSearchSourceExpressionPropsEqual( + prevProps: Readonly>, + nextProps: Readonly> +) { + const areErrorsEqual = deepEqual(prevProps.errors, nextProps.errors); + const areRuleParamsEqual = deepEqual(prevProps.ruleParams, nextProps.ruleParams); + return areErrorsEqual && areRuleParamsEqual; +} + +const SearchSourceExpressionMemoized = memo( + SearchSourceExpression, + areSearchSourceExpressionPropsEqual +); export const EsQueryAlertTypeExpression: React.FunctionComponent< RuleTypeParamsExpressionProps @@ -35,11 +39,11 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< const { ruleParams, errors } = props; const isSearchSource = isSearchSourceAlert(ruleParams); - const hasExpressionErrors = !!Object.keys(errors).find((errorKey) => { + const hasExpressionErrors = Object.keys(errors).some((errorKey) => { return ( - expressionFieldsWithValidation.includes(errorKey) && + EXPRESSION_ERROR_KEYS.includes(errorKey as ErrorKey) && errors[errorKey].length >= 1 && - ruleParams[errorKey as keyof EsQueryAlertParams] !== undefined + ruleParams[errorKey] !== undefined ); }); @@ -54,14 +58,13 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< <> {hasExpressionErrors && ( <> - )} {isSearchSource ? ( - + ) : ( )} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx deleted file mode 100644 index 6747c60bb840c..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n-react'; -import { css } from '@emotion/react'; - -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { getDisplayValueFromFilter } from '@kbn/data-plugin/public'; -import { Filter } from '@kbn/data-plugin/common'; -import { DataView } from '@kbn/data-views-plugin/public'; -import { FilterItem } from '@kbn/unified-search-plugin/public'; - -const FilterItemComponent = injectI18n(FilterItem); - -interface ReadOnlyFilterItemsProps { - filters: Filter[]; - indexPatterns: DataView[]; -} - -const noOp = () => {}; - -export const ReadOnlyFilterItems = ({ filters, indexPatterns }: ReadOnlyFilterItemsProps) => { - const { uiSettings } = useKibana().services; - - const filterList = filters.map((filter, index) => { - const filterValue = getDisplayValueFromFilter(filter, indexPatterns); - return ( - - - - ); - }); - - return ( - - {filterList} - - ); -}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx index 7041bba0fe2ff..d12833a3f258f 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx @@ -10,18 +10,12 @@ import React from 'react'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; -import { DataPublicPluginStart, ISearchStart } from '@kbn/data-plugin/public'; import { EsQueryAlertParams, SearchType } from '../types'; import { SearchSourceExpression } from './search_source_expression'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { act } from 'react-dom/test-utils'; -import { EuiCallOut, EuiLoadingSpinner } from '@elastic/eui'; -import { ReactWrapper } from 'enzyme'; - -const dataMock = dataPluginMock.createStartContract() as DataPublicPluginStart & { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - search: ISearchStart & { searchSource: { create: jest.MockedFunction } }; -}; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; const dataViewPluginMock = dataViewPluginMocks.createStartContract(); const chartsStartMock = chartPluginMock.createStartContract(); @@ -40,6 +34,18 @@ const defaultSearchSourceExpressionParams: EsQueryAlertParams { if (name === 'filter') { return []; @@ -48,7 +54,33 @@ const searchSourceMock = { }, }; -const setup = async (alertParams: EsQueryAlertParams) => { +const savedQueryMock = { + id: 'test-id', + attributes: { + title: 'test-filter-set', + description: '', + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + filters: [], + }, +}; + +jest.mock('./search_source_expression_form', () => ({ + SearchSourceExpressionForm: () =>
search source expression form mock
, +})); + +const dataMock = dataPluginMock.createStartContract(); +(dataMock.search.searchSource.create as jest.Mock).mockImplementation(() => + Promise.resolve(searchSourceMock) +); +(dataMock.dataViews.getIdsWithTitle as jest.Mock).mockImplementation(() => Promise.resolve([])); +(dataMock.query.savedQueries.getSavedQuery as jest.Mock).mockImplementation(() => + Promise.resolve(savedQueryMock) +); + +const setup = (alertParams: EsQueryAlertParams) => { const errors = { size: [], timeField: [], @@ -57,67 +89,58 @@ const setup = async (alertParams: EsQueryAlertParams) = }; const wrapper = mountWithIntl( - {}} - setRuleProperty={() => {}} - errors={errors} - unifiedSearch={unifiedSearchMock} - data={dataMock} - dataViews={dataViewPluginMock} - defaultActionGroupId="" - actionGroups={[]} - charts={chartsStartMock} - /> + + {}} + setRuleProperty={() => {}} + errors={errors} + unifiedSearch={unifiedSearchMock} + data={dataMock} + dataViews={dataViewPluginMock} + defaultActionGroupId="" + actionGroups={[]} + charts={chartsStartMock} + /> + ); return wrapper; }; -const rerender = async (wrapper: ReactWrapper) => { - const update = async () => +describe('SearchSourceAlertTypeExpression', () => { + test('should render correctly', async () => { + let wrapper = setup(defaultSearchSourceExpressionParams).children(); + + expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); + expect(wrapper.text().includes('Cant find searchSource')).toBeFalsy(); + await act(async () => { await nextTick(); - wrapper.update(); }); - await update(); -}; + wrapper = await wrapper.update(); -describe('SearchSourceAlertTypeExpression', () => { - test('should render loading prompt', async () => { - dataMock.search.searchSource.create.mockImplementation(() => - Promise.resolve(() => searchSourceMock) - ); - - const wrapper = await setup(defaultSearchSourceExpressionParams); - - expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); + expect(wrapper.text().includes('Cant find searchSource')).toBeFalsy(); + expect(wrapper.text().includes('search source expression form mock')).toBeTruthy(); }); test('should render error prompt', async () => { - dataMock.search.searchSource.create.mockImplementation(() => - Promise.reject(() => 'test error') + (dataMock.search.searchSource.create as jest.Mock).mockImplementationOnce(() => + Promise.reject(new Error('Cant find searchSource')) ); + let wrapper = setup(defaultSearchSourceExpressionParams).children(); - const wrapper = await setup(defaultSearchSourceExpressionParams); - await rerender(wrapper); - - expect(wrapper.find(EuiCallOut).exists()).toBeTruthy(); - }); - - test('should render SearchSourceAlertTypeExpression with expected components', async () => { - dataMock.search.searchSource.create.mockImplementation(() => - Promise.resolve(() => searchSourceMock) - ); + expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); + expect(wrapper.text().includes('Cant find searchSource')).toBeFalsy(); - const wrapper = await setup(defaultSearchSourceExpressionParams); - await rerender(wrapper); + await act(async () => { + await nextTick(); + }); + wrapper = await wrapper.update(); - expect(wrapper.find('[data-test-subj="sizeValueExpression"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy(); + expect(wrapper.text().includes('Cant find searchSource')).toBeTruthy(); }); }); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index 1d54609223aaf..26b2d074bfd8b 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -5,36 +5,27 @@ * 2.0. */ -import React, { Fragment, useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import './search_source_expression.scss'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiSpacer, - EuiTitle, - EuiExpression, - EuiLoadingSpinner, - EuiEmptyPrompt, - EuiCallOut, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Filter, ISearchSource } from '@kbn/data-plugin/common'; -import { - ForLastExpression, - RuleTypeParamsExpressionProps, - ThresholdExpression, - ValueExpression, -} from '@kbn/triggers-actions-ui-plugin/public'; +import { EuiSpacer, EuiLoadingSpinner, EuiEmptyPrompt, EuiCallOut } from '@elastic/eui'; +import { ISearchSource } from '@kbn/data-plugin/common'; +import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; +import { SavedQuery } from '@kbn/data-plugin/public'; import { EsQueryAlertParams, SearchType } from '../types'; +import { useTriggersAndActionsUiDeps } from '../util'; +import { SearchSourceExpressionForm } from './search_source_expression_form'; import { DEFAULT_VALUES } from '../constants'; -import { ReadOnlyFilterItems } from './read_only_filter_items'; + +export type SearchSourceExpressionProps = RuleTypeParamsExpressionProps< + EsQueryAlertParams +>; export const SearchSourceExpression = ({ ruleParams, + errors, setRuleParams, setRuleProperty, - data, - errors, -}: RuleTypeParamsExpressionProps>) => { +}: SearchSourceExpressionProps) => { const { searchConfiguration, thresholdComparator, @@ -43,48 +34,43 @@ export const SearchSourceExpression = ({ timeWindowUnit, size, } = ruleParams; - const [usedSearchSource, setUsedSearchSource] = useState(); - const [paramsError, setParamsError] = useState(); + const { data } = useTriggersAndActionsUiDeps(); - const [currentAlertParams, setCurrentAlertParams] = useState< - EsQueryAlertParams - >({ - searchConfiguration, - searchType: SearchType.searchSource, - timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, - timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, - threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, - thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR, - size: size ?? DEFAULT_VALUES.SIZE, - }); + const [searchSource, setSearchSource] = useState(); + const [savedQuery, setSavedQuery] = useState(); + const [paramsError, setParamsError] = useState(); const setParam = useCallback( - (paramField: string, paramValue: unknown) => { - setCurrentAlertParams((currentParams) => ({ - ...currentParams, - [paramField]: paramValue, - })); - setRuleParams(paramField, paramValue); - }, + (paramField: string, paramValue: unknown) => setRuleParams(paramField, paramValue), [setRuleParams] ); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => setRuleProperty('params', currentAlertParams), []); + useEffect(() => { + setRuleProperty('params', { + searchConfiguration, + searchType: SearchType.searchSource, + timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, + timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, + threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, + thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR, + size: size ?? DEFAULT_VALUES.SIZE, + }); + + const initSearchSource = () => + data.search.searchSource + .create(searchConfiguration) + .then((fetchedSearchSource) => setSearchSource(fetchedSearchSource)) + .catch(setParamsError); + + initSearchSource(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data.search.searchSource, data.dataViews]); useEffect(() => { - async function initSearchSource() { - try { - const loadedSearchSource = await data.search.searchSource.create(searchConfiguration); - setUsedSearchSource(loadedSearchSource); - } catch (error) { - setParamsError(error); - } - } - if (searchConfiguration) { - initSearchSource(); + if (ruleParams.savedQueryId) { + data.query.savedQueries.getSavedQuery(ruleParams.savedQueryId).then(setSavedQuery); } - }, [data.search.searchSource, searchConfiguration]); + }, [data.query.savedQueries, ruleParams.savedQueryId]); if (paramsError) { return ( @@ -97,124 +83,17 @@ export const SearchSourceExpression = ({ ); } - if (!usedSearchSource) { + if (!searchSource) { return } />; } - const dataView = usedSearchSource.getField('index')!; - const query = usedSearchSource.getField('query')!; - const filters = (usedSearchSource.getField('filter') as Filter[]).filter( - ({ meta }) => !meta.disabled - ); - const dataViews = [dataView]; return ( - - -
- -
-
- - - } - iconType="iInCircle" - /> - - - {query.query !== '' && ( - - )} - {filters.length > 0 && ( - } - display="columns" - /> - )} - - - -
- -
-
- - - setParam('threshold', selectedThresholds) - } - onChangeSelectedThresholdComparator={(selectedThresholdComparator) => - setParam('thresholdComparator', selectedThresholdComparator) - } - /> - - setParam('timeWindowSize', selectedWindowSize) - } - onChangeWindowUnit={(selectedWindowUnit: string) => - setParam('timeWindowUnit', selectedWindowUnit) - } - /> - - -
- -
-
- - { - setParam('size', updatedValue); - }} - /> - -
+ ); }; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx new file mode 100644 index 0000000000000..afd6a156187ee --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx @@ -0,0 +1,269 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Filter, DataView, Query, ISearchSource } from '@kbn/data-plugin/common'; +import { + ForLastExpression, + IErrorObject, + ThresholdExpression, + ValueExpression, +} from '@kbn/triggers-actions-ui-plugin/public'; +import { SearchBar } from '@kbn/unified-search-plugin/public'; +import { mapAndFlattenFilters, SavedQuery, TimeHistory } from '@kbn/data-plugin/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { EsQueryAlertParams, SearchType } from '../types'; +import { DEFAULT_VALUES } from '../constants'; +import { DataViewSelectPopover } from '../../components/data_view_select_popover'; +import { useTriggersAndActionsUiDeps } from '../util'; + +interface LocalState { + index: DataView; + filter: Filter[]; + query: Query; + threshold: number[]; + timeWindowSize: number; + size: number; +} + +interface LocalStateAction { + type: SearchSourceParamsAction['type'] | ('threshold' | 'timeWindowSize' | 'size'); + payload: SearchSourceParamsAction['payload'] | (number[] | number); +} + +type LocalStateReducer = (prevState: LocalState, action: LocalStateAction) => LocalState; + +interface SearchSourceParamsAction { + type: 'index' | 'filter' | 'query'; + payload: DataView | Filter[] | Query; +} + +interface SearchSourceExpressionFormProps { + searchSource: ISearchSource; + ruleParams: EsQueryAlertParams; + errors: IErrorObject; + initialSavedQuery?: SavedQuery; + setParam: (paramField: string, paramValue: unknown) => void; +} + +const isSearchSourceParam = (action: LocalStateAction): action is SearchSourceParamsAction => { + return action.type === 'filter' || action.type === 'index' || action.type === 'query'; +}; + +export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProps) => { + const { data } = useTriggersAndActionsUiDeps(); + const { searchSource, ruleParams, errors, initialSavedQuery, setParam } = props; + const { thresholdComparator, timeWindowUnit } = ruleParams; + const [savedQuery, setSavedQuery] = useState(); + + const timeHistory = useMemo(() => new TimeHistory(new Storage(localStorage)), []); + + useEffect(() => setSavedQuery(initialSavedQuery), [initialSavedQuery]); + + const [{ index: dataView, query, filter: filters, threshold, timeWindowSize, size }, dispatch] = + useReducer( + (currentState, action) => { + if (isSearchSourceParam(action)) { + searchSource.setParent(undefined).setField(action.type, action.payload); + setParam('searchConfiguration', searchSource.getSerializedFields()); + } else { + setParam(action.type, action.payload); + } + return { ...currentState, [action.type]: action.payload }; + }, + { + index: searchSource.getField('index')!, + query: searchSource.getField('query')!, + filter: mapAndFlattenFilters(searchSource.getField('filter') as Filter[]), + threshold: ruleParams.threshold, + timeWindowSize: ruleParams.timeWindowSize, + size: ruleParams.size, + } + ); + const dataViews = useMemo(() => [dataView], [dataView]); + + const onSelectDataView = useCallback( + (newDataViewId) => + data.dataViews + .get(newDataViewId) + .then((newDataView) => dispatch({ type: 'index', payload: newDataView })), + [data.dataViews] + ); + + const onUpdateFilters = useCallback((newFilters) => { + dispatch({ type: 'filter', payload: mapAndFlattenFilters(newFilters) }); + }, []); + + const onChangeQuery = useCallback( + ({ query: newQuery }: { query?: Query }) => { + if (!deepEqual(newQuery, query)) { + dispatch({ type: 'query', payload: newQuery || { ...query, query: '' } }); + } + }, + [query] + ); + + // needs to change language mode only + const onQueryBarSubmit = ({ query: newQuery }: { query?: Query }) => { + if (newQuery?.language !== query.language) { + dispatch({ type: 'query', payload: { ...query, language: newQuery?.language } as Query }); + } + }; + + // Saved query + const onSavedQuery = useCallback((newSavedQuery: SavedQuery) => { + setSavedQuery(newSavedQuery); + const newFilters = newSavedQuery.attributes.filters; + if (newFilters) { + dispatch({ type: 'filter', payload: newFilters }); + } + }, []); + + const onClearSavedQuery = () => { + setSavedQuery(undefined); + dispatch({ type: 'query', payload: { ...query, query: '' } }); + }; + + // window size + const onChangeWindowUnit = useCallback( + (selectedWindowUnit: string) => setParam('timeWindowUnit', selectedWindowUnit), + [setParam] + ); + + const onChangeWindowSize = useCallback( + (selectedWindowSize?: number) => + selectedWindowSize && dispatch({ type: 'timeWindowSize', payload: selectedWindowSize }), + [] + ); + + // threshold + const onChangeSelectedThresholdComparator = useCallback( + (selectedThresholdComparator?: string) => + setParam('thresholdComparator', selectedThresholdComparator), + [setParam] + ); + + const onChangeSelectedThreshold = useCallback( + (selectedThresholds?: number[]) => + selectedThresholds && dispatch({ type: 'threshold', payload: selectedThresholds }), + [] + ); + + const onChangeSizeValue = useCallback( + (updatedValue: number) => dispatch({ type: 'size', payload: updatedValue }), + [] + ); + + return ( + + +
+ +
+
+ + + + + + + + + + + +
+ +
+
+ + + + + +
+ +
+
+ + + +
+ ); +}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts index bccf6ed4ced43..703570ad5faae 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts @@ -7,6 +7,9 @@ import { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { EXPRESSION_ERRORS } from './constants'; export interface Comparator { text: string; @@ -19,7 +22,7 @@ export enum SearchType { searchSource = 'searchSource', } -export interface CommonAlertParams extends RuleTypeParams { +export interface CommonAlertParams extends RuleTypeParams { size: number; thresholdComparator?: string; threshold: number[]; @@ -28,8 +31,8 @@ export interface CommonAlertParams extends RuleTypeParams } export type EsQueryAlertParams = T extends SearchType.searchSource - ? CommonAlertParams & OnlySearchSourceAlertParams - : CommonAlertParams & OnlyEsQueryAlertParams; + ? CommonAlertParams & OnlySearchSourceAlertParams + : CommonAlertParams & OnlyEsQueryAlertParams; export interface OnlyEsQueryAlertParams { esQuery: string; @@ -39,4 +42,15 @@ export interface OnlyEsQueryAlertParams { export interface OnlySearchSourceAlertParams { searchType: 'searchSource'; searchConfiguration: SerializedSearchSourceFields; + savedQueryId?: string; +} + +export type DataViewOption = EuiComboBoxOptionOption; + +export type ExpressionErrors = typeof EXPRESSION_ERRORS; + +export type ErrorKey = keyof ExpressionErrors & unknown; + +export interface TriggersAndActionsUiDeps { + data: DataPublicPluginStart; } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/util.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/util.ts index 5b70da7cb3e80..1f57a133fa65a 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/util.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/util.ts @@ -5,10 +5,13 @@ * 2.0. */ -import { EsQueryAlertParams, SearchType } from './types'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { EsQueryAlertParams, SearchType, TriggersAndActionsUiDeps } from './types'; export const isSearchSourceAlert = ( ruleParams: EsQueryAlertParams ): ruleParams is EsQueryAlertParams => { return ruleParams.searchType === 'searchSource'; }; + +export const useTriggersAndActionsUiDeps = () => useKibana().services; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts index 914dd6a4f5f9f..8a1135e75492f 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts @@ -5,25 +5,17 @@ * 2.0. */ +import { defaultsDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ValidationResult, builtInComparators } from '@kbn/triggers-actions-ui-plugin/public'; -import { EsQueryAlertParams } from './types'; +import { EsQueryAlertParams, ExpressionErrors } from './types'; import { isSearchSourceAlert } from './util'; +import { EXPRESSION_ERRORS } from './constants'; export const validateExpression = (alertParams: EsQueryAlertParams): ValidationResult => { const { size, threshold, timeWindowSize, thresholdComparator } = alertParams; const validationResult = { errors: {} }; - const errors = { - index: new Array(), - timeField: new Array(), - esQuery: new Array(), - size: new Array(), - threshold0: new Array(), - threshold1: new Array(), - thresholdComparator: new Array(), - timeWindowSize: new Array(), - searchConfiguration: new Array(), - }; + const errors: ExpressionErrors = defaultsDeep({}, EXPRESSION_ERRORS); validationResult.errors = errors; if (!threshold || threshold.length === 0 || threshold[0] === undefined) { errors.threshold0.push( diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap index 65dff2bd3a6c6..fe53610caa316 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap @@ -30,6 +30,7 @@ exports[`should render BoundaryIndexExpression 1`] = ` "ensureDefaultIndexPattern": [MockFunction], "find": [MockFunction], "get": [MockFunction], + "getIdsWithTitle": [MockFunction], "make": [Function], } } @@ -106,6 +107,7 @@ exports[`should render EntityIndexExpression 1`] = ` "ensureDefaultIndexPattern": [MockFunction], "find": [MockFunction], "get": [MockFunction], + "getIdsWithTitle": [MockFunction], "make": [Function], } } @@ -188,6 +190,7 @@ exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = ` "ensureDefaultIndexPattern": [MockFunction], "find": [MockFunction], "get": [MockFunction], + "getIdsWithTitle": [MockFunction], "make": [Function], } } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts index 468729fb2120d..884bf606d2f90 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts @@ -20,6 +20,7 @@ describe('ActionContext', () => { timeWindowUnit: 'm', thresholdComparator: '>', threshold: [4], + searchType: 'esQuery', }) as OnlyEsQueryAlertParams; const base: EsQueryAlertActionContext = { date: '2020-01-01T00:00:00.000Z', @@ -50,6 +51,7 @@ describe('ActionContext', () => { timeWindowUnit: 'm', thresholdComparator: 'between', threshold: [4, 5], + searchType: 'esQuery', }) as OnlyEsQueryAlertParams; const base: EsQueryAlertActionContext = { date: '2020-01-01T00:00:00.000Z', diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index 3fce895a2bfd1..3304ca5e902f7 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -110,6 +110,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.LT, threshold: [0], + searchType: 'esQuery', }; expect(alertType.validate?.params?.validate(params)).toBeTruthy(); @@ -128,6 +129,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.BETWEEN, threshold: [0], + searchType: 'esQuery', }; expect(() => paramsSchema.validate(params)).toThrowErrorMatchingInlineSnapshot( @@ -145,6 +147,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.BETWEEN, threshold: [0], + searchType: 'esQuery', }; const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); @@ -174,6 +177,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], + searchType: 'esQuery', }; const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); @@ -219,6 +223,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], + searchType: 'esQuery', }; const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); @@ -267,6 +272,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], + searchType: 'esQuery', }; const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); @@ -309,6 +315,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], + searchType: 'esQuery', }; const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); @@ -380,6 +387,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], + searchType: 'esQuery', }; const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); @@ -425,6 +433,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], + searchType: 'esQuery', }; const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 5b41d7c55fe0a..dfab69f445629 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -7,10 +7,12 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Logger } from '@kbn/core/server'; +import { extractReferences, injectReferences } from '@kbn/data-plugin/common'; import { RuleType } from '../../types'; import { ActionContext } from './action_context'; import { EsQueryAlertParams, + EsQueryAlertParamsExtractedParams, EsQueryAlertParamsSchema, EsQueryAlertState, } from './alert_type_params'; @@ -18,13 +20,14 @@ import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { ExecutorOptions } from './types'; import { ActionGroupId, ES_QUERY_ID } from './constants'; import { executor } from './executor'; +import { isEsQueryAlert } from './util'; export function getAlertType( logger: Logger, core: CoreSetup ): RuleType< EsQueryAlertParams, - never, // Only use if defining useSavedObjectReferences hook + EsQueryAlertParamsExtractedParams, EsQueryAlertState, {}, ActionContext, @@ -159,6 +162,25 @@ export function getAlertType( { name: 'index', description: actionVariableContextIndexLabel }, ], }, + useSavedObjectReferences: { + extractReferences: (params) => { + if (isEsQueryAlert(params.searchType)) { + return { params: params as EsQueryAlertParamsExtractedParams, references: [] }; + } + const [searchConfiguration, references] = extractReferences(params.searchConfiguration); + const newParams = { ...params, searchConfiguration } as EsQueryAlertParamsExtractedParams; + return { params: newParams, references }; + }, + injectReferences: (params, references) => { + if (isEsQueryAlert(params.searchType)) { + return params; + } + return { + ...params, + searchConfiguration: injectReferences(params.searchConfiguration, references), + }; + }, + }, minimumLicenseRequired: 'basic', isExportable: true, executor: async (options: ExecutorOptions) => { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts index d6ba0468b7cbf..a1155fedb7a02 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts @@ -23,6 +23,7 @@ const DefaultParams: Writable> = { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], + searchType: 'esQuery', }; describe('alertType Params validate()', () => { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts index f205fbd0327ce..d32fce9debbc2 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { validateTimeWindowUnits } from '@kbn/triggers-actions-ui-plugin/server'; import { RuleTypeState } from '@kbn/alerting-plugin/server'; +import { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; import { Comparator } from '../../../common/comparator_types'; import { ComparatorFnNames } from '../lib'; import { getComparatorSchemaType } from '../lib/comparator'; @@ -21,13 +22,21 @@ export interface EsQueryAlertState extends RuleTypeState { latestTimestamp: string | undefined; } +export type EsQueryAlertParamsExtractedParams = Omit & { + searchConfiguration: SerializedSearchSourceFields & { + indexRefName: string; + }; +}; + const EsQueryAlertParamsSchemaProperties = { size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }), timeWindowSize: schema.number({ min: 1 }), timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }), threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }), thresholdComparator: getComparatorSchemaType(validateComparator), - searchType: schema.nullable(schema.literal('searchSource')), + searchType: schema.oneOf([schema.literal('searchSource'), schema.literal('esQuery')], { + defaultValue: 'esQuery', + }), // searchSource alert param only searchConfiguration: schema.conditional( schema.siblingRef('searchType'), @@ -38,21 +47,21 @@ const EsQueryAlertParamsSchemaProperties = { // esQuery alert params only esQuery: schema.conditional( schema.siblingRef('searchType'), - schema.literal('searchSource'), - schema.never(), - schema.string({ minLength: 1 }) + schema.literal('esQuery'), + schema.string({ minLength: 1 }), + schema.never() ), index: schema.conditional( schema.siblingRef('searchType'), - schema.literal('searchSource'), - schema.never(), - schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }) + schema.literal('esQuery'), + schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), + schema.never() ), timeField: schema.conditional( schema.siblingRef('searchType'), - schema.literal('searchSource'), - schema.never(), - schema.string({ minLength: 1 }) + schema.literal('esQuery'), + schema.string({ minLength: 1 }), + schema.never() ), }; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts index 670f76f5e19de..7b4cc7521654b 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts @@ -18,6 +18,7 @@ describe('es_query executor', () => { esQuery: '{ "query": "test-query" }', index: ['test-index'], timeField: '', + searchType: 'esQuery', }; describe('tryToParseAsDate', () => { it.each<[string | number]>([['2019-01-01T00:00:00.000Z'], [1546300800000]])( diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts index 44708a1df90fd..6e47c5f471d88 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts @@ -16,13 +16,14 @@ import { fetchEsQuery } from './lib/fetch_es_query'; import { EsQueryAlertParams } from './alert_type_params'; import { fetchSearchSourceQuery } from './lib/fetch_search_source_query'; import { Comparator } from '../../../common/comparator_types'; +import { isEsQueryAlert } from './util'; export async function executor( logger: Logger, core: CoreSetup, options: ExecutorOptions ) { - const esQueryAlert = isEsQueryAlert(options); + const esQueryAlert = isEsQueryAlert(options.params.searchType); const { alertId, name, services, params, state } = options; const { alertFactory, scopedClusterClient, searchSourceClient } = services; const currentTimestamp = new Date().toISOString(); @@ -162,10 +163,6 @@ export function tryToParseAsDate(sortValue?: string | number | null): undefined } } -export function isEsQueryAlert(options: ExecutorOptions) { - return options.params.searchType !== 'searchSource'; -} - export function getChecksum(params: EsQueryAlertParams) { return sha256.create().update(JSON.stringify(params)); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts index 12b2ee02af171..8595870a84940 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts @@ -10,7 +10,9 @@ import { ActionContext } from './action_context'; import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; import { ActionGroupId } from './constants'; -export type OnlyEsQueryAlertParams = Omit; +export type OnlyEsQueryAlertParams = Omit & { + searchType: 'esQuery'; +}; export type OnlySearchSourceAlertParams = Omit< EsQueryAlertParams, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/util.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/util.ts new file mode 100644 index 0000000000000..b58a362cd27e9 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/util.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EsQueryAlertParams } from './alert_type_params'; + +export function isEsQueryAlert(searchType: EsQueryAlertParams['searchType']) { + return searchType !== 'searchSource'; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index 1bca80a08c936..6da565b13d91e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -724,10 +724,10 @@ export const RuleForm = ({ name="interval" data-test-subj="intervalInput" onChange={(e) => { - const interval = - e.target.value !== '' ? parseInt(e.target.value, 10) : undefined; + const value = e.target.value; + const interval = value !== '' ? parseInt(value, 10) : undefined; setRuleInterval(interval); - setScheduleProperty('interval', `${e.target.value}${ruleIntervalUnit}`); + setScheduleProperty('interval', `${value}${ruleIntervalUnit}`); }} /> diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts index bae045fc93838..2cb77ac262ca6 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts @@ -30,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const supertest = getService('supertest'); const queryBar = getService('queryBar'); const security = getService('security'); + const filterBar = getService('filterBar'); const SOURCE_DATA_INDEX = 'search-source-alert'; const OUTPUT_DATA_INDEX = 'search-source-alert-output'; @@ -47,17 +48,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { mappings: { properties: { '@timestamp': { type: 'date' }, - message: { type: 'text' }, + message: { type: 'keyword' }, }, }, }, }); const generateNewDocs = async (docsNumber: number) => { - const mockMessages = new Array(docsNumber).map((current) => `msg-${current}`); + const mockMessages = Array.from({ length: docsNumber }, (_, i) => `msg-${i}`); const dateNow = new Date().toISOString(); - for (const message of mockMessages) { - await es.transport.request({ + for await (const message of mockMessages) { + es.transport.request({ path: `/${SOURCE_DATA_INDEX}/_doc`, method: 'POST', body: { @@ -212,7 +213,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await navigateToDiscover(link); }; - const openAlertRule = async () => { + const openAlertRuleInManagement = async () => { await PageObjects.common.navigateToApp('management'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -229,7 +230,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await security.testUser.setRoles(['discover_alert']); - log.debug('create source index'); + log.debug('create source indices'); await createSourceIndex(); log.debug('generate documents'); @@ -250,8 +251,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { - // delete only remaining output index - await es.transport.request({ + es.transport.request({ path: `/${OUTPUT_DATA_INDEX}`, method: 'DELETE', }); @@ -272,7 +272,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await defineSearchSourceAlert(RULE_NAME); await PageObjects.header.waitUntilLoadingHasFinished(); - await openAlertRule(); + await openAlertRuleInManagement(); await testSubjects.click('ruleDetails-viewInApp'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -298,10 +298,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should display warning about updated alert rule', async () => { - await openAlertRule(); + await openAlertRuleInManagement(); // change rule configuration await testSubjects.click('openEditRuleFlyoutButton'); + await queryBar.setQuery('message:msg-1'); + await filterBar.addFilter('message.keyword', 'is', 'msg-1'); + await testSubjects.click('thresholdPopover'); await testSubjects.setValue('alertThresholdInput', '1'); await testSubjects.click('saveEditedRuleButton'); @@ -311,7 +314,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await navigateToResults(); const { message, title } = await getLastToast(); - expect(await dataGrid.getDocCount()).to.be(5); + const queryString = await queryBar.getQueryString(); + const hasFilter = await filterBar.hasFilter('message.keyword', 'msg-1'); + + expect(queryString).to.be.equal('message:msg-1'); + expect(hasFilter).to.be.equal(true); + + expect(await dataGrid.getDocCount()).to.be(1); expect(title).to.be.equal('Alert rule has changed'); expect(message).to.be.equal( 'The displayed documents might not match the documents that triggered the alert because the rule configuration changed.' From 7e15097379841b2923a111629d53b6b560c44dd9 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 20 May 2022 07:32:27 -0700 Subject: [PATCH 042/120] [ML] Adds placeholder text for testing NLP models (#132486) --- .../test_models/models/ner/ner_inference.ts | 7 +++++-- .../models/text_classification/fill_mask_inference.ts | 5 +++-- .../models/text_classification/lang_ident_inference.ts | 9 ++++++++- .../text_classification/text_classification_inference.ts | 9 ++++++++- .../models/text_embedding/text_embedding_inference.ts | 9 ++++++++- 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/ner_inference.ts b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/ner_inference.ts index 13f07d8c88770..7d780559fb47d 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/ner_inference.ts +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/ner_inference.ts @@ -6,7 +6,7 @@ */ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - +import { i18n } from '@kbn/i18n'; import { InferenceBase, InferResponse } from '../inference_base'; import { getGeneralInputComponent } from '../text_input'; import { getNerOutputComponent } from './ner_output'; @@ -52,7 +52,10 @@ export class NerInference extends InferenceBase { } public getInputComponent(): JSX.Element { - return getGeneralInputComponent(this); + const placeholder = i18n.translate('xpack.ml.trainedModels.testModelsFlyout.ner.inputText', { + defaultMessage: 'Enter a phrase to test', + }); + return getGeneralInputComponent(this, placeholder); } public getOutputComponent(): JSX.Element { diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/text_classification/fill_mask_inference.ts b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/text_classification/fill_mask_inference.ts index bb4feaffffb38..b9c1c724ca348 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/text_classification/fill_mask_inference.ts +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/text_classification/fill_mask_inference.ts @@ -55,9 +55,10 @@ export class FillMaskInference extends InferenceBase public getInputComponent(): JSX.Element { const placeholder = i18n.translate( - 'xpack.ml.trainedModels.testModelsFlyout.langIdent.inputText', + 'xpack.ml.trainedModels.testModelsFlyout.fillMask.inputText', { - defaultMessage: 'Mask token: [MASK]. e.g. Paris is the [MASK] of France.', + defaultMessage: + 'Enter a phrase to test. Use [MASK] as a placeholder for the missing words.', } ); diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/text_classification/lang_ident_inference.ts b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/text_classification/lang_ident_inference.ts index a56d4a3598a66..155b696fa7665 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/text_classification/lang_ident_inference.ts +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/text_classification/lang_ident_inference.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import { InferenceBase, InferenceType } from '../inference_base'; import { processResponse } from './common'; import { getGeneralInputComponent } from '../text_input'; @@ -44,7 +45,13 @@ export class LangIdentInference extends InferenceBase } public getInputComponent(): JSX.Element { - return getGeneralInputComponent(this); + const placeholder = i18n.translate( + 'xpack.ml.trainedModels.testModelsFlyout.textEmbedding.inputText', + { + defaultMessage: 'Enter a phrase to test', + } + ); + return getGeneralInputComponent(this, placeholder); } public getOutputComponent(): JSX.Element { From 759f13f50f87365681c1baa98607e9b385567d60 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 20 May 2022 10:39:09 -0400 Subject: [PATCH 043/120] [Fleet] Remove reference to non removable package feature (#132458) --- .../context/fixtures/integration.nginx.ts | 1 - .../context/fixtures/integration.okta.ts | 1 - .../plugins/fleet/common/openapi/bundled.json | 3 - .../plugins/fleet/common/openapi/bundled.yaml | 2 - .../components/schemas/package_info.yaml | 2 - .../common/services/fixtures/aws_package.ts | 1 - .../plugins/fleet/common/types/models/epm.ts | 1 - .../create_package_policy_page/index.test.tsx | 1 - .../step_configure_package.test.tsx | 1 - .../edit_package_policy_page/index.test.tsx | 1 - .../epm/screens/detail/index.test.tsx | 1 - .../epm/screens/detail/settings/settings.tsx | 76 ++++++++----------- .../fleet/server/saved_objects/index.ts | 3 +- .../saved_objects/migrations/to_v8_3_0.ts | 19 +++++ .../fleet/server/services/epm/packages/get.ts | 1 - .../server/services/epm/packages/install.ts | 2 - .../server/services/epm/packages/remove.ts | 4 +- ...kage_policies_to_agent_permissions.test.ts | 1 - .../common/endpoint/generate_data.ts | 1 - .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../apis/epm/install_remove_assets.ts | 1 - .../apis/epm/update_assets.ts | 1 - .../test_packages/filetest/0.1.0/manifest.yml | 2 - .../0.1.0/manifest.yml | 2 - 26 files changed, 52 insertions(+), 79 deletions(-) create mode 100644 x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_3_0.ts diff --git a/x-pack/plugins/fleet/.storybook/context/fixtures/integration.nginx.ts b/x-pack/plugins/fleet/.storybook/context/fixtures/integration.nginx.ts index d74d7656ad58e..8f47d564c44a2 100644 --- a/x-pack/plugins/fleet/.storybook/context/fixtures/integration.nginx.ts +++ b/x-pack/plugins/fleet/.storybook/context/fixtures/integration.nginx.ts @@ -664,6 +664,5 @@ export const item: GetInfoResponse['item'] = { github: 'elastic/integrations', }, latestVersion: '0.7.0', - removable: true, status: 'not_installed', }; diff --git a/x-pack/plugins/fleet/.storybook/context/fixtures/integration.okta.ts b/x-pack/plugins/fleet/.storybook/context/fixtures/integration.okta.ts index 1f4b9e85043a6..8778938443661 100644 --- a/x-pack/plugins/fleet/.storybook/context/fixtures/integration.okta.ts +++ b/x-pack/plugins/fleet/.storybook/context/fixtures/integration.okta.ts @@ -263,6 +263,5 @@ export const item: GetInfoResponse['item'] = { github: 'elastic/security-external-integrations', }, latestVersion: '1.2.0', - removable: true, status: 'not_installed', }; diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index dca3fd3ccb678..ba18b78d5f768 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -3573,9 +3573,6 @@ }, "path": { "type": "string" - }, - "removable": { - "type": "boolean" } }, "required": [ diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index d1a114b35ab6c..e18fe6b8fc3f8 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -2228,8 +2228,6 @@ components: type: string path: type: string - removable: - type: boolean required: - name - title diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/package_info.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/package_info.yaml index ec4f18af8a223..e61c349f3f490 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/package_info.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/package_info.yaml @@ -102,8 +102,6 @@ properties: type: string path: type: string - removable: - type: boolean required: - name - title diff --git a/x-pack/plugins/fleet/common/services/fixtures/aws_package.ts b/x-pack/plugins/fleet/common/services/fixtures/aws_package.ts index 2b93cca3d4e4d..63397e484a7df 100644 --- a/x-pack/plugins/fleet/common/services/fixtures/aws_package.ts +++ b/x-pack/plugins/fleet/common/services/fixtures/aws_package.ts @@ -1921,7 +1921,6 @@ export const AWS_PACKAGE = { }, ], latestVersion: '0.5.3', - removable: true, status: 'not_installed', }; diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index c7951e86d7866..cb5d8f3bb009b 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -372,7 +372,6 @@ export interface EpmPackageAdditions { title: string; latestVersion: string; assets: AssetsGroupedByServiceByType; - removable?: boolean; notice?: string; keepPoliciesUpToDate?: boolean; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx index 0f719f6a61585..4a13f117ec6ba 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx @@ -164,7 +164,6 @@ describe('when on the package policy create page', () => { }, ], latestVersion: '1.3.0', - removable: true, keepPoliciesUpToDate: false, status: 'not_installed', }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.test.tsx index 543747307908e..ff4c39af799f2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.test.tsx @@ -96,7 +96,6 @@ describe('StepConfigurePackage', () => { }, ], latestVersion: '1.3.0', - removable: true, keepPoliciesUpToDate: false, status: 'not_installed', }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.test.tsx index 3a5050b1b6d06..464f705811ebf 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.test.tsx @@ -89,7 +89,6 @@ jest.mock('../../../hooks', () => { }, ], latestVersion: version, - removable: true, keepPoliciesUpToDate: false, status: 'not_installed', }, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx index e4341af45cf41..9d46c636150d3 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx @@ -509,7 +509,6 @@ const mockApiCalls = ( ], owner: { github: 'elastic/integrations-services' }, latestVersion: '0.3.7', - removable: true, status: 'installed', }, } as GetInfoResponse; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx index 05ff443a7b0e6..d84fab93dc8c2 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx @@ -97,7 +97,7 @@ interface Props { } export const SettingsPage: React.FC = memo(({ packageInfo, theme$ }: Props) => { - const { name, title, removable, latestVersion, version, keepPoliciesUpToDate } = packageInfo; + const { name, title, latestVersion, version, keepPoliciesUpToDate } = packageInfo; const [dryRunData, setDryRunData] = useState(); const [isUpgradingPackagePolicies, setIsUpgradingPackagePolicies] = useState(false); const getPackageInstallStatus = useGetPackageInstallStatus(); @@ -342,41 +342,39 @@ export const SettingsPage: React.FC = memo(({ packageInfo, theme$ }: Prop ) : ( - removable && ( - <> -
- -

- -

-
- -

+ <> +

+ +

+

+
+ +

+ +

+
+ + +

+

-
- - -

- -

-
-
- - ) + + + )} - {packageHasUsages && removable === true && ( + {packageHasUsages && (

= memo(({ packageInfo, theme$ }: Prop

)} - {removable === false && ( -

- - , - }} - /> - -

- )} )} {hideInstallOptions && isViewingOldPackage && !isUpdating && ( diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 2a8f14f795f7c..edcf2ed751f3e 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -38,6 +38,7 @@ import { migratePackagePolicyToV7150 } from './migrations/to_v7_15_0'; import { migrateInstallationToV7160, migratePackagePolicyToV7160 } from './migrations/to_v7_16_0'; import { migrateInstallationToV800, migrateOutputToV800 } from './migrations/to_v8_0_0'; import { migratePackagePolicyToV820 } from './migrations/to_v8_2_0'; +import { migrateInstallationToV830 } from './migrations/to_v8_3_0'; /* * Saved object types and mappings @@ -223,7 +224,6 @@ const getSavedObjectTypes = ( name: { type: 'keyword' }, version: { type: 'keyword' }, internal: { type: 'boolean' }, - removable: { type: 'boolean' }, keep_policies_up_to_date: { type: 'boolean', index: false }, es_index_patterns: { enabled: false, @@ -262,6 +262,7 @@ const getSavedObjectTypes = ( '7.14.1': migrateInstallationToV7140, '7.16.0': migrateInstallationToV7160, '8.0.0': migrateInstallationToV800, + '8.3.0': migrateInstallationToV830, }, }, [ASSETS_SAVED_OBJECT_TYPE]: { diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_3_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_3_0.ts new file mode 100644 index 0000000000000..843427f3cf862 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_3_0.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectMigrationFn } from '@kbn/core/server'; + +import type { Installation } from '../../../common'; + +export const migrateInstallationToV830: SavedObjectMigrationFn = ( + installationDoc, + migrationContext +) => { + delete installationDoc.attributes.removable; + + return installationDoc; +}; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 27468e77c8e9f..acd5761919a16 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -176,7 +176,6 @@ export async function getPackageInfo({ : resolvedPkgVersion, title: packageInfo.title || nameAsTitle(packageInfo.name), assets: Registry.groupPathsByService(paths || []), - removable: true, notice: Registry.getNoticePath(paths || []), keepPoliciesUpToDate: savedObject?.attributes.keep_policies_up_to_date ?? false, }; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index c7fc01c89eb06..6bbb91ada321c 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -598,7 +598,6 @@ export async function createInstallation(options: { ? true : undefined; - // TODO cleanup removable flag and isUnremovablePackage function const created = await savedObjectsClient.create( PACKAGES_SAVED_OBJECT_TYPE, { @@ -609,7 +608,6 @@ export async function createInstallation(options: { es_index_patterns: toSaveESIndexPatterns, name: pkgName, version: pkgVersion, - removable: true, install_version: pkgVersion, install_status: 'installing', install_started_at: new Date().toISOString(), diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 95e65acfebef6..53e001aeee8d0 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -44,11 +44,9 @@ export async function removeInstallation(options: { esClient: ElasticsearchClient; force?: boolean; }): Promise { - const { savedObjectsClient, pkgName, pkgVersion, esClient, force } = options; + const { savedObjectsClient, pkgName, pkgVersion, esClient } = options; const installation = await getInstallation({ savedObjectsClient, pkgName }); if (!installation) throw Boom.badRequest(`${pkgName} is not installed`); - if (installation.removable === false && !force) - throw Boom.badRequest(`${pkgName} is installed by default and cannot be removed`); const { total } = await packagePolicyService.list(savedObjectsClient, { kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${pkgName}`, diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts index 6bc56e8316da6..5c63d0ba5dca1 100644 --- a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts @@ -391,7 +391,6 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { }, ], latestVersion: '0.3.0', - removable: true, notice: undefined, status: 'not_installed', assets: { 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 5a6b20550f224..35eb9de6d4060 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -1764,7 +1764,6 @@ export class EndpointDocGenerator extends BaseDataGenerator { name: 'endpoint', version: '0.5.0', internal: false, - removable: false, install_version: '0.5.0', install_status: 'installed', install_started_at: '2020-06-24T14:41:23.098Z', diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 8bd7308a27a70..85ea8a0ffc348 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -12839,7 +12839,6 @@ "xpack.fleet.integrations.settings.packageUninstallDescription": "Supprimez les ressources Kibana et Elasticsearch installées par cette intégration.", "xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteDetail": "{strongNote} Impossible d'installer {title}, car des agents actifs utilisent cette intégration. Pour procéder à la désinstallation, supprimez toutes les intégrations {title} de vos stratégies d'agent.", "xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteLabel": "Remarque :", - "xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallUninstallableNoteDetail": "{strongNote} L'intégration de {title} est une intégration système. Vous ne pouvez pas la supprimer.", "xpack.fleet.integrations.settings.packageUninstallTitle": "Désinstaller", "xpack.fleet.integrations.settings.packageVersionTitle": "Version de {title}", "xpack.fleet.integrations.settings.versionInfo.installedVersion": "Version installée", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 12300057ca7ff..cf84dbd2d6305 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12946,7 +12946,6 @@ "xpack.fleet.integrations.settings.packageUninstallDescription": "この統合によってインストールされたKibanaおよびElasticsearchアセットを削除します。", "xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteDetail": "{strongNote} {title}をアンインストールできません。この統合を使用しているアクティブなエージェントがあります。アンインストールするには、エージェントポリシーからすべての{title}統合を削除します。", "xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteLabel": "注:", - "xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallUninstallableNoteDetail": "{strongNote} {title}統合はシステム統合であるため、削除できません。", "xpack.fleet.integrations.settings.packageUninstallTitle": "アンインストール", "xpack.fleet.integrations.settings.packageVersionTitle": "{title}バージョン", "xpack.fleet.integrations.settings.versionInfo.installedVersion": "インストールされているバージョン", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5953802b0a0a5..b15cacd8dc8ab 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12970,7 +12970,6 @@ "xpack.fleet.integrations.settings.packageUninstallDescription": "移除此集成安装的 Kibana 和 Elasticsearch 资产。", "xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteDetail": "{strongNote}{title} 无法卸载,因为存在使用此集成的活动代理。要卸载,请从您的代理策略中移除所有 {title} 集成。", "xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteLabel": "注意:", - "xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallUninstallableNoteDetail": "{strongNote}{title} 集成是系统集成,无法移除。", "xpack.fleet.integrations.settings.packageUninstallTitle": "卸载", "xpack.fleet.integrations.settings.packageVersionTitle": "{title} 版本", "xpack.fleet.integrations.settings.versionInfo.installedVersion": "已安装版本", diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index ddb9317789069..0d06a1ca9e0f7 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -738,7 +738,6 @@ const expectAssetsInstalled = ({ }, name: 'all_assets', version: '0.1.0', - removable: true, install_version: '0.1.0', install_status: 'installed', install_started_at: res.attributes.install_started_at, diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index 6cbedf68da567..e367e76049b72 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -498,7 +498,6 @@ export default function (providerContext: FtrProviderContext) { ], name: 'all_assets', version: '0.2.0', - removable: true, install_version: '0.2.0', install_status: 'installed', install_started_at: res.attributes.install_started_at, diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/manifest.yml index ec3586689becf..c4fb3f967913d 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/manifest.yml +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/manifest.yml @@ -10,8 +10,6 @@ release: beta # The default type is integration and will be set if empty. type: integration license: basic -# This package can be removed -removable: true requirement: elasticsearch: diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/manifest.yml index f1ed5a8a5a78b..472888818e717 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/manifest.yml +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/manifest.yml @@ -10,8 +10,6 @@ release: beta # The default type is integration and will be set if empty. type: integration license: basic -# This package can be removed -removable: true requirement: elasticsearch: From 1b4ac7d2719b64ec22c5c50a7e245e37d9e148fe Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 20 May 2022 17:54:13 +0300 Subject: [PATCH 044/120] [XY] Reference lines overlay fix. (#132607) --- .../reference_line.test.ts | 4 + .../common/types/expression_functions.ts | 3 +- .../reference_lines/reference_line.tsx | 4 +- .../reference_lines/reference_lines.test.tsx | 18 ++--- .../reference_lines/reference_lines.tsx | 53 ++++---------- .../components/reference_lines/utils.tsx | 73 ++++++++++++++++++- 6 files changed, 103 insertions(+), 52 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts index b96f39923fab2..4c7c2e3dc628f 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts @@ -14,6 +14,7 @@ describe('referenceLine', () => { test('produces the correct arguments for minimum arguments', async () => { const args: ReferenceLineArgs = { value: 100, + fill: 'above', }; const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); @@ -67,6 +68,7 @@ describe('referenceLine', () => { const args: ReferenceLineArgs = { name: 'some name', value: 100, + fill: 'none', }; const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); @@ -90,6 +92,7 @@ describe('referenceLine', () => { const args: ReferenceLineArgs = { value: 100, textVisibility: true, + fill: 'none', }; const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); @@ -115,6 +118,7 @@ describe('referenceLine', () => { value: 100, name: 'some text', textVisibility, + fill: 'none', }; const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index 05447607bc194..502bb39cda894 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -297,9 +297,10 @@ export type ExtendedAnnotationLayerConfigResult = ExtendedAnnotationLayerArgs & layerType: typeof LayerTypes.ANNOTATIONS; }; -export interface ReferenceLineArgs extends Omit { +export interface ReferenceLineArgs extends Omit { name?: string; value: number; + fill: FillStyle; } export interface ReferenceLineLayerArgs { diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx index 74bb18597f2f2..30f4a97986ec3 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx @@ -19,6 +19,7 @@ interface ReferenceLineProps { formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; axesMap: Record<'left' | 'right', boolean>; isHorizontal: boolean; + nextValue?: number; } export const ReferenceLine: FC = ({ @@ -27,6 +28,7 @@ export const ReferenceLine: FC = ({ formatters, paddingMap, isHorizontal, + nextValue, }) => { const { yConfig: [yConfig], @@ -46,7 +48,7 @@ export const ReferenceLine: FC = ({ return ( { ['yAccessorLeft', 'below'], ['yAccessorRight', 'above'], ['yAccessorRight', 'below'], - ] as Array<[string, ExtendedYConfig['fill']]>)( + ] as Array<[string, Exclude]>)( 'should render a RectAnnotation for a reference line with fill set: %s %s', (layerPrefix, fill) => { const axisMode = getAxisFromId(layerPrefix); @@ -154,7 +154,7 @@ describe('ReferenceLines', () => { it.each([ ['xAccessor', 'above'], ['xAccessor', 'below'], - ] as Array<[string, ExtendedYConfig['fill']]>)( + ] as Array<[string, Exclude]>)( 'should render a RectAnnotation for a reference line with fill set: %s %s', (layerPrefix, fill) => { const wrapper = shallow( @@ -196,7 +196,7 @@ describe('ReferenceLines', () => { ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], ['yAccessorRight', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], ['yAccessorRight', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], - ] as Array<[string, ExtendedYConfig['fill'], YCoords, YCoords]>)( + ] as Array<[string, Exclude, YCoords, YCoords]>)( 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', (layerPrefix, fill, coordsA, coordsB) => { const axisMode = getAxisFromId(layerPrefix); @@ -252,7 +252,7 @@ describe('ReferenceLines', () => { it.each([ ['xAccessor', 'above', { x0: 1, x1: 2 }, { x0: 2, x1: undefined }], ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: 1, x1: 2 }], - ] as Array<[string, ExtendedYConfig['fill'], XCoords, XCoords]>)( + ] as Array<[string, Exclude, XCoords, XCoords]>)( 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', (layerPrefix, fill, coordsA, coordsB) => { const wrapper = shallow( @@ -361,7 +361,7 @@ describe('ReferenceLines', () => { it.each([ ['above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], ['below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], - ] as Array<[ExtendedYConfig['fill'], YCoords, YCoords]>)( + ] as Array<[Exclude, YCoords, YCoords]>)( 'should be robust and works also for different axes when on same direction: 1x Left + 1x Right both %s', (fill, coordsA, coordsB) => { const wrapper = shallow( @@ -438,7 +438,7 @@ describe('ReferenceLines', () => { ['yAccessorLeft', 'below'], ['yAccessorRight', 'above'], ['yAccessorRight', 'below'], - ] as Array<[string, ExtendedYConfig['fill']]>)( + ] as Array<[string, Exclude]>)( 'should render a RectAnnotation for a reference line with fill set: %s %s', (layerPrefix, fill) => { const axisMode = getAxisFromId(layerPrefix); @@ -479,7 +479,7 @@ describe('ReferenceLines', () => { it.each([ ['xAccessor', 'above'], ['xAccessor', 'below'], - ] as Array<[string, ExtendedYConfig['fill']]>)( + ] as Array<[string, Exclude]>)( 'should render a RectAnnotation for a reference line with fill set: %s %s', (layerPrefix, fill) => { const value = 1; @@ -519,7 +519,7 @@ describe('ReferenceLines', () => { it.each([ ['yAccessorLeft', 'above', { y0: 10, y1: undefined }, { y0: 10, y1: undefined }], ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: undefined, y1: 5 }], - ] as Array<[string, ExtendedYConfig['fill'], YCoords, YCoords]>)( + ] as Array<[string, Exclude, YCoords, YCoords]>)( 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', (layerPrefix, fill, coordsA, coordsB) => { const axisMode = getAxisFromId(layerPrefix); @@ -570,7 +570,7 @@ describe('ReferenceLines', () => { it.each([ ['xAccessor', 'above', { x0: 1, x1: undefined }, { x0: 1, x1: undefined }], ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: undefined, x1: 1 }], - ] as Array<[string, ExtendedYConfig['fill'], XCoords, XCoords]>)( + ] as Array<[string, Exclude, XCoords, XCoords]>)( 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', (layerPrefix, fill, coordsA, coordsB) => { const value = coordsA.x0 ?? coordsA.x1!; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx index 9dca7b6107072..5d48c3c05166d 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx @@ -11,44 +11,11 @@ import './reference_lines.scss'; import React from 'react'; import { Position } from '@elastic/charts'; import type { FieldFormat } from '@kbn/field-formats-plugin/common'; -import type { CommonXYReferenceLineLayerConfig } from '../../../common/types'; -import { isReferenceLine, mapVerticalToHorizontalPlacement } from '../../helpers'; +import type { CommonXYReferenceLineLayerConfig, ReferenceLineConfig } from '../../../common/types'; +import { isReferenceLine } from '../../helpers'; import { ReferenceLineLayer } from './reference_line_layer'; import { ReferenceLine } from './reference_line'; - -export const computeChartMargins = ( - referenceLinePaddings: Partial>, - labelVisibility: Partial>, - titleVisibility: Partial>, - axesMap: Record<'left' | 'right', unknown>, - isHorizontal: boolean -) => { - const result: Partial> = {}; - if (!labelVisibility?.x && !titleVisibility?.x && referenceLinePaddings.bottom) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom'; - result[placement] = referenceLinePaddings.bottom; - } - if ( - referenceLinePaddings.left && - (isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft)) - ) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left'; - result[placement] = referenceLinePaddings.left; - } - if ( - referenceLinePaddings.right && - (isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight)) - ) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right'; - result[placement] = referenceLinePaddings.right; - } - // there's no top axis, so just check if a margin has been computed - if (referenceLinePaddings.top) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top'; - result[placement] = referenceLinePaddings.top; - } - return result; -}; +import { getNextValuesForReferenceLines } from './utils'; export interface ReferenceLinesProps { layers: CommonXYReferenceLineLayerConfig[]; @@ -59,6 +26,12 @@ export interface ReferenceLinesProps { } export const ReferenceLines = ({ layers, ...rest }: ReferenceLinesProps) => { + const referenceLines = layers.filter((layer): layer is ReferenceLineConfig => + isReferenceLine(layer) + ); + + const referenceLinesNextValues = getNextValuesForReferenceLines(referenceLines); + return ( <> {layers.flatMap((layer) => { @@ -66,13 +39,13 @@ export const ReferenceLines = ({ layers, ...rest }: ReferenceLinesProps) => { return null; } + const key = `referenceLine-${layer.layerId}`; if (isReferenceLine(layer)) { - return ; + const nextValue = referenceLinesNextValues[layer.yConfig[0].fill][layer.layerId]; + return ; } - return ( - - ); + return ; })} ); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx index 1a6eae6a490e6..85d96c573f314 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx @@ -10,7 +10,9 @@ import React from 'react'; import { Position } from '@elastic/charts'; import { euiLightVars } from '@kbn/ui-theme'; import { FieldFormat } from '@kbn/field-formats-plugin/common'; -import { IconPosition, YAxisMode } from '../../../common/types'; +import { groupBy, orderBy } from 'lodash'; +import { IconPosition, ReferenceLineConfig, YAxisMode, FillStyle } from '../../../common/types'; +import { FillStyles } from '../../../common/constants'; import { LINES_MARKER_SIZE, mapVerticalToHorizontalPlacement, @@ -141,3 +143,72 @@ export const getHorizontalRect = ( header: headerLabel, details: formatter?.convert(currentValue) || currentValue.toString(), }); + +const sortReferenceLinesByGroup = (referenceLines: ReferenceLineConfig[], group: FillStyle) => { + if (group === FillStyles.ABOVE || group === FillStyles.BELOW) { + const order = group === FillStyles.ABOVE ? 'asc' : 'desc'; + return orderBy(referenceLines, ({ yConfig: [{ value }] }) => value, [order]); + } + return referenceLines; +}; + +export const getNextValuesForReferenceLines = (referenceLines: ReferenceLineConfig[]) => { + const grouppedReferenceLines = groupBy(referenceLines, ({ yConfig: [yConfig] }) => yConfig.fill); + const groups = Object.keys(grouppedReferenceLines) as FillStyle[]; + + return groups.reduce>>( + (nextValueByDirection, group) => { + const sordedReferenceLines = sortReferenceLinesByGroup(grouppedReferenceLines[group], group); + + const nv = sordedReferenceLines.reduce>( + (nextValues, referenceLine, index, lines) => { + let nextValue: number | undefined; + if (index < lines.length - 1) { + const [yConfig] = lines[index + 1].yConfig; + nextValue = yConfig.value; + } + + return { ...nextValues, [referenceLine.layerId]: nextValue }; + }, + {} + ); + + return { ...nextValueByDirection, [group]: nv }; + }, + {} as Record> + ); +}; + +export const computeChartMargins = ( + referenceLinePaddings: Partial>, + labelVisibility: Partial>, + titleVisibility: Partial>, + axesMap: Record<'left' | 'right', unknown>, + isHorizontal: boolean +) => { + const result: Partial> = {}; + if (!labelVisibility?.x && !titleVisibility?.x && referenceLinePaddings.bottom) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom'; + result[placement] = referenceLinePaddings.bottom; + } + if ( + referenceLinePaddings.left && + (isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft)) + ) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left'; + result[placement] = referenceLinePaddings.left; + } + if ( + referenceLinePaddings.right && + (isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight)) + ) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right'; + result[placement] = referenceLinePaddings.right; + } + // there's no top axis, so just check if a margin has been computed + if (referenceLinePaddings.top) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top'; + result[placement] = referenceLinePaddings.top; + } + return result; +}; From d9f141a3e1bd4c9918b75e3b08d8a0ceac2cbf2c Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Fri, 20 May 2022 11:37:35 -0400 Subject: [PATCH 045/120] [Security Solution] Telemetry for Event Filters counts on both user and global entries (#132542) --- .../security_solution/server/lib/telemetry/tasks/endpoint.ts | 2 ++ .../plugins/security_solution/server/lib/telemetry/types.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts index 59bc07f8ca2eb..f6e3ca6e9d8ef 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts @@ -256,6 +256,7 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { malicious_behavior_rules: maliciousBehaviorRules, system_impact: systemImpact, threads, + event_filter: eventFilter, } = endpoint.endpoint_metrics.Endpoint.metrics; const endpointPolicyDetail = extractEndpointPolicyConfig(policyConfig); @@ -275,6 +276,7 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { maliciousBehaviorRules, systemImpact, threads, + eventFilter, }, endpoint_meta: { os: endpoint.endpoint_metrics.host.os, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts index 15c92740e3a71..d70a011ea85aa 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts @@ -233,6 +233,10 @@ export interface EndpointMetrics { library_load_events?: SystemImpactEventsMetrics; }>; threads: Array<{ name: string; cpu: { mean: number } }>; + event_filter: { + active_global_count: number; + active_user_count: number; + }; } interface EndpointMetricOS { From f70b4af7f2a5119bab5d56fb7a79d08f268570aa Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 20 May 2022 12:22:08 -0400 Subject: [PATCH 046/120] [Fleet] Fix rolling upgrade CANCEL and UI fixes (#132625) --- .../hooks/use_current_upgrades.tsx | 8 ++--- .../sections/agents/agent_list_page/index.tsx | 4 +-- .../server/services/agents/actions.test.ts | 30 +++++++++++++++++++ .../fleet/server/services/agents/actions.ts | 14 +++++++++ .../fleet/server/services/agents/upgrade.ts | 2 ++ 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_current_upgrades.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_current_upgrades.tsx index 02463025c86db..cdec2ad667be4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_current_upgrades.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_current_upgrades.tsx @@ -12,9 +12,9 @@ import { sendGetCurrentUpgrades, sendPostCancelAction, useStartServices } from ' import type { CurrentUpgrade } from '../../../../types'; -const POLL_INTERVAL = 30 * 1000; +const POLL_INTERVAL = 2 * 60 * 1000; // 2 minutes -export function useCurrentUpgrades() { +export function useCurrentUpgrades(onAbortSuccess: () => void) { const [currentUpgrades, setCurrentUpgrades] = useState([]); const currentTimeoutRef = useRef(); const isCancelledRef = useRef(false); @@ -65,7 +65,7 @@ export function useCurrentUpgrades() { return; } await sendPostCancelAction(currentUpgrade.actionId); - await refreshUpgrades(); + await Promise.all([refreshUpgrades(), onAbortSuccess()]); } catch (err) { notifications.toasts.addError(err, { title: i18n.translate('xpack.fleet.currentUpgrade.abortRequestError', { @@ -74,7 +74,7 @@ export function useCurrentUpgrades() { }); } }, - [refreshUpgrades, notifications.toasts, overlays] + [refreshUpgrades, notifications.toasts, overlays, onAbortSuccess] ); // Poll for upgrades diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 7ddf9b0f332f8..bbea3284f72b8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -338,7 +338,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }, [flyoutContext]); // Current upgrades - const { abortUpgrade, currentUpgrades, refreshUpgrades } = useCurrentUpgrades(); + const { abortUpgrade, currentUpgrades, refreshUpgrades } = useCurrentUpgrades(fetchData); const columns = [ { @@ -545,7 +545,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { selectionMode={selectionMode} currentQuery={kuery} selectedAgents={selectedAgents} - refreshAgents={() => fetchData()} + refreshAgents={() => Promise.all([fetchData(), refreshUpgrades()])} /> {/* Agent total, bulk actions and status bar */} diff --git a/x-pack/plugins/fleet/server/services/agents/actions.test.ts b/x-pack/plugins/fleet/server/services/agents/actions.test.ts index 2838f2204ad96..97d7c73035e6d 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.test.ts @@ -8,6 +8,11 @@ import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { cancelAgentAction } from './actions'; +import { bulkUpdateAgents } from './crud'; + +jest.mock('./crud'); + +const mockedBulkUpdateAgents = bulkUpdateAgents as jest.Mock; describe('Agent actions', () => { describe('cancelAgentAction', () => { @@ -67,5 +72,30 @@ describe('Agent actions', () => { }) ); }); + + it('should cancel UPGRADE action', async () => { + const esClient = elasticsearchServiceMock.createInternalClient(); + esClient.search.mockResolvedValue({ + hits: { + hits: [ + { + _source: { + type: 'UPGRADE', + action_id: 'action1', + agents: ['agent1', 'agent2'], + expiration: '2022-05-12T18:16:18.019Z', + }, + }, + ], + }, + } as any); + await cancelAgentAction(esClient, 'action1'); + + expect(mockedBulkUpdateAgents).toBeCalled(); + expect(mockedBulkUpdateAgents).toBeCalledWith(expect.anything(), [ + expect.objectContaining({ agentId: 'agent1' }), + expect.objectContaining({ agentId: 'agent2' }), + ]); + }); }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index afa65bfe91fb3..c4f3530892543 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -17,6 +17,8 @@ import type { import { AGENT_ACTIONS_INDEX, SO_SEARCH_LIMIT } from '../../../common/constants'; import { AgentActionNotFoundError } from '../../errors'; +import { bulkUpdateAgents } from './crud'; + const ONE_MONTH_IN_MS = 2592000000; export async function createAgentAction( @@ -131,6 +133,18 @@ export async function cancelAgentAction(esClient: ElasticsearchClient, actionId: created_at: now, expiration: hit._source.expiration, }); + if (hit._source.type === 'UPGRADE') { + await bulkUpdateAgents( + esClient, + hit._source.agents.map((agentId) => ({ + agentId, + data: { + upgraded_at: null, + upgrade_started_at: null, + }, + })) + ); + } } return { diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 6d0174e064184..d7f2735e2d284 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -267,6 +267,7 @@ async function _getCancelledActionId( ) { const res = await esClient.search({ index: AGENT_ACTIONS_INDEX, + ignore_unavailable: true, query: { bool: { must: [ @@ -296,6 +297,7 @@ async function _getCancelledActionId( async function _getUpgradeActions(esClient: ElasticsearchClient, now = new Date().toISOString()) { const res = await esClient.search({ index: AGENT_ACTIONS_INDEX, + ignore_unavailable: true, query: { bool: { must: [ From d70ae0fa8a02fc927b932b2a17e539272f4ed5fc Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Fri, 20 May 2022 11:34:35 -0500 Subject: [PATCH 047/120] [ILM] Add warnings for managed system policies (#132269) * Add warnings to system/managed policies * Fix translations, policies * Add jest tests * Add jest tests to assert new toggle behavior * Add jest tests for edit policy callout * Fix snapshot * [ML] Update jest tests with helper, rename helper for clarity * [ML] Add hook for local storage to remember toggle setting * [ML] Fix naming Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/policy_table.test.tsx.snap | 64 +++++----- .../edit_policy/constants.ts | 23 ++++ .../edit_policy/features/edit_warning.test.ts | 15 ++- .../__jest__/policy_table.test.tsx | 112 +++++++++++++++++- .../application/lib/settings_local_storage.ts | 31 +++++ .../edit_policy/components/edit_warning.tsx | 29 ++++- .../policy_list/components/confirm_delete.tsx | 62 ++++++++-- .../policy_list/components/policy_table.tsx | 101 +++++++++++++--- 8 files changed, 375 insertions(+), 62 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/lib/settings_local_storage.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap index 8cbb4aa450c7c..32d2b96675594 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap @@ -7,26 +7,26 @@ Array [ "testy10", "testy100", "testy101", - "testy102", "testy103", "testy104", "testy11", - "testy12", + "testy13", + "testy14", ] `; exports[`policy table changes pages when a pagination link is clicked on 2`] = ` Array [ - "testy13", - "testy14", - "testy15", "testy16", "testy17", - "testy18", "testy19", "testy2", "testy20", - "testy21", + "testy22", + "testy23", + "testy25", + "testy26", + "testy28", ] `; @@ -113,15 +113,15 @@ exports[`policy table shows empty state when there are no policies 1`] = ` exports[`policy table sorts when linked index templates header is clicked 1`] = ` Array [ "testy1", - "testy3", "testy5", "testy7", - "testy9", "testy11", "testy13", - "testy15", "testy17", "testy19", + "testy23", + "testy25", + "testy29", ] `; @@ -130,28 +130,28 @@ Array [ "testy0", "testy2", "testy4", - "testy6", "testy8", "testy10", - "testy12", "testy14", "testy16", - "testy18", + "testy20", + "testy22", + "testy26", ] `; exports[`policy table sorts when linked indices header is clicked 1`] = ` Array [ "testy1", - "testy3", "testy5", "testy7", - "testy9", "testy11", "testy13", - "testy15", "testy17", "testy19", + "testy23", + "testy25", + "testy29", ] `; @@ -160,13 +160,13 @@ Array [ "testy0", "testy2", "testy4", - "testy6", "testy8", "testy10", - "testy12", "testy14", "testy16", - "testy18", + "testy20", + "testy22", + "testy26", ] `; @@ -175,13 +175,13 @@ Array [ "testy0", "testy104", "testy103", - "testy102", "testy101", "testy100", - "testy99", "testy98", "testy97", - "testy96", + "testy95", + "testy94", + "testy92", ] `; @@ -189,29 +189,29 @@ exports[`policy table sorts when modified date header is clicked 2`] = ` Array [ "testy1", "testy2", - "testy3", "testy4", "testy5", - "testy6", "testy7", "testy8", - "testy9", "testy10", + "testy11", + "testy13", + "testy14", ] `; exports[`policy table sorts when name header is clicked 1`] = ` Array [ - "testy99", "testy98", "testy97", - "testy96", "testy95", "testy94", - "testy93", "testy92", "testy91", - "testy90", + "testy89", + "testy88", + "testy86", + "testy85", ] `; @@ -220,12 +220,12 @@ Array [ "testy0", "testy1", "testy2", - "testy3", "testy4", "testy5", - "testy6", "testy7", "testy8", - "testy9", + "testy10", + "testy11", + "testy13", ] `; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index f57f351ae0831..620cb9d6f8dde 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -221,6 +221,29 @@ export const POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS = { name: POLICY_NAME, } as any as PolicyFromES; +export const POLICY_MANAGED_BY_ES: PolicyFromES = { + version: 1, + modifiedDate: Date.now().toString(), + policy: { + name: POLICY_NAME, + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_age: '30d', + max_primary_shard_size: '50gb', + }, + }, + }, + }, + _meta: { + managed: true, + }, + }, + name: POLICY_NAME, +}; + export const getGeneratedPolicies = (): PolicyFromES[] => { const policy = { phases: { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/edit_warning.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/edit_warning.test.ts index 0cf57f4140aa4..98d6078da031c 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/edit_warning.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/edit_warning.test.ts @@ -9,7 +9,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test-jest-helpers'; import { setupEnvironment } from '../../helpers'; import { initTestBed } from '../init_test_bed'; -import { getDefaultHotPhasePolicy, POLICY_NAME } from '../constants'; +import { getDefaultHotPhasePolicy, POLICY_NAME, POLICY_MANAGED_BY_ES } from '../constants'; describe(' edit warning', () => { let testBed: TestBed; @@ -54,6 +54,19 @@ describe(' edit warning', () => { expect(exists('editWarning')).toBe(true); }); + test('an edit warning callout is shown for an existing, managed policy', async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_MANAGED_BY_ES]); + + await act(async () => { + testBed = await initTestBed(httpSetup); + }); + const { exists, component } = testBed; + component.update(); + + expect(exists('editWarning')).toBe(true); + expect(exists('editManagedPolicyCallOut')).toBe(true); + }); + test('no indices link if no indices', async () => { httpRequestsMockHelpers.setLoadPolicies([ { ...getDefaultHotPhasePolicy(POLICY_NAME), indices: [] }, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx index 771cf70e3daea..0e8ac17ff86c2 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx @@ -52,17 +52,27 @@ const testPolicy = { }, }; +const isUsedByAnIndex = (i: number) => i % 2 === 0; +const isDesignatedManagedPolicy = (i: number) => i > 0 && i % 3 === 0; + const policies: PolicyFromES[] = [testPolicy]; for (let i = 1; i < 105; i++) { policies.push({ version: i, modifiedDate: moment().subtract(i, 'days').toISOString(), - indices: i % 2 === 0 ? [`index${i}`] : [], + indices: isUsedByAnIndex(i) ? [`index${i}`] : [], indexTemplates: i % 2 === 0 ? [`indexTemplate${i}`] : [], name: `testy${i}`, policy: { name: `testy${i}`, phases: {}, + ...(isDesignatedManagedPolicy(i) + ? { + _meta: { + managed: true, + }, + } + : {}), }, }); } @@ -89,6 +99,20 @@ const getPolicyNames = (rendered: ReactWrapper): string[] => { return (getPolicyLinks(rendered) as ReactWrapper).map((button) => button.text()); }; +const getPolicies = (rendered: ReactWrapper) => { + const visiblePolicyNames = getPolicyNames(rendered); + const visiblePolicies = visiblePolicyNames.map((name) => { + const version = parseInt(name.replace('testy', ''), 10); + return { + version, + name, + isManagedPolicy: isDesignatedManagedPolicy(version), + isUsedByAnIndex: isUsedByAnIndex(version), + }; + }); + return visiblePolicies; +}; + const testSort = (headerName: string) => { const rendered = mountWithIntl(component); const nameHeader = findTestSubject(rendered, `tableHeaderCell_${headerName}`).find('button'); @@ -114,6 +138,7 @@ const TestComponent = ({ testPolicies }: { testPolicies: PolicyFromES[] }) => { describe('policy table', () => { beforeEach(() => { component = ; + window.localStorage.removeItem('ILM_SHOW_MANAGED_POLICIES_BY_DEFAULT'); }); test('shows empty state when there are no policies', () => { @@ -129,8 +154,23 @@ describe('policy table', () => { rendered.update(); snapshot(getPolicyNames(rendered)); }); + + test('does not show any hidden policies by default', () => { + const rendered = mountWithIntl(component); + const includeHiddenPoliciesSwitch = findTestSubject(rendered, `includeHiddenPoliciesSwitch`); + expect(includeHiddenPoliciesSwitch.prop('aria-checked')).toEqual(false); + const visiblePolicies = getPolicies(rendered); + const hasManagedPolicies = visiblePolicies.some((p) => { + const policyRow = findTestSubject(rendered, `policyTableRow-${p.name}`); + const warningBadge = findTestSubject(policyRow, 'managedPolicyBadge'); + return warningBadge.exists(); + }); + expect(hasManagedPolicies).toEqual(false); + }); + test('shows more policies when "Rows per page" value is increased', () => { const rendered = mountWithIntl(component); + const perPageButton = rendered.find('EuiTablePagination EuiPopover').find('button'); perPageButton.simulate('click'); rendered.update(); @@ -139,6 +179,36 @@ describe('policy table', () => { rendered.update(); expect(getPolicyNames(rendered).length).toBe(25); }); + + test('shows hidden policies with Managed badges when setting is switched on', () => { + const rendered = mountWithIntl(component); + const includeHiddenPoliciesSwitch = findTestSubject(rendered, `includeHiddenPoliciesSwitch`); + includeHiddenPoliciesSwitch.find('button').simulate('click'); + rendered.update(); + + // Increase page size for better sample set that contains managed indices + // Since table is ordered alphabetically and not numerically + const perPageButton = rendered.find('EuiTablePagination EuiPopover').find('button'); + perPageButton.simulate('click'); + rendered.update(); + const numberOfRowsButton = rendered.find('.euiContextMenuItem').at(2); + numberOfRowsButton.simulate('click'); + rendered.update(); + + const visiblePolicies = getPolicies(rendered); + expect(visiblePolicies.filter((p) => p.isManagedPolicy).length).toBeGreaterThan(0); + + visiblePolicies.forEach((p) => { + const policyRow = findTestSubject(rendered, `policyTableRow-${p.name}`); + const warningBadge = findTestSubject(policyRow, 'managedPolicyBadge'); + if (p.isManagedPolicy) { + expect(warningBadge.exists()).toBeTruthy(); + } else { + expect(warningBadge.exists()).toBeFalsy(); + } + }); + }); + test('filters based on content of search input', () => { const rendered = mountWithIntl(component); const searchInput = rendered.find('.euiFieldSearch').first(); @@ -167,7 +237,11 @@ describe('policy table', () => { }); test('delete policy button is enabled when there are no linked indices', () => { const rendered = mountWithIntl(component); - const policyRow = findTestSubject(rendered, `policyTableRow-testy1`); + const visiblePolicies = getPolicies(rendered); + const unusedPolicy = visiblePolicies.find((p) => !p.isUsedByAnIndex); + expect(unusedPolicy).toBeDefined(); + + const policyRow = findTestSubject(rendered, `policyTableRow-${unusedPolicy!.name}`); const deleteButton = findTestSubject(policyRow, 'deletePolicy'); expect(deleteButton.props().disabled).toBeFalsy(); }); @@ -179,6 +253,36 @@ describe('policy table', () => { rendered.update(); expect(findTestSubject(rendered, 'deletePolicyModal').exists()).toBeTruthy(); }); + + test('confirmation modal shows warning when delete button is pressed for a hidden policy', () => { + const rendered = mountWithIntl(component); + + // Toggles switch to show managed policies + const includeHiddenPoliciesSwitch = findTestSubject(rendered, `includeHiddenPoliciesSwitch`); + includeHiddenPoliciesSwitch.find('button').simulate('click'); + rendered.update(); + + // Increase page size for better sample set that contains managed indices + // Since table is ordered alphabetically and not numerically + const perPageButton = rendered.find('EuiTablePagination EuiPopover').find('button'); + perPageButton.simulate('click'); + rendered.update(); + const numberOfRowsButton = rendered.find('.euiContextMenuItem').at(2); + numberOfRowsButton.simulate('click'); + rendered.update(); + + const visiblePolicies = getPolicies(rendered); + const managedPolicy = visiblePolicies.find((p) => p.isManagedPolicy && !p.isUsedByAnIndex); + expect(managedPolicy).toBeDefined(); + + const policyRow = findTestSubject(rendered, `policyTableRow-${managedPolicy!.name}`); + const addPolicyToTemplateButton = findTestSubject(policyRow, 'deletePolicy'); + addPolicyToTemplateButton.simulate('click'); + rendered.update(); + expect(findTestSubject(rendered, 'deletePolicyModal').exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'deleteManagedPolicyCallOut').exists()).toBeTruthy(); + }); + test('add index template modal shows when add policy to index template button is pressed', () => { const rendered = mountWithIntl(component); const policyRow = findTestSubject(rendered, `policyTableRow-${testPolicy.name}`); @@ -190,8 +294,8 @@ describe('policy table', () => { test('displays policy properties', () => { const rendered = mountWithIntl(component); const firstRow = findTestSubject(rendered, 'policyTableRow-testy0'); - const policyName = findTestSubject(firstRow, 'policy-name').text(); - expect(policyName).toBe(`Name${testPolicy.name}`); + const policyName = findTestSubject(firstRow, 'policyTablePolicyNameLink').text(); + expect(policyName).toBe(`${testPolicy.name}`); const policyIndexTemplates = findTestSubject(firstRow, 'policy-indexTemplates').text(); expect(policyIndexTemplates).toBe(`Linked index templates${testPolicy.indexTemplates.length}`); const policyIndices = findTestSubject(firstRow, 'policy-indices').text(); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/settings_local_storage.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/settings_local_storage.ts new file mode 100644 index 0000000000000..0eb5ae22fd01c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/settings_local_storage.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; + +function parseJsonOrDefault(value: string | null, defaultValue: Obj): Obj { + if (!value) { + return defaultValue; + } + try { + return JSON.parse(value) as Obj; + } catch (e) { + return defaultValue; + } +} + +export function useStateWithLocalStorage( + key: string, + defaultState: State +): [State, Dispatch>] { + const storageState = localStorage.getItem(key); + const [state, setState] = useState(parseJsonOrDefault(storageState, defaultState)); + useEffect(() => { + localStorage.setItem(key, JSON.stringify(state)); + }, [key, state]); + return [state, setState]; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/edit_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/edit_warning.tsx index 8b0c21e9999c0..c2acc89fe34d1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/edit_warning.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/edit_warning.tsx @@ -6,7 +6,7 @@ */ import React, { FunctionComponent, useState } from 'react'; -import { EuiLink, EuiText } from '@elastic/eui'; +import { EuiCallOut, EuiLink, EuiText, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useEditPolicyContext } from '../edit_policy_context'; import { getIndicesListPath } from '../../../services/navigation'; @@ -14,7 +14,7 @@ import { useKibana } from '../../../../shared_imports'; import { IndexTemplatesFlyout } from '../../../components/index_templates_flyout'; export const EditWarning: FunctionComponent = () => { - const { isNewPolicy, indices, indexTemplates, policyName } = useEditPolicyContext(); + const { isNewPolicy, indices, indexTemplates, policyName, policy } = useEditPolicyContext(); const { services: { getUrlForApp }, } = useKibana(); @@ -67,6 +67,8 @@ export const EditWarning: FunctionComponent = () => { ) : ( indexTemplatesLink ); + const isManagedPolicy = policy?._meta?.managed; + return ( <> {isIndexTemplatesFlyoutShown && ( @@ -77,6 +79,29 @@ export const EditWarning: FunctionComponent = () => { /> )} + {isManagedPolicy && ( + <> + + } + color="danger" + iconType="alert" + data-test-subj="editManagedPolicyCallOut" + > +

+ +

+
+ + + )}

void; } export class ConfirmDelete extends Component { + public state = { + isDeleteConfirmed: false, + }; + + setIsDeleteConfirmed = (confirmed: boolean) => { + this.setState({ + isDeleteConfirmed: confirmed, + }); + }; + deletePolicy = async () => { const { policyToDelete, callback } = this.props; const policyName = policyToDelete.name; @@ -43,8 +53,12 @@ export class ConfirmDelete extends Component { callback(); } }; + isPolicyPolicy = true; render() { const { policyToDelete, onCancel } = this.props; + const { isDeleteConfirmed } = this.state; + const isManagedPolicy = policyToDelete.policy?._meta?.managed; + const title = i18n.translate('xpack.indexLifecycleMgmt.confirmDelete.title', { defaultMessage: 'Delete policy "{name}"', values: { name: policyToDelete.name }, @@ -68,13 +82,47 @@ export class ConfirmDelete extends Component { /> } buttonColor="danger" + confirmButtonDisabled={isManagedPolicy ? !isDeleteConfirmed : false} > -

- -
+ {isManagedPolicy ? ( + + } + color="danger" + iconType="alert" + data-test-subj="deleteManagedPolicyCallOut" + > +

+ +

+ + } + checked={isDeleteConfirmed} + onChange={(e) => this.setIsDeleteConfirmed(e.target.checked)} + /> +
+ ) : ( +
+ +
+ )} ); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/components/policy_table.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/components/policy_table.tsx index 8a89759a4225e..2d79737baf2bc 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/components/policy_table.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/components/policy_table.tsx @@ -5,8 +5,17 @@ * 2.0. */ -import React from 'react'; -import { EuiButtonEmpty, EuiLink, EuiInMemoryTable, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { + EuiButtonEmpty, + EuiLink, + EuiInMemoryTable, + EuiToolTip, + EuiButtonIcon, + EuiBadge, + EuiFlexItem, + EuiSwitch, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -15,6 +24,8 @@ import { METRIC_TYPE } from '@kbn/analytics'; import { useHistory } from 'react-router-dom'; import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useStateWithLocalStorage } from '../../../lib/settings_local_storage'; import { PolicyFromES } from '../../../../../common/types'; import { useKibana } from '../../../../shared_imports'; import { getIndicesListPath, getPolicyEditPath } from '../../../services/navigation'; @@ -45,17 +56,63 @@ const actionTooltips = { ), }; +const managedPolicyTooltips = { + badge: i18n.translate('xpack.indexLifecycleMgmt.policyTable.templateBadgeType.managedLabel', { + defaultMessage: 'Managed', + }), + badgeTooltip: i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.templateBadgeType.managedDescription', + { + defaultMessage: + 'This policy is preconfigured and managed by Elastic; editing or deleting this policy might break Kibana.', + } + ), +}; + interface Props { policies: PolicyFromES[]; } +const SHOW_MANAGED_POLICIES_BY_DEFAULT = 'ILM_SHOW_MANAGED_POLICIES_BY_DEFAULT'; + export const PolicyTable: React.FunctionComponent = ({ policies }) => { const history = useHistory(); const { services: { getUrlForApp }, } = useKibana(); - + const [managedPoliciesVisible, setManagedPoliciesVisible] = useStateWithLocalStorage( + SHOW_MANAGED_POLICIES_BY_DEFAULT, + false + ); const { setListAction } = usePolicyListContext(); + const searchOptions = useMemo( + () => ({ + box: { incremental: true, 'data-test-subj': 'ilmSearchBar' }, + toolsRight: ( + + setManagedPoliciesVisible(event.target.checked)} + label={ + + } + /> + + ), + }), + [managedPoliciesVisible, setManagedPoliciesVisible] + ); + + const filteredPolicies = useMemo(() => { + return managedPoliciesVisible + ? policies + : policies.filter((item) => !item.policy?._meta?.managed); + }, [policies, managedPoliciesVisible]); const columns: Array> = [ { @@ -65,17 +122,31 @@ export const PolicyTable: React.FunctionComponent = ({ policies }) => { defaultMessage: 'Name', }), sortable: true, - render: (value: string) => { + render: (value: string, item) => { + const isManaged = item.policy?._meta?.managed; return ( - - trackUiMetric(METRIC_TYPE.CLICK, UIM_EDIT_CLICK) + <> + + trackUiMetric(METRIC_TYPE.CLICK, UIM_EDIT_CLICK) + )} + > + {value} + + + {isManaged && ( + <> +   + + + {managedPolicyTooltips.badge} + + + )} - > - {value} - + ); }, }, @@ -191,11 +262,9 @@ export const PolicyTable: React.FunctionComponent = ({ policies }) => { direction: 'asc', }, }} - search={{ - box: { incremental: true, 'data-test-subj': 'ilmSearchBar' }, - }} + search={searchOptions} tableLayout="auto" - items={policies} + items={filteredPolicies} columns={columns} rowProps={(policy: PolicyFromES) => ({ 'data-test-subj': `policyTableRow-${policy.name}` })} /> From c24488361a0f53b9d71d4248dd1a57d00520adad Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 20 May 2022 10:35:00 -0600 Subject: [PATCH 048/120] [maps] show marker size in legend (#132549) * [Maps] size legend * clean-up * refine spacing * clean up * more cleanup * use euiTheme for colors * fix jest test * do not show marker sizes for icons * remove lodash Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/legend/marker_size_legend.tsx | 164 ++++++++++++++++ .../dynamic_size_property.test.tsx.snap | 176 +++++++++++++++++- .../dynamic_size_property.test.tsx | 48 ++++- .../dynamic_size_property.tsx | 7 +- 4 files changed, 386 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/legend/marker_size_legend.tsx diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/marker_size_legend.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/marker_size_legend.tsx new file mode 100644 index 0000000000000..295e7c57b7a22 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/marker_size_legend.tsx @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component } from 'react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; +import { DynamicSizeProperty } from '../../properties/dynamic_size_property'; + +const FONT_SIZE = 10; +const HALF_FONT_SIZE = FONT_SIZE / 2; +const MIN_MARKER_DISTANCE = (FONT_SIZE + 2) / 2; + +const EMPTY_VALUE = ''; + +interface Props { + style: DynamicSizeProperty; +} + +interface State { + label: string; +} + +export class MarkerSizeLegend extends Component { + private _isMounted: boolean = false; + + state: State = { + label: EMPTY_VALUE, + }; + + componentDidMount() { + this._isMounted = true; + this._loadLabel(); + } + + componentDidUpdate() { + this._loadLabel(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + async _loadLabel() { + const field = this.props.style.getField(); + if (!field) { + return; + } + const label = await field.getLabel(); + if (this._isMounted && this.state.label !== label) { + this.setState({ label }); + } + } + + _formatValue(value: string | number) { + return value === EMPTY_VALUE ? value : this.props.style.formatField(value); + } + + _renderMarkers() { + const fieldMeta = this.props.style.getRangeFieldMeta(); + const options = this.props.style.getOptions(); + if (!fieldMeta || !options) { + return null; + } + + const circleStyle = { + fillOpacity: 0, + stroke: euiThemeVars.euiTextColor, + strokeWidth: 1, + }; + + const svgHeight = options.maxSize * 2 + HALF_FONT_SIZE + circleStyle.strokeWidth * 2; + const circleCenterX = options.maxSize + circleStyle.strokeWidth; + const circleBottomY = svgHeight - circleStyle.strokeWidth; + + function makeMarker(radius: number, formattedValue: string | number) { + const circleCenterY = circleBottomY - radius; + const circleTopY = circleCenterY - radius; + return ( + + + + {formattedValue} + + + + ); + } + + function getMarkerRadius(percentage: number) { + const delta = options.maxSize - options.minSize; + return percentage * delta + options.minSize; + } + + function getValue(percentage: number) { + // Markers interpolated by area instead of radius to be more consistent with how the human eye+brain perceive shapes + // and their visual relevance + // This function mirrors output of maplibre expression created from DynamicSizeProperty.getMbSizeExpression + const value = Math.pow(percentage * Math.sqrt(fieldMeta!.delta), 2) + fieldMeta!.min; + return fieldMeta!.delta > 3 ? Math.round(value) : value; + } + + const markers = []; + + if (fieldMeta.delta > 0) { + const smallestMarker = makeMarker(options.minSize, this._formatValue(fieldMeta.min)); + markers.push(smallestMarker); + + const markerDelta = options.maxSize - options.minSize; + if (markerDelta > MIN_MARKER_DISTANCE * 3) { + markers.push(makeMarker(getMarkerRadius(0.25), this._formatValue(getValue(0.25)))); + markers.push(makeMarker(getMarkerRadius(0.5), this._formatValue(getValue(0.5)))); + markers.push(makeMarker(getMarkerRadius(0.75), this._formatValue(getValue(0.75)))); + } else if (markerDelta > MIN_MARKER_DISTANCE) { + markers.push(makeMarker(getMarkerRadius(0.5), this._formatValue(getValue(0.5)))); + } + } + + const largestMarker = makeMarker(options.maxSize, this._formatValue(fieldMeta.max)); + markers.push(largestMarker); + + return ( + + {markers} + + ); + } + + render() { + return ( +
+ + + + + + {this.state.label} + + + + + + {this._renderMarkers()} +
+ ); + } +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/__snapshots__/dynamic_size_property.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/__snapshots__/dynamic_size_property.test.tsx.snap index 9dc0e99669c79..bf239aa40e33a 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/__snapshots__/dynamic_size_property.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/__snapshots__/dynamic_size_property.test.tsx.snap @@ -1,6 +1,165 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renderLegendDetailRow Should render as range 1`] = ` +exports[`renderLegendDetailRow Should render icon size scale 1`] = ` +
+ + + + + + + foobar_label + + + + + + + + + + + 0_format + + + + + + + 25_format + + + + + + + 100_format + + + + +
+`; + +exports[`renderLegendDetailRow Should render line width simple range 1`] = ` @@ -36,9 +196,10 @@ exports[`renderLegendDetailRow Should render as range 1`] = ` @@ -56,8 +217,9 @@ exports[`renderLegendDetailRow Should render as range 1`] = ` `; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.test.tsx index 0446b9e30f47b..9f92d81313da7 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.test.tsx @@ -20,7 +20,53 @@ import { IField } from '../../../../fields/field'; import { IVectorLayer } from '../../../../layers/vector_layer'; describe('renderLegendDetailRow', () => { - test('Should render as range', async () => { + test('Should render line width simple range', async () => { + const field = { + getLabel: async () => { + return 'foobar_label'; + }, + getName: () => { + return 'foodbar'; + }, + getOrigin: () => { + return FIELD_ORIGIN.SOURCE; + }, + supportsFieldMetaFromEs: () => { + return true; + }, + supportsFieldMetaFromLocalData: () => { + return true; + }, + } as unknown as IField; + const sizeProp = new DynamicSizeProperty( + { minSize: 0, maxSize: 10, fieldMetaOptions: { isEnabled: true } }, + VECTOR_STYLES.LINE_WIDTH, + field, + {} as unknown as IVectorLayer, + () => { + return (value: RawValue) => value + '_format'; + }, + false + ); + sizeProp.getRangeFieldMeta = () => { + return { + min: 0, + max: 100, + delta: 100, + }; + }; + + const legendRow = sizeProp.renderLegendDetailRow(); + const component = shallow(legendRow); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + expect(component).toMatchSnapshot(); + }); + + test('Should render icon size scale', async () => { const field = { getLabel: async () => { return 'foobar_label'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.tsx index d8fe8463edba8..83ac50c7b4eaa 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.tsx @@ -9,6 +9,7 @@ import React from 'react'; import type { Map as MbMap } from '@kbn/mapbox-gl'; import { DynamicStyleProperty } from '../dynamic_style_property'; import { OrdinalLegend } from '../../components/legend/ordinal_legend'; +import { MarkerSizeLegend } from '../../components/legend/marker_size_legend'; import { makeMbClampedNumberExpression } from '../../style_util'; import { FieldFormatter, @@ -141,6 +142,10 @@ export class DynamicSizeProperty extends DynamicStyleProperty; + return this.getStyleName() === VECTOR_STYLES.ICON_SIZE && !this._isSymbolizedAsIcon ? ( + + ) : ( + + ); } } From 583d2b78e085ec7e51f6d7608b0d1fe75f1bfcc4 Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Fri, 20 May 2022 13:12:32 -0400 Subject: [PATCH 049/120] [Workplace Search] Add documentation links for v8.3.0 connectors (#132547) --- packages/kbn-doc-links/src/get_doc_links.ts | 6 ++++++ packages/kbn-doc-links/src/types.ts | 6 ++++++ .../shared/doc_links/doc_links.ts | 20 +++++++++++++++++++ .../views/content_sources/source_data.tsx | 12 +++++------ 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 55909e360b0e5..53f69411c43dd 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -125,7 +125,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { apiKeys: `${WORKPLACE_SEARCH_DOCS}workplace-search-api-authentication.html`, box: `${WORKPLACE_SEARCH_DOCS}workplace-search-box-connector.html`, confluenceCloud: `${WORKPLACE_SEARCH_DOCS}workplace-search-confluence-cloud-connector.html`, + confluenceCloudConnectorPackage: `${WORKPLACE_SEARCH_DOCS}confluence-cloud.html`, confluenceServer: `${WORKPLACE_SEARCH_DOCS}workplace-search-confluence-server-connector.html`, + customConnectorPackage: `${WORKPLACE_SEARCH_DOCS}custom-connector-package.html`, customSources: `${WORKPLACE_SEARCH_DOCS}workplace-search-custom-api-sources.html`, customSourcePermissions: `${WORKPLACE_SEARCH_DOCS}workplace-search-custom-api-sources.html#custom-api-source-document-level-access-control`, documentPermissions: `${WORKPLACE_SEARCH_DOCS}workplace-search-sources-document-permissions.html`, @@ -139,7 +141,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { indexingSchedule: `${WORKPLACE_SEARCH_DOCS}workplace-search-customizing-indexing-rules.html#_indexing_schedule`, jiraCloud: `${WORKPLACE_SEARCH_DOCS}workplace-search-jira-cloud-connector.html`, jiraServer: `${WORKPLACE_SEARCH_DOCS}workplace-search-jira-server-connector.html`, + networkDrive: `${WORKPLACE_SEARCH_DOCS}network-drives.html`, oneDrive: `${WORKPLACE_SEARCH_DOCS}workplace-search-onedrive-connector.html`, + outlook: `${WORKPLACE_SEARCH_DOCS}microsoft-outlook.html`, permissions: `${WORKPLACE_SEARCH_DOCS}workplace-search-permissions.html#organizational-sources-private-sources`, salesforce: `${WORKPLACE_SEARCH_DOCS}workplace-search-salesforce-connector.html`, security: `${WORKPLACE_SEARCH_DOCS}workplace-search-security.html`, @@ -148,7 +152,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { sharePointServer: `${WORKPLACE_SEARCH_DOCS}sharepoint-server.html`, slack: `${WORKPLACE_SEARCH_DOCS}workplace-search-slack-connector.html`, synch: `${WORKPLACE_SEARCH_DOCS}workplace-search-customizing-indexing-rules.html`, + teams: `${WORKPLACE_SEARCH_DOCS}microsoft-teams.html`, zendesk: `${WORKPLACE_SEARCH_DOCS}workplace-search-zendesk-connector.html`, + zoom: `${WORKPLACE_SEARCH_DOCS}zoom.html`, }, metricbeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index c492509e80511..6dc3ad0f5fdda 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -111,7 +111,9 @@ export interface DocLinks { readonly apiKeys: string; readonly box: string; readonly confluenceCloud: string; + readonly confluenceCloudConnectorPackage: string; readonly confluenceServer: string; + readonly customConnectorPackage: string; readonly customSources: string; readonly customSourcePermissions: string; readonly documentPermissions: string; @@ -125,7 +127,9 @@ export interface DocLinks { readonly indexingSchedule: string; readonly jiraCloud: string; readonly jiraServer: string; + readonly networkDrive: string; readonly oneDrive: string; + readonly outlook: string; readonly permissions: string; readonly salesforce: string; readonly security: string; @@ -134,7 +138,9 @@ export interface DocLinks { readonly sharePointServer: string; readonly slack: string; readonly synch: string; + readonly teams: string; readonly zendesk: string; + readonly zoom: string; }; readonly heartbeat: { readonly base: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index b037a5aed6217..1d38cb584fa43 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -64,7 +64,9 @@ class DocLinks { public workplaceSearchApiKeys: string; public workplaceSearchBox: string; public workplaceSearchConfluenceCloud: string; + public workplaceSearchConfluenceCloudConnectorPackage: string; public workplaceSearchConfluenceServer: string; + public workplaceSearchCustomConnectorPackage: string; public workplaceSearchCustomSources: string; public workplaceSearchCustomSourcePermissions: string; public workplaceSearchDocumentPermissions: string; @@ -78,7 +80,9 @@ class DocLinks { public workplaceSearchIndexingSchedule: string; public workplaceSearchJiraCloud: string; public workplaceSearchJiraServer: string; + public workplaceSearchNetworkDrive: string; public workplaceSearchOneDrive: string; + public workplaceSearchOutlook: string; public workplaceSearchPermissions: string; public workplaceSearchSalesforce: string; public workplaceSearchSecurity: string; @@ -87,7 +91,9 @@ class DocLinks { public workplaceSearchSharePointServer: string; public workplaceSearchSlack: string; public workplaceSearchSynch: string; + public workplaceSearchTeams: string; public workplaceSearchZendesk: string; + public workplaceSearchZoom: string; constructor() { this.appSearchApis = ''; @@ -146,7 +152,9 @@ class DocLinks { this.workplaceSearchApiKeys = ''; this.workplaceSearchBox = ''; this.workplaceSearchConfluenceCloud = ''; + this.workplaceSearchConfluenceCloudConnectorPackage = ''; this.workplaceSearchConfluenceServer = ''; + this.workplaceSearchCustomConnectorPackage = ''; this.workplaceSearchCustomSources = ''; this.workplaceSearchCustomSourcePermissions = ''; this.workplaceSearchDocumentPermissions = ''; @@ -160,7 +168,9 @@ class DocLinks { this.workplaceSearchIndexingSchedule = ''; this.workplaceSearchJiraCloud = ''; this.workplaceSearchJiraServer = ''; + this.workplaceSearchNetworkDrive = ''; this.workplaceSearchOneDrive = ''; + this.workplaceSearchOutlook = ''; this.workplaceSearchPermissions = ''; this.workplaceSearchSalesforce = ''; this.workplaceSearchSecurity = ''; @@ -169,7 +179,9 @@ class DocLinks { this.workplaceSearchSharePointServer = ''; this.workplaceSearchSlack = ''; this.workplaceSearchSynch = ''; + this.workplaceSearchTeams = ''; this.workplaceSearchZendesk = ''; + this.workplaceSearchZoom = ''; } public setDocLinks(docLinks: DocLinksStart): void { @@ -230,7 +242,11 @@ class DocLinks { this.workplaceSearchApiKeys = docLinks.links.workplaceSearch.apiKeys; this.workplaceSearchBox = docLinks.links.workplaceSearch.box; this.workplaceSearchConfluenceCloud = docLinks.links.workplaceSearch.confluenceCloud; + this.workplaceSearchConfluenceCloudConnectorPackage = + docLinks.links.workplaceSearch.confluenceCloudConnectorPackage; this.workplaceSearchConfluenceServer = docLinks.links.workplaceSearch.confluenceServer; + this.workplaceSearchCustomConnectorPackage = + docLinks.links.workplaceSearch.customConnectorPackage; this.workplaceSearchCustomSources = docLinks.links.workplaceSearch.customSources; this.workplaceSearchCustomSourcePermissions = docLinks.links.workplaceSearch.customSourcePermissions; @@ -246,7 +262,9 @@ class DocLinks { this.workplaceSearchIndexingSchedule = docLinks.links.workplaceSearch.indexingSchedule; this.workplaceSearchJiraCloud = docLinks.links.workplaceSearch.jiraCloud; this.workplaceSearchJiraServer = docLinks.links.workplaceSearch.jiraServer; + this.workplaceSearchNetworkDrive = docLinks.links.workplaceSearch.networkDrive; this.workplaceSearchOneDrive = docLinks.links.workplaceSearch.oneDrive; + this.workplaceSearchOutlook = docLinks.links.workplaceSearch.outlook; this.workplaceSearchPermissions = docLinks.links.workplaceSearch.permissions; this.workplaceSearchSalesforce = docLinks.links.workplaceSearch.salesforce; this.workplaceSearchSecurity = docLinks.links.workplaceSearch.security; @@ -255,7 +273,9 @@ class DocLinks { this.workplaceSearchSharePointServer = docLinks.links.workplaceSearch.sharePointServer; this.workplaceSearchSlack = docLinks.links.workplaceSearch.slack; this.workplaceSearchSynch = docLinks.links.workplaceSearch.synch; + this.workplaceSearchTeams = docLinks.links.workplaceSearch.teams; this.workplaceSearchZendesk = docLinks.links.workplaceSearch.zendesk; + this.workplaceSearchZoom = docLinks.links.workplaceSearch.zoom; } } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index db3da678e1e00..181cd8b7c9a73 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -25,7 +25,7 @@ export const staticGenericExternalSourceData: SourceDataItem = { isPublicKey: false, hasOauthRedirect: false, needsBaseUrl: false, - documentationUrl: docLinks.workplaceSearchCustomSources, // TODO Update this when we have a doclink + documentationUrl: docLinks.workplaceSearchCustomConnectorPackage, applicationPortalUrl: '', }, objTypes: [], @@ -107,7 +107,7 @@ export const staticSourceData: SourceDataItem[] = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: true, - documentationUrl: docLinks.workplaceSearchConfluenceCloud, // TODO Update this when we have a doclink + documentationUrl: docLinks.workplaceSearchConfluenceCloudConnectorPackage, applicationPortalUrl: 'https://developer.atlassian.com/console/myapps/', }, objTypes: [ @@ -387,7 +387,7 @@ export const staticSourceData: SourceDataItem[] = [ isPublicKey: false, hasOauthRedirect: false, needsBaseUrl: false, - documentationUrl: docLinks.workplaceSearchCustomSources, // TODO Update this when we have a doclink + documentationUrl: docLinks.workplaceSearchNetworkDrive, applicationPortalUrl: '', githubRepository: 'elastic/enterprise-search-network-drive-connector', }, @@ -433,7 +433,7 @@ export const staticSourceData: SourceDataItem[] = [ isPublicKey: false, hasOauthRedirect: false, needsBaseUrl: false, - documentationUrl: docLinks.workplaceSearchCustomSources, // TODO Update this when we have a doclink + documentationUrl: docLinks.workplaceSearchOutlook, applicationPortalUrl: '', githubRepository: 'elastic/enterprise-search-outlook-connector', }, @@ -649,7 +649,7 @@ export const staticSourceData: SourceDataItem[] = [ isPublicKey: false, hasOauthRedirect: false, needsBaseUrl: false, - documentationUrl: docLinks.workplaceSearchCustomSources, // TODO Update this when we have a doclink + documentationUrl: docLinks.workplaceSearchTeams, applicationPortalUrl: '', githubRepository: 'elastic/enterprise-search-teams-connector', }, @@ -691,7 +691,7 @@ export const staticSourceData: SourceDataItem[] = [ isPublicKey: false, hasOauthRedirect: false, needsBaseUrl: false, - documentationUrl: docLinks.workplaceSearchCustomSources, // TODO Update this when we have a doclink + documentationUrl: docLinks.workplaceSearchZoom, applicationPortalUrl: '', githubRepository: 'elastic/enterprise-search-zoom-connector', }, From 065ea3e772f50cd0e5357ba98ba5bb04ccd4323f Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Fri, 20 May 2022 13:12:49 -0400 Subject: [PATCH 050/120] [Workplace Search] Remove Custom API Source Integration tile (#132538) --- .../apis/custom_integration/integrations.ts | 2 +- .../assets/source_icons/custom_api_source.svg | 1 - .../enterprise_search/server/integrations.ts | 18 ------------------ .../translations/translations/fr-FR.json | 2 -- .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 6 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/assets/source_icons/custom_api_source.svg diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts index c1b6518f6684a..c4fda918328f8 100644 --- a/test/api_integration/apis/custom_integration/integrations.ts +++ b/test/api_integration/apis/custom_integration/integrations.ts @@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.be.an('array'); - expect(resp.body.length).to.be(43); + expect(resp.body.length).to.be(42); // Test for sample data card expect(resp.body.findIndex((c: { id: string }) => c.id === 'sample_data_all')).to.be.above( diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/custom_api_source.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/custom_api_source.svg deleted file mode 100644 index cc07fbbc50877..0000000000000 --- a/x-pack/plugins/enterprise_search/public/assets/source_icons/custom_api_source.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts index d2d3b5d4d6829..140e36ba15555 100644 --- a/x-pack/plugins/enterprise_search/server/integrations.ts +++ b/x-pack/plugins/enterprise_search/server/integrations.ts @@ -338,24 +338,6 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ categories: ['enterprise_search', 'communications', 'productivity'], uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/zoom', }, - { - id: 'custom_api_source', - title: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceName', - { - defaultMessage: 'Custom API Source', - } - ), - description: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceDescription', - { - defaultMessage: - 'Search over anything by building your own integration with Workplace Search.', - } - ), - categories: ['custom'], - uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/custom', - }, ]; export const registerEnterpriseSearchIntegrations = ( diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 85ea8a0ffc348..b62a957cfa927 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -11801,8 +11801,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.confluenceCloudName": "Cloud Confluence", "xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerDescription": "Effectuez des recherches sur le contenu de votre organisation sur le serveur Confluence avec Workplace Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerName": "Serveur Confluence", - "xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceDescription": "Effectuez n'importe quelle recherche en créant votre propre intégration avec Workplace Search.", - "xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceName": "Source d'API personnalisée", "xpack.enterpriseSearch.workplaceSearch.integrations.dropboxDescription": "Effectuez des recherches dans vos fichiers et dossiers stockés sur Dropbox avec Workplace Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.dropboxName": "Dropbox", "xpack.enterpriseSearch.workplaceSearch.integrations.githubDescription": "Effectuez des recherches sur vos projets et référentiels sur GitHub avec Workplace Search.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cf84dbd2d6305..fe7056a5e3ec1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11900,8 +11900,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.confluenceCloudName": "Confluence Cloud", "xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerDescription": "Workplace Searchを使用して、Confluence Serverの組織コンテンツを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerName": "Confluence Server", - "xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceDescription": "Workplace Searchを使用して、独自の統合を構築し、項目を検索します。", - "xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceName": "カスタムAPIソース", "xpack.enterpriseSearch.workplaceSearch.integrations.dropboxDescription": "Workplace Searchを使用して、Dropboxに保存されたファイルとフォルダーを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.dropboxName": "Dropbox", "xpack.enterpriseSearch.workplaceSearch.integrations.githubDescription": "Workplace Searchを使用して、GitHubのプロジェクトとリポジトリを検索します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b15cacd8dc8ab..990a113fcd9d6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11922,8 +11922,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.confluenceCloudName": "Confluence Cloud", "xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerDescription": "通过 Workplace Search 搜索 Confluence Server 上的组织内容。", "xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerName": "Confluence Server", - "xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceDescription": "通过使用 Workplace Search 构建自己的集成来搜索任何内容。", - "xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceName": "定制 API 源", "xpack.enterpriseSearch.workplaceSearch.integrations.dropboxDescription": "通过 Workplace Search 搜索存储在 Dropbox 上的文件和文件夹。", "xpack.enterpriseSearch.workplaceSearch.integrations.dropboxName": "Dropbox", "xpack.enterpriseSearch.workplaceSearch.integrations.githubDescription": "通过 Workplace Search 搜索 GitHub 上的项目和存储库。", From ecca23166e0a619c4a529d463aefecf31da39830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Fri, 20 May 2022 19:37:03 +0200 Subject: [PATCH 051/120] [Stack Monitoring] Convert setup routes to TypeScript (#131265) --- x-pack/plugins/infra/server/mocks.ts | 34 ++++++ .../log_views/log_views_service.mock.ts | 6 +- .../http_api/cluster/index.ts} | 5 +- .../common/http_api/cluster/post_cluster.ts | 29 +++++ .../common/http_api/cluster/post_clusters.ts | 20 ++++ .../monitoring/common/http_api/setup/index.ts | 10 ++ .../setup/post_cluster_setup_status.ts | 44 +++++++ .../setup/post_disable_internal_collection.ts | 14 +++ .../http_api/setup/post_node_setup_status.ts | 43 +++++++ .../http_api/shared/literal_value.test.ts | 30 +++++ .../shared/query_string_boolean.test.ts | 23 ++++ .../plugins/monitoring/server/debug_logger.ts | 19 +-- .../lib/cluster/flag_supported_clusters.ts | 18 ++- .../server/lib/cluster/get_index_patterns.ts | 6 +- .../elasticsearch/verify_monitoring_auth.ts | 4 +- ....test.js => get_collection_status.test.ts} | 108 ++++++++++++------ .../setup/collection/get_collection_status.ts | 23 ++-- x-pack/plugins/monitoring/server/mocks.ts | 25 ++++ .../server/routes/api/v1/alerts/enable.ts | 8 +- .../server/routes/api/v1/alerts/index.ts | 10 +- .../server/routes/api/v1/alerts/status.ts | 7 +- .../server/routes/api/v1/apm/index.ts | 13 ++- .../server/routes/api/v1/beats/index.ts | 13 ++- .../api/v1/check_access/check_access.ts | 9 +- .../routes/api/v1/check_access/index.ts | 7 +- .../server/routes/api/v1/cluster/cluster.ts | 46 ++++---- .../server/routes/api/v1/cluster/clusters.ts | 40 +++---- .../server/routes/api/v1/cluster/index.ts | 10 +- .../routes/api/v1/elasticsearch/index.ts | 28 +++-- .../check/internal_monitoring.ts | 4 +- .../api/v1/elasticsearch_settings/index.ts | 22 +++- .../monitoring/server/routes/api/v1/index.ts | 16 +++ .../server/routes/api/v1/logstash/index.ts | 25 ++-- .../api/v1/setup/cluster_setup_status.js | 72 ------------ .../api/v1/setup/cluster_setup_status.ts | 62 ++++++++++ ...able_elasticsearch_internal_collection.ts} | 18 ++- .../server/routes/api/v1/setup/index.ts | 17 +++ .../routes/api/v1/setup/node_setup_status.js | 74 ------------ .../routes/api/v1/setup/node_setup_status.ts | 64 +++++++++++ .../monitoring/server/routes/api/v1/ui.js | 42 ------- .../monitoring/server/routes/api/v1/ui.ts | 14 +++ .../plugins/monitoring/server/routes/index.ts | 35 ++++-- x-pack/plugins/monitoring/server/types.ts | 3 +- 43 files changed, 753 insertions(+), 367 deletions(-) create mode 100644 x-pack/plugins/infra/server/mocks.ts rename x-pack/plugins/monitoring/{server/routes/api/v1/setup/index.js => common/http_api/cluster/index.ts} (52%) create mode 100644 x-pack/plugins/monitoring/common/http_api/cluster/post_cluster.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/cluster/post_clusters.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/setup/index.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/setup/post_cluster_setup_status.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/setup/post_disable_internal_collection.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/setup/post_node_setup_status.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/shared/literal_value.test.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/shared/query_string_boolean.test.ts rename x-pack/plugins/monitoring/server/lib/setup/collection/{get_collection_status.test.js => get_collection_status.test.ts} (79%) create mode 100644 x-pack/plugins/monitoring/server/mocks.ts create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/index.ts delete mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.js create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.ts rename x-pack/plugins/monitoring/server/routes/api/v1/setup/{disable_elasticsearch_internal_collection.js => disable_elasticsearch_internal_collection.ts} (74%) create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/setup/index.ts delete mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.js create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.ts delete mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/ui.js create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/ui.ts diff --git a/x-pack/plugins/infra/server/mocks.ts b/x-pack/plugins/infra/server/mocks.ts new file mode 100644 index 0000000000000..5b587a1fe80d5 --- /dev/null +++ b/x-pack/plugins/infra/server/mocks.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + createLogViewsServiceSetupMock, + createLogViewsServiceStartMock, +} from './services/log_views/log_views_service.mock'; +import { InfraPluginSetup, InfraPluginStart } from './types'; + +const createInfraSetupMock = () => { + const infraSetupMock: jest.Mocked = { + defineInternalSourceConfiguration: jest.fn(), + logViews: createLogViewsServiceSetupMock(), + }; + + return infraSetupMock; +}; + +const createInfraStartMock = () => { + const infraStartMock: jest.Mocked = { + getMetricIndices: jest.fn(), + logViews: createLogViewsServiceStartMock(), + }; + return infraStartMock; +}; + +export const infraPluginMock = { + createSetupContract: createInfraSetupMock, + createStartContract: createInfraStartMock, +}; diff --git a/x-pack/plugins/infra/server/services/log_views/log_views_service.mock.ts b/x-pack/plugins/infra/server/services/log_views/log_views_service.mock.ts index becd5a015b2ec..e472e30fae2b4 100644 --- a/x-pack/plugins/infra/server/services/log_views/log_views_service.mock.ts +++ b/x-pack/plugins/infra/server/services/log_views/log_views_service.mock.ts @@ -6,7 +6,11 @@ */ import { createLogViewsClientMock } from './log_views_client.mock'; -import { LogViewsServiceStart } from './types'; +import { LogViewsServiceSetup, LogViewsServiceStart } from './types'; + +export const createLogViewsServiceSetupMock = (): jest.Mocked => ({ + defineInternalLogView: jest.fn(), +}); export const createLogViewsServiceStartMock = (): jest.Mocked => ({ getClient: jest.fn((_savedObjectsClient: any, _elasticsearchClient: any) => diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/index.js b/x-pack/plugins/monitoring/common/http_api/cluster/index.ts similarity index 52% rename from x-pack/plugins/monitoring/server/routes/api/v1/setup/index.js rename to x-pack/plugins/monitoring/common/http_api/cluster/index.ts index f450fc906d076..af53ade67f610 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/setup/index.js +++ b/x-pack/plugins/monitoring/common/http_api/cluster/index.ts @@ -5,6 +5,5 @@ * 2.0. */ -export { clusterSetupStatusRoute } from './cluster_setup_status'; -export { nodeSetupStatusRoute } from './node_setup_status'; -export { disableElasticsearchInternalCollectionRoute } from './disable_elasticsearch_internal_collection'; +export * from './post_cluster'; +export * from './post_clusters'; diff --git a/x-pack/plugins/monitoring/common/http_api/cluster/post_cluster.ts b/x-pack/plugins/monitoring/common/http_api/cluster/post_cluster.ts new file mode 100644 index 0000000000000..faa26989fec37 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/cluster/post_cluster.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { ccsRT, clusterUuidRT, timeRangeRT } from '../shared'; + +export const postClusterRequestParamsRT = rt.type({ + clusterUuid: clusterUuidRT, +}); + +export const postClusterRequestPayloadRT = rt.intersection([ + rt.partial({ + ccs: ccsRT, + }), + rt.type({ + timeRange: timeRangeRT, + codePaths: rt.array(rt.string), + }), +]); + +export type PostClusterRequestPayload = rt.TypeOf; + +export const postClusterResponsePayloadRT = rt.type({ + // TODO: add payload entries +}); diff --git a/x-pack/plugins/monitoring/common/http_api/cluster/post_clusters.ts b/x-pack/plugins/monitoring/common/http_api/cluster/post_clusters.ts new file mode 100644 index 0000000000000..ad3214c354bc5 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/cluster/post_clusters.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { timeRangeRT } from '../shared'; + +export const postClustersRequestPayloadRT = rt.type({ + timeRange: timeRangeRT, + codePaths: rt.array(rt.string), +}); + +export type PostClustersRequestPayload = rt.TypeOf; + +export const postClustersResponsePayloadRT = rt.type({ + // TODO: add payload entries +}); diff --git a/x-pack/plugins/monitoring/common/http_api/setup/index.ts b/x-pack/plugins/monitoring/common/http_api/setup/index.ts new file mode 100644 index 0000000000000..33cce5833c3c5 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/setup/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './post_cluster_setup_status'; +export * from './post_node_setup_status'; +export * from './post_disable_internal_collection'; diff --git a/x-pack/plugins/monitoring/common/http_api/setup/post_cluster_setup_status.ts b/x-pack/plugins/monitoring/common/http_api/setup/post_cluster_setup_status.ts new file mode 100644 index 0000000000000..2c4f1293fb89e --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/setup/post_cluster_setup_status.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { + booleanFromStringRT, + ccsRT, + clusterUuidRT, + createLiteralValueFromUndefinedRT, + timeRangeRT, +} from '../shared'; + +export const postClusterSetupStatusRequestParamsRT = rt.partial({ + clusterUuid: clusterUuidRT, +}); + +export const postClusterSetupStatusRequestQueryRT = rt.partial({ + // This flag is not intended to be used in production. It was introduced + // as a way to ensure consistent API testing - the typical data source + // for API tests are archived data, where the cluster configuration and data + // are consistent from environment to environment. However, this endpoint + // also attempts to retrieve data from the running stack products (ES and Kibana) + // which will vary from environment to environment making it difficult + // to write tests against. Therefore, this flag exists and should only be used + // in our testing environment. + skipLiveData: rt.union([booleanFromStringRT, createLiteralValueFromUndefinedRT(false)]), +}); + +export const postClusterSetupStatusRequestPayloadRT = rt.partial({ + ccs: ccsRT, + timeRange: timeRangeRT, +}); + +export type PostClusterSetupStatusRequestPayload = rt.TypeOf< + typeof postClusterSetupStatusRequestPayloadRT +>; + +export const postClusterSetupStatusResponsePayloadRT = rt.type({ + // TODO: add payload entries +}); diff --git a/x-pack/plugins/monitoring/common/http_api/setup/post_disable_internal_collection.ts b/x-pack/plugins/monitoring/common/http_api/setup/post_disable_internal_collection.ts new file mode 100644 index 0000000000000..d44794d7e1829 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/setup/post_disable_internal_collection.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { clusterUuidRT } from '../shared'; + +export const postDisableInternalCollectionRequestParamsRT = rt.partial({ + // the cluster uuid seems to be required but never used + clusterUuid: clusterUuidRT, +}); diff --git a/x-pack/plugins/monitoring/common/http_api/setup/post_node_setup_status.ts b/x-pack/plugins/monitoring/common/http_api/setup/post_node_setup_status.ts new file mode 100644 index 0000000000000..1d51d36ae4477 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/setup/post_node_setup_status.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { + booleanFromStringRT, + ccsRT, + createLiteralValueFromUndefinedRT, + timeRangeRT, +} from '../shared'; + +export const postNodeSetupStatusRequestParamsRT = rt.type({ + nodeUuid: rt.string, +}); + +export const postNodeSetupStatusRequestQueryRT = rt.partial({ + // This flag is not intended to be used in production. It was introduced + // as a way to ensure consistent API testing - the typical data source + // for API tests are archived data, where the cluster configuration and data + // are consistent from environment to environment. However, this endpoint + // also attempts to retrieve data from the running stack products (ES and Kibana) + // which will vary from environment to environment making it difficult + // to write tests against. Therefore, this flag exists and should only be used + // in our testing environment. + skipLiveData: rt.union([booleanFromStringRT, createLiteralValueFromUndefinedRT(false)]), +}); + +export const postNodeSetupStatusRequestPayloadRT = rt.partial({ + ccs: ccsRT, + timeRange: timeRangeRT, +}); + +export type PostNodeSetupStatusRequestPayload = rt.TypeOf< + typeof postNodeSetupStatusRequestPayloadRT +>; + +export const postNodeSetupStatusResponsePayloadRT = rt.type({ + // TODO: add payload entries +}); diff --git a/x-pack/plugins/monitoring/common/http_api/shared/literal_value.test.ts b/x-pack/plugins/monitoring/common/http_api/shared/literal_value.test.ts new file mode 100644 index 0000000000000..3d70e86620602 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/shared/literal_value.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { either } from 'fp-ts'; +import * as rt from 'io-ts'; +import { createLiteralValueFromUndefinedRT } from './literal_value'; + +describe('LiteralValueFromUndefined runtime type', () => { + it('decodes undefined to a given literal value', () => { + expect(createLiteralValueFromUndefinedRT('SOME_VALUE').decode(undefined)).toEqual( + either.right('SOME_VALUE') + ); + }); + + it('can be used to define default values when decoding', () => { + expect( + rt.union([rt.boolean, createLiteralValueFromUndefinedRT(true)]).decode(undefined) + ).toEqual(either.right(true)); + }); + + it('rejects other values', () => { + expect( + either.isLeft(createLiteralValueFromUndefinedRT('SOME_VALUE').decode('DEFINED')) + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/monitoring/common/http_api/shared/query_string_boolean.test.ts b/x-pack/plugins/monitoring/common/http_api/shared/query_string_boolean.test.ts new file mode 100644 index 0000000000000..1801c6746feb2 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/shared/query_string_boolean.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { either } from 'fp-ts'; +import { booleanFromStringRT } from './query_string_boolean'; + +describe('BooleanFromString runtime type', () => { + it('decodes string "true" to a boolean', () => { + expect(booleanFromStringRT.decode('true')).toEqual(either.right(true)); + }); + + it('decodes string "false" to a boolean', () => { + expect(booleanFromStringRT.decode('false')).toEqual(either.right(false)); + }); + + it('rejects other strings', () => { + expect(either.isLeft(booleanFromStringRT.decode('maybe'))).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/monitoring/server/debug_logger.ts b/x-pack/plugins/monitoring/server/debug_logger.ts index 0add1f12f0304..cce00f834cbb2 100644 --- a/x-pack/plugins/monitoring/server/debug_logger.ts +++ b/x-pack/plugins/monitoring/server/debug_logger.ts @@ -4,18 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { RouteMethod } from '@kbn/core/server'; import fs from 'fs'; import { MonitoringConfig } from './config'; -import { RouteDependencies } from './types'; +import { LegacyRequest, MonitoringCore, MonitoringRouteConfig, RouteDependencies } from './types'; export function decorateDebugServer( - _server: any, + server: MonitoringCore, config: MonitoringConfig, logger: RouteDependencies['logger'] -) { +): MonitoringCore { // bail if the proper config value is not set (extra protection) if (!config.ui.debug_mode) { - return _server; + return server; } // create a debug logger that will either write to file (if debug_log_path exists) or log out via logger @@ -23,14 +24,16 @@ export function decorateDebugServer( return { // maintain the rest of _server untouched - ..._server, + ...server, // TODO: replace any - route: (options: any) => { + route: ( + options: MonitoringRouteConfig + ) => { const apiPath = options.path; - return _server.route({ + return server.route({ ...options, // TODO: replace any - handler: async (req: any) => { + handler: async (req: LegacyRequest): Promise => { const { elasticsearch: cached } = req.server.plugins; const apiRequestHeaders = req.headers; req.server.plugins.elasticsearch = { diff --git a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts index 80d17a8ad0627..f93c3f8ad7590 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts @@ -6,13 +6,18 @@ */ import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; +import { TimeRange } from '../../../common/http_api/shared'; import { ElasticsearchResponse } from '../../../common/types/es'; -import { LegacyRequest, Cluster } from '../../types'; -import { getNewIndexPatterns } from './get_index_patterns'; import { Globals } from '../../static_globals'; +import { Cluster, LegacyRequest } from '../../types'; +import { getNewIndexPatterns } from './get_index_patterns'; + +export interface FindSupportClusterRequestPayload { + timeRange: TimeRange; +} async function findSupportedBasicLicenseCluster( - req: LegacyRequest, + req: LegacyRequest, clusters: Cluster[], ccs: string, kibanaUuid: string, @@ -53,7 +58,7 @@ async function findSupportedBasicLicenseCluster( }, }, { term: { 'kibana_stats.kibana.uuid': kibanaUuid } }, - { range: { timestamp: { gte, lte, format: 'strict_date_optional_time' } } }, + { range: { timestamp: { gte, lte, format: 'epoch_millis' } } }, ], }, }, @@ -86,7 +91,10 @@ async function findSupportedBasicLicenseCluster( * Non-Basic license clusters and any cluster in a single-cluster environment * are also flagged as supported in this method. */ -export function flagSupportedClusters(req: LegacyRequest, ccs: string) { +export function flagSupportedClusters( + req: LegacyRequest, + ccs: string +) { const serverLog = (message: string) => req.getLogger('supported-clusters').debug(message); const flagAllSupported = (clusters: Cluster[]) => { clusters.forEach((cluster) => { diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts index 7d470857dfe5a..2ebf4fe6b480e 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { LegacyServer } from '../../types'; import { prefixIndexPatternWithCcs } from '../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH, @@ -20,14 +19,13 @@ import { INDEX_PATTERN_ENTERPRISE_SEARCH, CCS_REMOTE_PATTERN, } from '../../../common/constants'; -import { MonitoringConfig } from '../..'; +import { MonitoringConfig } from '../../config'; export function getIndexPatterns( - server: LegacyServer, + config: MonitoringConfig, additionalPatterns: Record = {}, ccs: string = CCS_REMOTE_PATTERN ) { - const config = server.config; const esIndexPattern = prefixIndexPatternWithCcs(config, INDEX_PATTERN_ELASTICSEARCH, ccs); const kbnIndexPattern = prefixIndexPatternWithCcs(config, INDEX_PATTERN_KIBANA, ccs); const lsIndexPattern = prefixIndexPatternWithCcs(config, INDEX_PATTERN_LOGSTASH, ccs); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts index 3bd9f6d2265dc..a5ee876012c1d 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts @@ -19,7 +19,7 @@ import { LegacyRequest } from '../../types'; */ // TODO: replace LegacyRequest with current request object + plugin retrieval -export async function verifyMonitoringAuth(req: LegacyRequest) { +export async function verifyMonitoringAuth(req: LegacyRequest) { const xpackInfo = get(req.server.plugins.monitoring, 'info'); if (xpackInfo) { @@ -42,7 +42,7 @@ export async function verifyMonitoringAuth(req: LegacyRequest) { */ // TODO: replace LegacyRequest with current request object + plugin retrieval -async function verifyHasPrivileges(req: LegacyRequest) { +async function verifyHasPrivileges(req: LegacyRequest): Promise { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); let response; diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.ts similarity index 79% rename from x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js rename to x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.ts index 214e8d5907443..ed92948be8e3b 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.ts @@ -5,49 +5,50 @@ * 2.0. */ -import { getCollectionStatus } from '.'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; +import { infraPluginMock } from '@kbn/infra-plugin/server/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; +import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks'; +import { configSchema, createConfig } from '../../../config'; +import { monitoringPluginMock } from '../../../mocks'; +import { LegacyRequest } from '../../../types'; import { getIndexPatterns } from '../../cluster/get_index_patterns'; +import { getCollectionStatus } from './get_collection_status'; const liveClusterUuid = 'a12'; const mockReq = ( - searchResult = {}, - securityEnabled = true, - userHasPermissions = true, - securityErrorMessage = null -) => { + searchResult: object = {}, + securityEnabled: boolean = true, + userHasPermissions: boolean = true, + securityErrorMessage: string | null = null +): LegacyRequest => { + const usageCollectionSetup = usageCollectionPluginMock.createSetupContract(); + const licenseService = monitoringPluginMock.createLicenseServiceMock(); + licenseService.getSecurityFeature.mockReturnValue({ + isAvailable: securityEnabled, + isEnabled: securityEnabled, + }); + const logger = loggerMock.create(); + return { server: { instanceUuid: 'kibana-1234', newPlatform: { setup: { plugins: { - usageCollection: { - getCollectorByType: () => ({ - isReady: () => false, - }), - }, + usageCollection: usageCollectionSetup, + features: featuresPluginMock.createSetup(), + infra: infraPluginMock.createSetupContract(), }, }, }, - config: { ui: { ccs: { enabled: false } } }, - usage: { - collectorSet: { - getCollectorByType: () => ({ - isReady: () => false, - }), - }, - }, + config: createConfig(configSchema.validate({ ui: { ccs: { enabled: false } } })), + log: logger, + route: jest.fn(), plugins: { monitoring: { info: { - getLicenseService: () => ({ - getSecurityFeature: () => { - return { - isAvailable: securityEnabled, - isEnabled: securityEnabled, - }; - }, - }), + getLicenseService: () => licenseService, }, }, elasticsearch: { @@ -86,6 +87,17 @@ const mockReq = ( }, }, }, + logger, + getLogger: () => logger, + params: {}, + payload: {}, + query: {}, + headers: {}, + getKibanaStatsCollector: () => null, + getUiSettingsService: () => null, + getActionTypeRegistry: () => null, + getRulesClient: () => null, + getActionsClient: () => null, }; }; @@ -124,7 +136,7 @@ describe('getCollectionStatus', () => { }, }); - const result = await getCollectionStatus(req, getIndexPatterns(req.server)); + const result = await getCollectionStatus(req, getIndexPatterns(req.server.config)); expect(result.kibana.totalUniqueInstanceCount).toBe(1); expect(result.kibana.totalUniqueFullyMigratedCount).toBe(0); @@ -173,7 +185,7 @@ describe('getCollectionStatus', () => { }, }); - const result = await getCollectionStatus(req, getIndexPatterns(req.server)); + const result = await getCollectionStatus(req, getIndexPatterns(req.server.config)); expect(result.kibana.totalUniqueInstanceCount).toBe(1); expect(result.kibana.totalUniqueFullyMigratedCount).toBe(1); @@ -229,7 +241,7 @@ describe('getCollectionStatus', () => { }, }); - const result = await getCollectionStatus(req, getIndexPatterns(req.server)); + const result = await getCollectionStatus(req, getIndexPatterns(req.server.config)); expect(result.kibana.totalUniqueInstanceCount).toBe(2); expect(result.kibana.totalUniqueFullyMigratedCount).toBe(1); @@ -251,7 +263,11 @@ describe('getCollectionStatus', () => { it('should detect products based on other indices', async () => { const req = mockReq({ hits: { total: { value: 1 } } }); - const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + const result = await getCollectionStatus( + req, + getIndexPatterns(req.server.config), + liveClusterUuid + ); expect(result.kibana.detected.doesExist).toBe(true); expect(result.elasticsearch.detected.doesExist).toBe(true); @@ -261,13 +277,21 @@ describe('getCollectionStatus', () => { it('should work properly when security is disabled', async () => { const req = mockReq({ hits: { total: { value: 1 } } }, false); - const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + const result = await getCollectionStatus( + req, + getIndexPatterns(req.server.config), + liveClusterUuid + ); expect(result.kibana.detected.doesExist).toBe(true); }); it('should work properly with an unknown security message', async () => { const req = mockReq({ hits: { total: { value: 1 } } }, true, true, 'foobar'); - const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + const result = await getCollectionStatus( + req, + getIndexPatterns(req.server.config), + liveClusterUuid + ); expect(result._meta.hasPermissions).toBe(false); }); @@ -278,7 +302,11 @@ describe('getCollectionStatus', () => { true, 'no handler found for uri [/_security/user/_has_privileges] and method [POST]' ); - const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + const result = await getCollectionStatus( + req, + getIndexPatterns(req.server.config), + liveClusterUuid + ); expect(result.kibana.detected.doesExist).toBe(true); }); @@ -289,13 +317,21 @@ describe('getCollectionStatus', () => { true, 'Invalid index name [_security]' ); - const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + const result = await getCollectionStatus( + req, + getIndexPatterns(req.server.config), + liveClusterUuid + ); expect(result.kibana.detected.doesExist).toBe(true); }); it('should not work if the user does not have the necessary permissions', async () => { const req = mockReq({ hits: { total: { value: 1 } } }, true, false); - const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + const result = await getCollectionStatus( + req, + getIndexPatterns(req.server.config), + liveClusterUuid + ); expect(result._meta.hasPermissions).toBe(false); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.ts b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.ts index b06b74fd255f4..568b8bbaef567 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.ts +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.ts @@ -5,17 +5,18 @@ * 2.0. */ -import { get, uniq } from 'lodash'; import { CollectorFetchContext, UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; +import { get, uniq } from 'lodash'; import { - METRICBEAT_INDEX_NAME_UNIQUE_TOKEN, - ELASTICSEARCH_SYSTEM_ID, APM_SYSTEM_ID, - KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID, - LOGSTASH_SYSTEM_ID, + ELASTICSEARCH_SYSTEM_ID, KIBANA_STATS_TYPE_MONITORING, + KIBANA_SYSTEM_ID, + LOGSTASH_SYSTEM_ID, + METRICBEAT_INDEX_NAME_UNIQUE_TOKEN, } from '../../../../common/constants'; +import { TimeRange } from '../../../../common/http_api/shared'; import { LegacyRequest } from '../../../types'; import { getLivesNodes } from '../../elasticsearch/nodes/get_nodes/get_live_nodes'; @@ -31,7 +32,7 @@ interface Bucket { const NUMBER_OF_SECONDS_AGO_TO_LOOK = 30; const getRecentMonitoringDocuments = async ( - req: LegacyRequest, + req: LegacyRequest, indexPatterns: Record, clusterUuid?: string, nodeUuid?: string, @@ -300,7 +301,7 @@ function isBeatFromAPM(bucket: Bucket) { return get(beatType, 'buckets[0].key') === 'apm-server'; } -async function hasNecessaryPermissions(req: LegacyRequest) { +async function hasNecessaryPermissions(req: LegacyRequest) { const licenseService = await req.server.plugins.monitoring.info.getLicenseService(); const securityFeature = licenseService.getSecurityFeature(); if (!securityFeature.isAvailable || !securityFeature.isEnabled) { @@ -366,7 +367,7 @@ async function getLiveKibanaInstance(usageCollection?: UsageCollectionSetup) { ); } -async function getLiveElasticsearchClusterUuid(req: LegacyRequest) { +async function getLiveElasticsearchClusterUuid(req: LegacyRequest) { const params = { path: '/_cluster/state/cluster_uuid', method: 'GET', @@ -377,7 +378,9 @@ async function getLiveElasticsearchClusterUuid(req: LegacyRequest) { return clusterUuid; } -async function getLiveElasticsearchCollectionEnabled(req: LegacyRequest) { +async function getLiveElasticsearchCollectionEnabled( + req: LegacyRequest +) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); const response = await callWithRequest(req, 'transport.request', { method: 'GET', @@ -425,7 +428,7 @@ async function getLiveElasticsearchCollectionEnabled(req: LegacyRequest) { * @param {*} skipLiveData Optional and will not make any live api calls if set to true */ export const getCollectionStatus = async ( - req: LegacyRequest, + req: LegacyRequest, indexPatterns: Record, clusterUuid?: string, nodeUuid?: string, diff --git a/x-pack/plugins/monitoring/server/mocks.ts b/x-pack/plugins/monitoring/server/mocks.ts new file mode 100644 index 0000000000000..5adeae22acfc0 --- /dev/null +++ b/x-pack/plugins/monitoring/server/mocks.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ILicense } from '@kbn/licensing-plugin/server'; +import { Subject } from 'rxjs'; +import { MonitoringLicenseService } from './types'; + +const createLicenseServiceMock = (): jest.Mocked => ({ + refresh: jest.fn(), + license$: new Subject(), + getMessage: jest.fn(), + getWatcherFeature: jest.fn(), + getMonitoringFeature: jest.fn(), + getSecurityFeature: jest.fn(), + stop: jest.fn(), +}); + +// this might be incomplete and is added to as needed +export const monitoringPluginMock = { + createLicenseServiceMock, +}; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index 9188215137565..b773e25b81152 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -8,15 +8,15 @@ // @ts-ignore import { ActionResult } from '@kbn/actions-plugin/common'; import { RuleTypeParams, SanitizedRule } from '@kbn/alerting-plugin/common'; -import { handleError } from '../../../../lib/errors'; -import { AlertsFactory } from '../../../../alerts'; -import { LegacyServer, RouteDependencies } from '../../../../types'; import { ALERT_ACTION_TYPE_LOG } from '../../../../../common/constants'; +import { AlertsFactory } from '../../../../alerts'; import { disableWatcherClusterAlerts } from '../../../../lib/alerts/disable_watcher_cluster_alerts'; +import { handleError } from '../../../../lib/errors'; +import { MonitoringCore, RouteDependencies } from '../../../../types'; const DEFAULT_SERVER_LOG_NAME = 'Monitoring: Write to Kibana log'; -export function enableAlertsRoute(server: LegacyServer, npRoute: RouteDependencies) { +export function enableAlertsRoute(server: MonitoringCore, npRoute: RouteDependencies) { npRoute.router.post( { path: '/api/monitoring/v1/alerts/enable', diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.ts index 11782c73d9b55..c2511e1d24c0a 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.ts @@ -5,5 +5,11 @@ * 2.0. */ -export { enableAlertsRoute } from './enable'; -export { alertStatusRoute } from './status'; +import { MonitoringCore, RouteDependencies } from '../../../../types'; +import { enableAlertsRoute } from './enable'; +import { alertStatusRoute } from './status'; + +export function registerV1AlertRoutes(server: MonitoringCore, npRoute: RouteDependencies) { + alertStatusRoute(npRoute); + enableAlertsRoute(server, npRoute); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts index a145d92921634..a9efc14c8c458 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts @@ -6,13 +6,12 @@ */ import { schema } from '@kbn/config-schema'; -// @ts-ignore +import { CommonAlertFilter } from '../../../../../common/types/alerts'; +import { fetchStatus } from '../../../../lib/alerts/fetch_status'; import { handleError } from '../../../../lib/errors'; import { RouteDependencies } from '../../../../types'; -import { fetchStatus } from '../../../../lib/alerts/fetch_status'; -import { CommonAlertFilter } from '../../../../../common/types/alerts'; -export function alertStatusRoute(server: any, npRoute: RouteDependencies) { +export function alertStatusRoute(npRoute: RouteDependencies) { npRoute.router.post( { path: '/api/monitoring/v1/alert/{clusterUuid}/status', diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/apm/index.ts index 0fb4dd78c9be6..97d9a2f9789d7 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/index.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/index.ts @@ -5,6 +5,13 @@ * 2.0. */ -export { apmInstanceRoute } from './instance'; -export { apmInstancesRoute } from './instances'; -export { apmOverviewRoute } from './overview'; +import { MonitoringCore } from '../../../../types'; +import { apmInstanceRoute } from './instance'; +import { apmInstancesRoute } from './instances'; +import { apmOverviewRoute } from './overview'; + +export function registerV1ApmRoutes(server: MonitoringCore) { + apmInstanceRoute(server); + apmInstancesRoute(server); + apmOverviewRoute(server); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/beats/index.ts index 57423052760bf..935ca35c3a384 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/index.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/index.ts @@ -5,6 +5,13 @@ * 2.0. */ -export { beatsOverviewRoute } from './overview'; -export { beatsListingRoute } from './beats'; -export { beatsDetailRoute } from './beat_detail'; +import { MonitoringCore } from '../../../../types'; +import { beatsListingRoute } from './beats'; +import { beatsDetailRoute } from './beat_detail'; +import { beatsOverviewRoute } from './overview'; + +export function registerV1BeatsRoutes(server: MonitoringCore) { + beatsDetailRoute(server); + beatsListingRoute(server); + beatsOverviewRoute(server); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.ts b/x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.ts index 450872049a3de..2db7481882b89 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.ts @@ -7,18 +7,19 @@ import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth'; import { handleError } from '../../../../lib/errors'; -import { LegacyRequest, LegacyServer } from '../../../../types'; +import { LegacyRequest, MonitoringCore } from '../../../../types'; /* * API for checking read privilege on Monitoring Data * Used for the "Access Denied" page as something to auto-retry with. */ -// TODO: Replace this LegacyServer call with the "new platform" core Kibana route method -export function checkAccessRoute(server: LegacyServer) { +// TODO: Replace this legacy route registration with the "new platform" core Kibana route method +export function checkAccessRoute(server: MonitoringCore) { server.route({ - method: 'GET', + method: 'get', path: '/api/monitoring/v1/check_access', + validate: {}, handler: async (req: LegacyRequest) => { const response: { has_access?: boolean } = {}; try { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/check_access/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/check_access/index.ts index 0fb8228f82442..5209ec8b92e9a 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/check_access/index.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/check_access/index.ts @@ -5,4 +5,9 @@ * 2.0. */ -export { checkAccessRoute } from './check_access'; +import { MonitoringCore } from '../../../../types'; +import { checkAccessRoute } from './check_access'; + +export function registerV1CheckAccessRoutes(server: MonitoringCore) { + checkAccessRoute(server); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.ts b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.ts index 30749f2e95c9f..6bd0a19d79c5f 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.ts @@ -5,39 +5,36 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { + postClusterRequestParamsRT, + postClusterRequestPayloadRT, + postClusterResponsePayloadRT, +} from '../../../../../common/http_api/cluster'; +import { createValidationFunction } from '../../../../lib/create_route_validation_function'; import { getClustersFromRequest } from '../../../../lib/cluster/get_clusters_from_request'; -// @ts-ignore -import { handleError } from '../../../../lib/errors'; import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; -import { LegacyRequest, LegacyServer } from '../../../../types'; +import { handleError } from '../../../../lib/errors'; +import { MonitoringCore } from '../../../../types'; -export function clusterRoute(server: LegacyServer) { +export function clusterRoute(server: MonitoringCore) { /* * Cluster Overview */ + + const validateParams = createValidationFunction(postClusterRequestParamsRT); + const validateBody = createValidationFunction(postClusterRequestPayloadRT); + server.route({ - method: 'POST', + method: 'post', path: '/api/monitoring/v1/clusters/{clusterUuid}', - config: { - validate: { - params: schema.object({ - clusterUuid: schema.string(), - }), - body: schema.object({ - ccs: schema.maybe(schema.string()), - timeRange: schema.object({ - min: schema.string(), - max: schema.string(), - }), - codePaths: schema.arrayOf(schema.string()), - }), - }, + validate: { + params: validateParams, + body: validateBody, }, - handler: async (req: LegacyRequest) => { + handler: async (req) => { const config = server.config; - const indexPatterns = getIndexPatterns(server, { + const indexPatterns = getIndexPatterns(config, { filebeatIndexPattern: config.ui.logs.index, }); const options = { @@ -47,13 +44,12 @@ export function clusterRoute(server: LegacyServer) { codePaths: req.payload.codePaths, }; - let clusters = []; try { - clusters = await getClustersFromRequest(req, indexPatterns, options); + const clusters = await getClustersFromRequest(req, indexPatterns, options); + return postClusterResponsePayloadRT.encode(clusters); } catch (err) { throw handleError(err, req); } - return clusters; }, }); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.ts b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.ts index 81acd0e53f319..9591dda205487 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.ts @@ -5,36 +5,33 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; -import { LegacyRequest, LegacyServer } from '../../../../types'; +import { + postClustersRequestPayloadRT, + postClustersResponsePayloadRT, +} from '../../../../../common/http_api/cluster'; import { getClustersFromRequest } from '../../../../lib/cluster/get_clusters_from_request'; +import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; +import { createValidationFunction } from '../../../../lib/create_route_validation_function'; import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth'; import { handleError } from '../../../../lib/errors'; -import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; +import { MonitoringCore } from '../../../../types'; -export function clustersRoute(server: LegacyServer) { +export function clustersRoute(server: MonitoringCore) { /* * Monitoring Home * Route Init (for checking license and compatibility for multi-cluster monitoring */ + const validateBody = createValidationFunction(postClustersRequestPayloadRT); + // TODO switch from the LegacyServer route() method to the "new platform" route methods server.route({ - method: 'POST', + method: 'post', path: '/api/monitoring/v1/clusters', - config: { - validate: { - body: schema.object({ - timeRange: schema.object({ - min: schema.string(), - max: schema.string(), - }), - codePaths: schema.arrayOf(schema.string()), - }), - }, + validate: { + body: validateBody, }, - handler: async (req: LegacyRequest) => { - let clusters = []; + handler: async (req) => { const config = server.config; // NOTE using try/catch because checkMonitoringAuth is expected to throw @@ -42,17 +39,16 @@ export function clustersRoute(server: LegacyServer) { // the monitoring data. `try/catch` makes it a little more explicit. try { await verifyMonitoringAuth(req); - const indexPatterns = getIndexPatterns(server, { + const indexPatterns = getIndexPatterns(config, { filebeatIndexPattern: config.ui.logs.index, }); - clusters = await getClustersFromRequest(req, indexPatterns, { - codePaths: req.payload.codePaths as string[], // TODO remove this cast when we can properly type req by using the right route handler + const clusters = await getClustersFromRequest(req, indexPatterns, { + codePaths: req.payload.codePaths, }); + return postClustersResponsePayloadRT.encode(clusters); } catch (err) { throw handleError(err, req); } - - return clusters; }, }); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/index.ts index 769f315480d9c..9534398db52c1 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/index.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/index.ts @@ -5,5 +5,11 @@ * 2.0. */ -export { clusterRoute } from './cluster'; -export { clustersRoute } from './clusters'; +import { clusterRoute } from './cluster'; +import { clustersRoute } from './clusters'; +import { MonitoringCore } from '../../../../types'; + +export function registerV1ClusterRoutes(server: MonitoringCore) { + clusterRoute(server); + clustersRoute(server); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index.ts index b2d432a5e35b5..e706dc61c0a41 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index.ts @@ -5,11 +5,23 @@ * 2.0. */ -export { esIndexRoute } from './index_detail'; -export { esIndicesRoute } from './indices'; -export { esNodeRoute } from './node_detail'; -export { esNodesRoute } from './nodes'; -export { esOverviewRoute } from './overview'; -export { mlJobRoute } from './ml_jobs'; -export { ccrRoute } from './ccr'; -export { ccrShardRoute } from './ccr_shard'; +import { MonitoringCore } from '../../../../types'; +import { ccrRoute } from './ccr'; +import { ccrShardRoute } from './ccr_shard'; +import { esIndexRoute } from './index_detail'; +import { esIndicesRoute } from './indices'; +import { mlJobRoute } from './ml_jobs'; +import { esNodesRoute } from './nodes'; +import { esNodeRoute } from './node_detail'; +import { esOverviewRoute } from './overview'; + +export function registerV1ElasticsearchRoutes(server: MonitoringCore) { + esIndexRoute(server); + esIndicesRoute(server); + esNodeRoute(server); + esNodesRoute(server); + esOverviewRoute(server); + mlJobRoute(server); + ccrRoute(server); + ccrShardRoute(server); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts index 11e0eec3f08f0..f8742144b28f8 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts @@ -19,7 +19,7 @@ import { } from '../../../../../../common/http_api/elasticsearch_settings'; import { createValidationFunction } from '../../../../../lib/create_route_validation_function'; import { handleError } from '../../../../../lib/errors'; -import { LegacyServer, RouteDependencies } from '../../../../../types'; +import { MonitoringCore, RouteDependencies } from '../../../../../types'; const queryBody = { size: 0, @@ -72,7 +72,7 @@ const checkLatestMonitoringIsLegacy = async (context: RequestHandlerContext, ind return counts; }; -export function internalMonitoringCheckRoute(server: LegacyServer, npRoute: RouteDependencies) { +export function internalMonitoringCheckRoute(server: MonitoringCore, npRoute: RouteDependencies) { const validateBody = createValidationFunction( postElasticsearchSettingsInternalMonitoringRequestPayloadRT ); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.ts index 61bb1ba804a5a..dfc68068bf80d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.ts @@ -5,8 +5,20 @@ * 2.0. */ -export { clusterSettingsCheckRoute } from './check/cluster'; -export { internalMonitoringCheckRoute } from './check/internal_monitoring'; -export { nodesSettingsCheckRoute } from './check/nodes'; -export { setCollectionEnabledRoute } from './set/collection_enabled'; -export { setCollectionIntervalRoute } from './set/collection_interval'; +import { MonitoringCore, RouteDependencies } from '../../../../types'; +import { clusterSettingsCheckRoute } from './check/cluster'; +import { internalMonitoringCheckRoute } from './check/internal_monitoring'; +import { nodesSettingsCheckRoute } from './check/nodes'; +import { setCollectionEnabledRoute } from './set/collection_enabled'; +import { setCollectionIntervalRoute } from './set/collection_interval'; + +export function registerV1ElasticsearchSettingsRoutes( + server: MonitoringCore, + npRoute: RouteDependencies +) { + clusterSettingsCheckRoute(server); + internalMonitoringCheckRoute(server, npRoute); + nodesSettingsCheckRoute(server); + setCollectionEnabledRoute(server); + setCollectionIntervalRoute(server); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/index.ts new file mode 100644 index 0000000000000..e0f5e55c6c128 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { registerV1AlertRoutes } from './alerts'; +export { registerV1ApmRoutes } from './apm'; +export { registerV1BeatsRoutes } from './beats'; +export { registerV1CheckAccessRoutes } from './check_access'; +export { registerV1ClusterRoutes } from './cluster'; +export { registerV1ElasticsearchRoutes } from './elasticsearch'; +export { registerV1ElasticsearchSettingsRoutes } from './elasticsearch_settings'; +export { registerV1LogstashRoutes } from './logstash'; +export { registerV1SetupRoutes } from './setup'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/index.ts index b267c17fc3346..a4975726cf0a1 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/index.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/index.ts @@ -5,10 +5,21 @@ * 2.0. */ -export { logstashNodesRoute } from './nodes'; -export { logstashNodeRoute } from './node'; -export { logstashOverviewRoute } from './overview'; -export { logstashPipelineRoute } from './pipeline'; -export { logstashNodePipelinesRoute } from './pipelines/node_pipelines'; -export { logstashClusterPipelinesRoute } from './pipelines/cluster_pipelines'; -export { logstashClusterPipelineIdsRoute } from './pipelines/cluster_pipeline_ids'; +import { MonitoringCore } from '../../../../types'; +import { logstashNodeRoute } from './node'; +import { logstashNodesRoute } from './nodes'; +import { logstashOverviewRoute } from './overview'; +import { logstashPipelineRoute } from './pipeline'; +import { logstashClusterPipelinesRoute } from './pipelines/cluster_pipelines'; +import { logstashClusterPipelineIdsRoute } from './pipelines/cluster_pipeline_ids'; +import { logstashNodePipelinesRoute } from './pipelines/node_pipelines'; + +export function registerV1LogstashRoutes(server: MonitoringCore) { + logstashClusterPipelineIdsRoute(server); + logstashClusterPipelinesRoute(server); + logstashNodePipelinesRoute(server); + logstashNodeRoute(server); + logstashNodesRoute(server); + logstashOverviewRoute(server); + logstashPipelineRoute(server); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.js b/x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.js deleted file mode 100644 index bc8b722d22214..0000000000000 --- a/x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth'; -import { handleError } from '../../../../lib/errors'; -import { getCollectionStatus } from '../../../../lib/setup/collection'; -import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; - -export function clusterSetupStatusRoute(server) { - /* - * Monitoring Home - * Route Init (for checking license and compatibility for multi-cluster monitoring - */ - server.route({ - method: 'POST', - path: '/api/monitoring/v1/setup/collection/cluster/{clusterUuid?}', - config: { - validate: { - params: schema.object({ - clusterUuid: schema.maybe(schema.string()), - }), - query: schema.object({ - // This flag is not intended to be used in production. It was introduced - // as a way to ensure consistent API testing - the typical data source - // for API tests are archived data, where the cluster configuration and data - // are consistent from environment to environment. However, this endpoint - // also attempts to retrieve data from the running stack products (ES and Kibana) - // which will vary from environment to environment making it difficult - // to write tests against. Therefore, this flag exists and should only be used - // in our testing environment. - skipLiveData: schema.boolean({ defaultValue: false }), - }), - body: schema.nullable( - schema.object({ - ccs: schema.maybe(schema.string()), - timeRange: schema.object({ - min: schema.string({ defaultValue: '' }), - max: schema.string({ defaultValue: '' }), - }), - }) - ), - }, - }, - handler: async (req) => { - let status = null; - - // NOTE using try/catch because checkMonitoringAuth is expected to throw - // an error when current logged-in user doesn't have permission to read - // the monitoring data. `try/catch` makes it a little more explicit. - try { - await verifyMonitoringAuth(req); - const indexPatterns = getIndexPatterns(server, {}, req.payload.ccs); - status = await getCollectionStatus( - req, - indexPatterns, - req.params.clusterUuid, - null, - req.query.skipLiveData - ); - } catch (err) { - throw handleError(err, req); - } - - return status; - }, - }); -} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.ts new file mode 100644 index 0000000000000..370947df46b42 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + postClusterSetupStatusRequestParamsRT, + postClusterSetupStatusRequestPayloadRT, + postClusterSetupStatusRequestQueryRT, + postClusterSetupStatusResponsePayloadRT, +} from '../../../../../common/http_api/setup'; +import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; +import { createValidationFunction } from '../../../../lib/create_route_validation_function'; +import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth'; +import { handleError } from '../../../../lib/errors'; +import { getCollectionStatus } from '../../../../lib/setup/collection'; +import { MonitoringCore } from '../../../../types'; + +export function clusterSetupStatusRoute(server: MonitoringCore) { + /* + * Monitoring Home + * Route Init (for checking license and compatibility for multi-cluster monitoring + */ + + const validateParams = createValidationFunction(postClusterSetupStatusRequestParamsRT); + const validateQuery = createValidationFunction(postClusterSetupStatusRequestQueryRT); + const validateBody = createValidationFunction(postClusterSetupStatusRequestPayloadRT); + + server.route({ + method: 'post', + path: '/api/monitoring/v1/setup/collection/cluster/{clusterUuid?}', + validate: { + params: validateParams, + query: validateQuery, + body: validateBody, + }, + handler: async (req) => { + const clusterUuid = req.params.clusterUuid; + const skipLiveData = req.query.skipLiveData; + + // NOTE using try/catch because checkMonitoringAuth is expected to throw + // an error when current logged-in user doesn't have permission to read + // the monitoring data. `try/catch` makes it a little more explicit. + try { + await verifyMonitoringAuth(req); + const indexPatterns = getIndexPatterns(server.config, {}, req.payload.ccs); + const status = await getCollectionStatus( + req, + indexPatterns, + clusterUuid, + undefined, + skipLiveData + ); + return postClusterSetupStatusResponsePayloadRT.encode(status); + } catch (err) { + throw handleError(err, req); + } + }, + }); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/disable_elasticsearch_internal_collection.js b/x-pack/plugins/monitoring/server/routes/api/v1/setup/disable_elasticsearch_internal_collection.ts similarity index 74% rename from x-pack/plugins/monitoring/server/routes/api/v1/setup/disable_elasticsearch_internal_collection.js rename to x-pack/plugins/monitoring/server/routes/api/v1/setup/disable_elasticsearch_internal_collection.ts index 9590d91c357ee..cdecf346bae9d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/setup/disable_elasticsearch_internal_collection.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/setup/disable_elasticsearch_internal_collection.ts @@ -5,21 +5,19 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { postDisableInternalCollectionRequestParamsRT } from '../../../../../common/http_api/setup'; +import { createValidationFunction } from '../../../../lib/create_route_validation_function'; import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth'; -import { handleError } from '../../../../lib/errors'; import { setCollectionDisabled } from '../../../../lib/elasticsearch_settings/set/collection_disabled'; +import { handleError } from '../../../../lib/errors'; +import { MonitoringCore } from '../../../../types'; -export function disableElasticsearchInternalCollectionRoute(server) { +export function disableElasticsearchInternalCollectionRoute(server: MonitoringCore) { server.route({ - method: 'POST', + method: 'post', path: '/api/monitoring/v1/setup/collection/{clusterUuid}/disable_internal_collection', - config: { - validate: { - params: schema.object({ - clusterUuid: schema.string(), - }), - }, + validate: { + params: createValidationFunction(postDisableInternalCollectionRequestParamsRT), }, handler: async (req) => { // NOTE using try/catch because checkMonitoringAuth is expected to throw diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/setup/index.ts new file mode 100644 index 0000000000000..6a8ecac8597a8 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/setup/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MonitoringCore } from '../../../../types'; +import { clusterSetupStatusRoute } from './cluster_setup_status'; +import { disableElasticsearchInternalCollectionRoute } from './disable_elasticsearch_internal_collection'; +import { nodeSetupStatusRoute } from './node_setup_status'; + +export function registerV1SetupRoutes(server: MonitoringCore) { + clusterSetupStatusRoute(server); + disableElasticsearchInternalCollectionRoute(server); + nodeSetupStatusRoute(server); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.js b/x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.js deleted file mode 100644 index 1f93e92843ea8..0000000000000 --- a/x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth'; -import { handleError } from '../../../../lib/errors'; -import { getCollectionStatus } from '../../../../lib/setup/collection'; -import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; - -export function nodeSetupStatusRoute(server) { - /* - * Monitoring Home - * Route Init (for checking license and compatibility for multi-cluster monitoring - */ - server.route({ - method: 'POST', - path: '/api/monitoring/v1/setup/collection/node/{nodeUuid}', - config: { - validate: { - params: schema.object({ - nodeUuid: schema.string(), - }), - query: schema.object({ - // This flag is not intended to be used in production. It was introduced - // as a way to ensure consistent API testing - the typical data source - // for API tests are archived data, where the cluster configuration and data - // are consistent from environment to environment. However, this endpoint - // also attempts to retrieve data from the running stack products (ES and Kibana) - // which will vary from environment to environment making it difficult - // to write tests against. Therefore, this flag exists and should only be used - // in our testing environment. - skipLiveData: schema.boolean({ defaultValue: false }), - }), - body: schema.nullable( - schema.object({ - ccs: schema.maybe(schema.string()), - timeRange: schema.maybe( - schema.object({ - min: schema.string(), - max: schema.string(), - }) - ), - }) - ), - }, - }, - handler: async (req) => { - let status = null; - - // NOTE using try/catch because checkMonitoringAuth is expected to throw - // an error when current logged-in user doesn't have permission to read - // the monitoring data. `try/catch` makes it a little more explicit. - try { - await verifyMonitoringAuth(req); - const indexPatterns = getIndexPatterns(server, {}, req.payload.ccs); - status = await getCollectionStatus( - req, - indexPatterns, - null, - req.params.nodeUuid, - req.query.skipLiveData - ); - } catch (err) { - throw handleError(err, req); - } - - return status; - }, - }); -} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.ts new file mode 100644 index 0000000000000..327b741a0e64a --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + postNodeSetupStatusRequestParamsRT, + postNodeSetupStatusRequestPayloadRT, + postNodeSetupStatusRequestQueryRT, + postNodeSetupStatusResponsePayloadRT, +} from '../../../../../common/http_api/setup'; +import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; +import { createValidationFunction } from '../../../../lib/create_route_validation_function'; +import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth'; +import { handleError } from '../../../../lib/errors'; +import { getCollectionStatus } from '../../../../lib/setup/collection'; +import { MonitoringCore } from '../../../../types'; + +export function nodeSetupStatusRoute(server: MonitoringCore) { + /* + * Monitoring Home + * Route Init (for checking license and compatibility for multi-cluster monitoring + */ + + const validateParams = createValidationFunction(postNodeSetupStatusRequestParamsRT); + const validateQuery = createValidationFunction(postNodeSetupStatusRequestQueryRT); + const validateBody = createValidationFunction(postNodeSetupStatusRequestPayloadRT); + + server.route({ + method: 'post', + path: '/api/monitoring/v1/setup/collection/node/{nodeUuid}', + validate: { + params: validateParams, + query: validateQuery, + body: validateBody, + }, + handler: async (req) => { + const nodeUuid = req.params.nodeUuid; + const skipLiveData = req.query.skipLiveData; + const ccs = req.payload.ccs; + + // NOTE using try/catch because checkMonitoringAuth is expected to throw + // an error when current logged-in user doesn't have permission to read + // the monitoring data. `try/catch` makes it a little more explicit. + try { + await verifyMonitoringAuth(req); + const indexPatterns = getIndexPatterns(server.config, {}, ccs); + const status = await getCollectionStatus( + req, + indexPatterns, + undefined, + nodeUuid, + skipLiveData + ); + + return postNodeSetupStatusResponsePayloadRT.encode(status); + } catch (err) { + throw handleError(err, req); + } + }, + }); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/ui.js b/x-pack/plugins/monitoring/server/routes/api/v1/ui.js deleted file mode 100644 index 618d12afedef7..0000000000000 --- a/x-pack/plugins/monitoring/server/routes/api/v1/ui.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// all routes for the app -export { checkAccessRoute } from './check_access'; -export * from './alerts'; -export { beatsDetailRoute, beatsListingRoute, beatsOverviewRoute } from './beats'; -export { clusterRoute, clustersRoute } from './cluster'; -export { - esIndexRoute, - esIndicesRoute, - esNodeRoute, - esNodesRoute, - esOverviewRoute, - mlJobRoute, - ccrRoute, - ccrShardRoute, -} from './elasticsearch'; -export { - internalMonitoringCheckRoute, - clusterSettingsCheckRoute, - nodesSettingsCheckRoute, - setCollectionEnabledRoute, - setCollectionIntervalRoute, -} from './elasticsearch_settings'; -export { kibanaInstanceRoute, kibanaInstancesRoute, kibanaOverviewRoute } from './kibana'; -export { apmInstanceRoute, apmInstancesRoute, apmOverviewRoute } from './apm'; -export { - logstashClusterPipelinesRoute, - logstashNodePipelinesRoute, - logstashNodeRoute, - logstashNodesRoute, - logstashOverviewRoute, - logstashPipelineRoute, - logstashClusterPipelineIdsRoute, -} from './logstash'; -export { entSearchOverviewRoute } from './enterprise_search'; -export * from './setup'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/ui.ts b/x-pack/plugins/monitoring/server/routes/api/v1/ui.ts new file mode 100644 index 0000000000000..7aaa6591e868e --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/ui.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// these are the remaining routes not yet converted to TypeScript +// all others are registered through index.ts + +// @ts-expect-error +export { kibanaInstanceRoute, kibanaInstancesRoute, kibanaOverviewRoute } from './kibana'; +// @ts-expect-error +export { entSearchOverviewRoute } from './enterprise_search'; diff --git a/x-pack/plugins/monitoring/server/routes/index.ts b/x-pack/plugins/monitoring/server/routes/index.ts index 05a8de96b4c07..f38612d5a42da 100644 --- a/x-pack/plugins/monitoring/server/routes/index.ts +++ b/x-pack/plugins/monitoring/server/routes/index.ts @@ -8,22 +8,43 @@ import { MonitoringConfig } from '../config'; import { decorateDebugServer } from '../debug_logger'; -import { RouteDependencies } from '../types'; -// @ts-ignore -import * as uiRoutes from './api/v1/ui'; // namespace import +import { MonitoringCore, RouteDependencies } from '../types'; +import { + registerV1AlertRoutes, + registerV1ApmRoutes, + registerV1BeatsRoutes, + registerV1CheckAccessRoutes, + registerV1ClusterRoutes, + registerV1ElasticsearchRoutes, + registerV1ElasticsearchSettingsRoutes, + registerV1LogstashRoutes, + registerV1SetupRoutes, +} from './api/v1'; +import * as uiRoutes from './api/v1/ui'; export function requireUIRoutes( - _server: any, + server: MonitoringCore, config: MonitoringConfig, npRoute: RouteDependencies ) { const routes = Object.keys(uiRoutes); - const server = config.ui.debug_mode - ? decorateDebugServer(_server, config, npRoute.logger) - : _server; + const decoratedServer = config.ui.debug_mode + ? decorateDebugServer(server, config, npRoute.logger) + : server; routes.forEach((route) => { + // @ts-expect-error const registerRoute = uiRoutes[route]; // computed reference to module objects imported via namespace registerRoute(server, npRoute); }); + + registerV1AlertRoutes(decoratedServer, npRoute); + registerV1ApmRoutes(server); + registerV1BeatsRoutes(server); + registerV1CheckAccessRoutes(server); + registerV1ClusterRoutes(server); + registerV1ElasticsearchRoutes(server); + registerV1ElasticsearchSettingsRoutes(server, npRoute); + registerV1LogstashRoutes(server); + registerV1SetupRoutes(server); } diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 86447a24fdf04..64931f5888514 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -34,7 +34,7 @@ import { LicensingPluginStart } from '@kbn/licensing-plugin/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '@kbn/features-plugin/server'; import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; import { CloudSetup } from '@kbn/cloud-plugin/server'; -import { RouteConfig, RouteMethod } from '@kbn/core/server'; +import { RouteConfig, RouteMethod, Headers } from '@kbn/core/server'; import { ElasticsearchModifiedSource } from '../common/types/es'; import { RulesByType } from '../common/types/alerts'; import { configSchema, MonitoringConfig } from './config'; @@ -124,6 +124,7 @@ export interface LegacyRequest { payload: Body; params: Params; query: Query; + headers: Headers; getKibanaStatsCollector: () => any; getUiSettingsService: () => any; getActionTypeRegistry: () => any; From 6fc2fff3f2dfc263f767bbc54f46eb4946438e4c Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 20 May 2022 10:48:15 -0700 Subject: [PATCH 052/120] [ML] Minor edits in prebuilt job descriptions (#132633) --- .../modules/security_auth/ml/auth_high_count_logon_events.json | 2 +- .../ml/auth_high_count_logon_events_for_a_source_ip.json | 2 +- .../modules/security_auth/ml/auth_high_count_logon_fails.json | 2 +- .../models/data_recognizer/modules/security_linux/manifest.json | 2 +- .../security_network/ml/high_count_by_destination_country.json | 2 +- .../modules/security_network/ml/high_count_network_denies.json | 2 +- .../modules/security_network/ml/high_count_network_events.json | 2 +- .../data_recognizer/modules/security_windows/manifest.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json index 35fc14e23624f..fa87299dfb464 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json @@ -1,6 +1,6 @@ { "job_type": "anomaly_detector", - "description": "Security: Authentication - looks for an unusually large spike in successful authentication events. This can be due to password spraying, user enumeration or brute force activity.", + "description": "Security: Authentication - Looks for an unusually large spike in successful authentication events. This can be due to password spraying, user enumeration, or brute force activity.", "groups": [ "security", "authentication" diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json index cdf219152c7fd..9f2f10973a35b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json @@ -1,6 +1,6 @@ { "job_type": "anomaly_detector", - "description": "Security: Authentication - looks for an unusually large spike in successful authentication events from a particular source IP address. This can be due to password spraying, user enumeration or brute force activity.", + "description": "Security: Authentication - Looks for an unusually large spike in successful authentication events from a particular source IP address. This can be due to password spraying, user enumeration, or brute force activity.", "groups": [ "security", "authentication" diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json index cde52bf7d33cc..c74dff5257864 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json @@ -1,6 +1,6 @@ { "job_type": "anomaly_detector", - "description": "Security: Authentication - looks for an unusually large spike in authentication failure events. This can be due to password spraying, user enumeration or brute force activity and may be a precursor to account takeover or credentialed access.", + "description": "Security: Authentication - Looks for an unusually large spike in authentication failure events. This can be due to password spraying, user enumeration, or brute force activity and may be a precursor to account takeover or credentialed access.", "groups": [ "security", "authentication" diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json index efed4a3c9e9b1..cfa9f45c5d1ac 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json @@ -1,7 +1,7 @@ { "id": "security_linux_v3", "title": "Security: Linux", - "description": "Anomaly detection jobs for Linux host based threat hunting and detection.", + "description": "Anomaly detection jobs for Linux host-based threat hunting and detection.", "type": "linux data", "logoFile": "logo.json", "defaultIndexPattern": "auditbeat-*,logs-*", diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json index 2360233937c2b..45375ad939f36 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json @@ -1,6 +1,6 @@ { "job_type": "anomaly_detector", - "description": "Security: Network - looks for an unusually large spike in network activity to one destination country in the network logs. This could be due to unusually large amounts of reconnaissance or enumeration traffic. Data exfiltration activity may also produce such a surge in traffic to a destination country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.", + "description": "Security: Network - Looks for an unusually large spike in network activity to one destination country in the network logs. This could be due to unusually large amounts of reconnaissance or enumeration traffic. Data exfiltration activity may also produce such a surge in traffic to a destination country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.", "groups": [ "security", "network" diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json index 2a3b4b0100183..45c22599f37d2 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json @@ -1,6 +1,6 @@ { "job_type": "anomaly_detector", - "description": "Security: Network - looks for an unusually large spike in network traffic that was denied by network ACLs or firewall rules. Such a burst of denied traffic is usually either 1) a misconfigured application or firewall or 2) suspicious or malicious activity. Unsuccessful attempts at network transit, in order to connect to command-and-control (C2), or engage in data exfiltration, may produce a burst of failed connections. This could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", + "description": "Security: Network - Looks for an unusually large spike in network traffic that was denied by network ACLs or firewall rules. Such a burst of denied traffic is usually either 1) a misconfigured application or firewall or 2) suspicious or malicious activity. Unsuccessful attempts at network transit, in order to connect to command-and-control (C2), or engage in data exfiltration, may produce a burst of failed connections. This could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", "groups": [ "security", "network" diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json index 792d7f2513985..a3bb734ad9bdc 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json @@ -1,6 +1,6 @@ { "job_type": "anomaly_detector", - "description": "Security: Network - looks for an unusually large spike in network traffic. Such a burst of traffic, if not caused by a surge in business activity, can be due to suspicious or malicious activity. Large-scale data exfiltration may produce a burst of network traffic; this could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", + "description": "Security: Network - Looks for an unusually large spike in network traffic. Such a burst of traffic, if not caused by a surge in business activity, can be due to suspicious or malicious activity. Large-scale data exfiltration may produce a burst of network traffic; this could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", "groups": [ "security", "network" diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/manifest.json index bf39cd7ec7902..8d01d0d91e0c2 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/manifest.json @@ -1,7 +1,7 @@ { "id": "security_windows_v3", "title": "Security: Windows", - "description": "Anomaly detection jobs for Windows host based threat hunting and detection.", + "description": "Anomaly detection jobs for Windows host-based threat hunting and detection.", "type": "windows data", "logoFile": "logo.json", "defaultIndexPattern": "winlogbeat-*,logs-*", From e857b30f8a9b4b1ccaa9527b3809875b892dec01 Mon Sep 17 00:00:00 2001 From: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> Date: Fri, 20 May 2022 20:36:59 +0200 Subject: [PATCH 053/120] remove human-readable automatic slug generation (#132593) * remove human-readable automatic slug generation * make change non-breaking * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * remove test Co-authored-by: streamich Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/share/README.mdx | 13 ------------- .../share/common/url_service/short_urls/types.ts | 6 ------ .../short_urls/short_url_client.test.ts | 2 -- .../url_service/short_urls/short_url_client.ts | 3 --- .../http/short_urls/register_create_route.ts | 12 ++++++++++-- .../short_urls/short_url_client.test.ts | 13 ------------- .../url_service/short_urls/short_url_client.ts | 4 +--- .../apis/short_url/create_short_url/main.ts | 16 ---------------- 8 files changed, 11 insertions(+), 58 deletions(-) diff --git a/src/plugins/share/README.mdx b/src/plugins/share/README.mdx index 1a1e2e721c2ab..1a1fef0587812 100644 --- a/src/plugins/share/README.mdx +++ b/src/plugins/share/README.mdx @@ -215,19 +215,6 @@ const url = await shortUrls.create({ }); ``` -You can make the short URL slug human-readable by specifying the -`humanReadableSlug` flag: - -```ts -const url = await shortUrls.create({ - locator, - params: { - dashboardId: '123', - }, - humanReadableSlug: true, -}); -``` - Or you can manually specify the slug for the short URL using the `slug` option: ```ts diff --git a/src/plugins/share/common/url_service/short_urls/types.ts b/src/plugins/share/common/url_service/short_urls/types.ts index 44e11a0610a66..a7c4d106873f9 100644 --- a/src/plugins/share/common/url_service/short_urls/types.ts +++ b/src/plugins/share/common/url_service/short_urls/types.ts @@ -79,12 +79,6 @@ export interface ShortUrlCreateParams

{ * URL. This part will be visible to the user, it can have user-friendly text. */ slug?: string; - - /** - * Whether to generate a slug automatically. If `true`, the slug will be - * a human-readable text consisting of three worlds: "--". - */ - humanReadableSlug?: boolean; } /** diff --git a/src/plugins/share/public/url_service/short_urls/short_url_client.test.ts b/src/plugins/share/public/url_service/short_urls/short_url_client.test.ts index 8a125206d1c80..693d06538e63e 100644 --- a/src/plugins/share/public/url_service/short_urls/short_url_client.test.ts +++ b/src/plugins/share/public/url_service/short_urls/short_url_client.test.ts @@ -88,7 +88,6 @@ describe('create()', () => { body: expect.any(String), }); expect(JSON.parse(fetchSpy.mock.calls[0][1].body)).toStrictEqual({ - humanReadableSlug: false, locatorId: LEGACY_SHORT_URL_LOCATOR_ID, params: { url: 'https://example.com/foo/bar', @@ -173,7 +172,6 @@ describe('createFromLongUrl()', () => { body: expect.any(String), }); expect(JSON.parse(fetchSpy.mock.calls[0][1].body)).toStrictEqual({ - humanReadableSlug: true, locatorId: LEGACY_SHORT_URL_LOCATOR_ID, params: { url: '/a/b/c', diff --git a/src/plugins/share/public/url_service/short_urls/short_url_client.ts b/src/plugins/share/public/url_service/short_urls/short_url_client.ts index 63dcdc0b78718..4a9dbf3909288 100644 --- a/src/plugins/share/public/url_service/short_urls/short_url_client.ts +++ b/src/plugins/share/public/url_service/short_urls/short_url_client.ts @@ -59,7 +59,6 @@ export class BrowserShortUrlClient implements IShortUrlClient { locator, params, slug = undefined, - humanReadableSlug = false, }: ShortUrlCreateParams

): Promise> { const { http } = this.dependencies; const data = await http.fetch>('/api/short_url', { @@ -67,7 +66,6 @@ export class BrowserShortUrlClient implements IShortUrlClient { body: JSON.stringify({ locatorId: locator.id, slug, - humanReadableSlug, params, }), }); @@ -113,7 +111,6 @@ export class BrowserShortUrlClient implements IShortUrlClient { const result = await this.createWithLocator({ locator, - humanReadableSlug: true, params: { url: relativeUrl, }, diff --git a/src/plugins/share/server/url_service/http/short_urls/register_create_route.ts b/src/plugins/share/server/url_service/http/short_urls/register_create_route.ts index 1208f6fda4d1e..97594837f0720 100644 --- a/src/plugins/share/server/url_service/http/short_urls/register_create_route.ts +++ b/src/plugins/share/server/url_service/http/short_urls/register_create_route.ts @@ -26,6 +26,15 @@ export const registerCreateRoute = (router: IRouter, url: ServerUrlService) => { minLength: 3, maxLength: 255, }), + /** + * @deprecated + * + * This field is deprecated as the API does not support automatic + * human-readable slug generation. + * + * @todo This field will be removed in a future version. It is left + * here for backwards compatibility. + */ humanReadableSlug: schema.boolean({ defaultValue: false, }), @@ -36,7 +45,7 @@ export const registerCreateRoute = (router: IRouter, url: ServerUrlService) => { router.handleLegacyErrors(async (ctx, req, res) => { const savedObjects = (await ctx.core).savedObjects.client; const shortUrls = url.shortUrls.get({ savedObjects }); - const { locatorId, params, slug, humanReadableSlug } = req.body; + const { locatorId, params, slug } = req.body; const locator = url.locators.get(locatorId); if (!locator) { @@ -51,7 +60,6 @@ export const registerCreateRoute = (router: IRouter, url: ServerUrlService) => { locator, params, slug, - humanReadableSlug, }); return res.ok({ diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts b/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts index 5fc108cdbf56c..fe6365d498628 100644 --- a/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts +++ b/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts @@ -128,19 +128,6 @@ describe('ServerShortUrlClient', () => { }) ).rejects.toThrowError(new UrlServiceError(`Slug "lala" already exists.`, 'SLUG_EXISTS')); }); - - test('can automatically generate human-readable slug', async () => { - const { client, locator } = setup(); - const shortUrl = await client.create({ - locator, - humanReadableSlug: true, - params: { - url: '/app/test#foo/bar/baz', - }, - }); - - expect(shortUrl.data.slug.split('-').length).toBe(3); - }); }); describe('.get()', () => { diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client.ts b/src/plugins/share/server/url_service/short_urls/short_url_client.ts index 762ded11bf8ee..cecc4c3127135 100644 --- a/src/plugins/share/server/url_service/short_urls/short_url_client.ts +++ b/src/plugins/share/server/url_service/short_urls/short_url_client.ts @@ -8,7 +8,6 @@ import type { SerializableRecord } from '@kbn/utility-types'; import { SavedObjectReference } from '@kbn/core/server'; -import { generateSlug } from 'random-word-slugs'; import { ShortUrlRecord } from '.'; import type { IShortUrlClient, @@ -60,14 +59,13 @@ export class ServerShortUrlClient implements IShortUrlClient { locator, params, slug = '', - humanReadableSlug = false, }: ShortUrlCreateParams

): Promise> { if (slug) { validateSlug(slug); } if (!slug) { - slug = humanReadableSlug ? generateSlug() : randomStr(4); + slug = randomStr(5); } const { storage, currentVersion } = this.dependencies; diff --git a/test/api_integration/apis/short_url/create_short_url/main.ts b/test/api_integration/apis/short_url/create_short_url/main.ts index 4eb6fa489b725..d0b57a9873135 100644 --- a/test/api_integration/apis/short_url/create_short_url/main.ts +++ b/test/api_integration/apis/short_url/create_short_url/main.ts @@ -70,22 +70,6 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body.url).to.be(''); }); - it('can generate a human-readable slug, composed of three words', async () => { - const response = await supertest.post('/api/short_url').send({ - locatorId: 'LEGACY_SHORT_URL_LOCATOR', - params: {}, - humanReadableSlug: true, - }); - - expect(response.status).to.be(200); - expect(typeof response.body.slug).to.be('string'); - const words = response.body.slug.split('-'); - expect(words.length).to.be(3); - for (const word of words) { - expect(word.length > 0).to.be(true); - } - }); - it('can create a short URL with custom slug', async () => { const rnd = Math.round(Math.random() * 1e6) + 1; const slug = 'test-slug-' + Date.now() + '-' + rnd; From 46cd72911c5e96b8dc8df6e12d47df004efba381 Mon Sep 17 00:00:00 2001 From: Jan Monschke Date: Fri, 20 May 2022 22:02:00 +0200 Subject: [PATCH 054/120] [SecuritySolution] Disable agent status filters and timeline interaction (#132586) * fix: disable drag-ability and hover actions for agent statuses The agent fields cannot be queried with ECS and therefore should not provide Filter In/Out functionality nor should users be able to add their representative fields to timeline investigations. Therefore users should not be able to add them to a timeline query by dragging them. * chore: make code more readable --- .../table/summary_value_cell.tsx | 46 +++++++++++-------- .../body/renderers/agent_statuses.tsx | 38 +++------------ 2 files changed, 32 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx index 1c9c0292ed912..d4677d22485b4 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx @@ -16,6 +16,8 @@ import { AGENT_STATUS_FIELD_NAME } from '../../../../timelines/components/timeli const FIELDS_WITHOUT_ACTIONS: { [field: string]: boolean } = { [AGENT_STATUS_FIELD_NAME]: true }; +const style = { flexGrow: 0 }; + export const SummaryValueCell: React.FC = ({ data, eventId, @@ -25,32 +27,36 @@ export const SummaryValueCell: React.FC = ({ timelineId, values, isReadOnly, -}) => ( - <> - - {timelineId !== TimelineId.active && !isReadOnly && !FIELDS_WITHOUT_ACTIONS[data.field] && ( - { + const hoverActionsEnabled = !FIELDS_WITHOUT_ACTIONS[data.field]; + + return ( + <> + - )} - -); + {timelineId !== TimelineId.active && !isReadOnly && hoverActionsEnabled && ( + + )} + + ); +}; SummaryValueCell.displayName = 'SummaryValueCell'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx index edc8faff1b5fc..c459a9f05a678 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { DefaultDraggable } from '../../../../../common/components/draggables'; import { EndpointHostIsolationStatus } from '../../../../../common/components/endpoint/host_isolation'; import { useHostIsolationStatus } from '../../../../../detections/containers/detection_engine/alerts/use_host_isolation_status'; import { AgentStatus } from '../../../../../common/components/endpoint/agent_status'; @@ -33,26 +32,11 @@ export const AgentStatuses = React.memo( }) => { const { isIsolated, agentStatus, pendingIsolation, pendingUnisolation } = useHostIsolationStatus({ agentId: value }); - const isolationFieldName = 'host.isolation'; return ( {agentStatus !== undefined ? ( - {isDraggable ? ( - - - - ) : ( - - )} + ) : ( @@ -60,21 +44,11 @@ export const AgentStatuses = React.memo( )} - - - + ); From e55bf409976a22c429d3f48f4a3c198e2591cbe3 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 20 May 2022 14:15:00 -0600 Subject: [PATCH 055/120] [Maps] create MVT_VECTOR when using choropleth wizard (#132648) --- .../create_choropleth_layer_descriptor.ts | 61 +++++++++++-------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts index 92045f5911176..36e07d7383d18 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts @@ -10,6 +10,7 @@ import { AGG_TYPE, COLOR_MAP_TYPE, FIELD_ORIGIN, + LAYER_TYPE, SCALING_TYPES, SOURCE_TYPES, STYLE_TYPE, @@ -21,10 +22,11 @@ import { CountAggDescriptor, EMSFileSourceDescriptor, ESSearchSourceDescriptor, + JoinDescriptor, VectorStylePropertiesDescriptor, } from '../../../../../common/descriptor_types'; import { VectorStyle } from '../../../styles/vector/vector_style'; -import { GeoJsonVectorLayer } from '../../vector_layer'; +import { GeoJsonVectorLayer, MvtVectorLayer } from '../../vector_layer'; import { EMSFileSource } from '../../../sources/ems_file_source'; // @ts-ignore import { ESSearchSource } from '../../../sources/es_search_source'; @@ -38,14 +40,14 @@ function createChoroplethLayerDescriptor({ rightIndexPatternId, rightIndexPatternTitle, rightTermField, - setLabelStyle, + layerType, }: { sourceDescriptor: EMSFileSourceDescriptor | ESSearchSourceDescriptor; leftField: string; rightIndexPatternId: string; rightIndexPatternTitle: string; rightTermField: string; - setLabelStyle: boolean; + layerType: LAYER_TYPE.GEOJSON_VECTOR | LAYER_TYPE.MVT_VECTOR; }) { const metricsDescriptor: CountAggDescriptor = { type: AGG_TYPE.COUNT }; const joinId = uuid(); @@ -75,7 +77,8 @@ function createChoroplethLayerDescriptor({ }, }, }; - if (setLabelStyle) { + // Styling label by join metric with MVT is not supported + if (layerType === LAYER_TYPE.GEOJSON_VECTOR) { styleProperties[VECTOR_STYLES.LABEL_TEXT] = { type: STYLE_TYPE.DYNAMIC, options: { @@ -88,26 +91,34 @@ function createChoroplethLayerDescriptor({ }; } - return GeoJsonVectorLayer.createDescriptor({ - joins: [ - { - leftField, - right: { - type: SOURCE_TYPES.ES_TERM_SOURCE, - id: joinId, - indexPatternId: rightIndexPatternId, - indexPatternTitle: rightIndexPatternTitle, - term: rightTermField, - metrics: [metricsDescriptor], - applyGlobalQuery: true, - applyGlobalTime: true, - applyForceRefresh: true, - }, + const joins = [ + { + leftField, + right: { + type: SOURCE_TYPES.ES_TERM_SOURCE, + id: joinId, + indexPatternId: rightIndexPatternId, + indexPatternTitle: rightIndexPatternTitle, + term: rightTermField, + metrics: [metricsDescriptor], + applyGlobalQuery: true, + applyGlobalTime: true, + applyForceRefresh: true, }, - ], - sourceDescriptor, - style: VectorStyle.createDescriptor(styleProperties), - }); + } as JoinDescriptor, + ]; + + return layerType === LAYER_TYPE.MVT_VECTOR + ? MvtVectorLayer.createDescriptor({ + joins, + sourceDescriptor, + style: VectorStyle.createDescriptor(styleProperties), + }) + : GeoJsonVectorLayer.createDescriptor({ + joins, + sourceDescriptor, + style: VectorStyle.createDescriptor(styleProperties), + }); } export function createEmsChoroplethLayerDescriptor({ @@ -132,7 +143,7 @@ export function createEmsChoroplethLayerDescriptor({ rightIndexPatternId, rightIndexPatternTitle, rightTermField, - setLabelStyle: true, + layerType: LAYER_TYPE.GEOJSON_VECTOR, }); } @@ -165,6 +176,6 @@ export function createEsChoroplethLayerDescriptor({ rightIndexPatternId, rightIndexPatternTitle, rightTermField, - setLabelStyle: false, // Styling label by join metric with MVT is not supported + layerType: LAYER_TYPE.MVT_VECTOR, }); } From 791ebfad8c589b91555a7e252d99ea1841f75906 Mon Sep 17 00:00:00 2001 From: debadair Date: Fri, 20 May 2022 13:34:04 -0700 Subject: [PATCH 056/120] [DOCS] Remove obsolete license expiration info (#131474) * [DOCS] Remove obsolete license expiration info As of https://github.com/elastic/elasticsearch/pull/79671, Elasticsearch does a more stringent license check rather than operating in a semi-degraded mode. Closes #127845 Closes #125702 * Update docs/management/managing-licenses.asciidoc Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/management/managing-licenses.asciidoc | 192 +++------------------ 1 file changed, 22 insertions(+), 170 deletions(-) diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc index cf501518ea534..837a83f0aae38 100644 --- a/docs/management/managing-licenses.asciidoc +++ b/docs/management/managing-licenses.asciidoc @@ -1,191 +1,43 @@ [[managing-licenses]] == License Management -When you install the default distribution of {kib}, you receive free features -with no expiration date. For the full list of features, refer to -{subscriptions}. +By default, new installations have a Basic license that never expires. +For the full list of features available at the Free and Open Basic subscription level, +refer to {subscriptions}. -If you want to try out the full set of features, you can activate a free 30-day -trial. To view the status of your license, start a trial, or install a new -license, open the main menu, then click *Stack Management > License Management*. - -NOTE: You can start a trial only if your cluster has not already activated a -trial license for the current major product version. For example, if you have -already activated a trial for 6.0, you cannot start a new trial until -7.0. You can, however, request an extended trial at {extendtrial}. - -When you activate a new license level, new features appear in *Stack Management*. - -[role="screenshot"] -image::images/management-license.png[] +To explore all of the available solutions and features, start a 30-day free trial. +You can activate a trial subscription once per major product version. +If you need more than 30 days to complete your evaluation, +request an extended trial at {extendtrial}. -At the end of the trial period, some features operate in a -<>. You can revert to Basic, extend the trial, -or purchase a subscription. - -TIP: If {security-features} are enabled, unless you have a trial license, -you must configure Transport Layer Security (TLS) in {es}. -See {ref}/encrypting-communications.html[Encrypting communications]. -{kib} and the {ref}/start-basic.html[start basic API] provide a list of all of -the features that will no longer be supported if you revert to a basic license. +To view the status of your license, start a trial, or install a new +license, open the main menu, then click *Stack Management > License Management*. -[float] +[discrete] === Required permissions The `manage` cluster privilege is required to access *License Management*. To add the privilege, open the main menu, then click *Stack Management > Roles*. -[discrete] -[[update-license]] -=== Update your license - -You can update your license at runtime without shutting down your {es} nodes. -License updates take effect immediately. The license is provided as a _JSON_ -file that you install in {kib} or by using the -{ref}/update-license.html[update license API]. - -TIP: If you are using a basic or trial license, {security-features} are disabled -by default. In all other licenses, {security-features} are enabled by default; -you must secure the {stack} or disable the {security-features}. - [discrete] [[license-expiration]] === License expiration -Your license is time based and expires at a future date. If you're using -{monitor-features} and your license will expire within 30 days, a license -expiration warning is displayed prominently. Warnings are also displayed on -startup and written to the {es} log starting 30 days from the expiration date. -These error messages tell you when the license expires and what features will be -disabled if you do not update the license. - -IMPORTANT: You should update your license as soon as possible. You are -essentially flying blind when running with an expired license. Access to the -cluster health and stats APIs is critical for monitoring and managing an {es} -cluster. - -[discrete] -[[expiration-beats]] -==== Beats - -* Beats will continue to poll centrally-managed configuration. - -[discrete] -[[expiration-elasticsearch]] -==== {es} - -// Upgrade API is disabled -* The deprecation API is disabled. -* SQL support is disabled. -* Aggregations provided by the analytics plugin are no longer usable. -* All searchable snapshots indices are unassigned and cannot be searched. - -[discrete] -[[expiration-watcher]] -==== {stack} {alert-features} - -* The PUT and GET watch APIs are disabled. The DELETE watch API continues to work. -* Watches execute and write to the history. -* The actions of the watches do not execute. - -[discrete] -[[expiration-graph]] -==== {stack} {graph-features} - -* Graph explore APIs are disabled. - -[discrete] -[[expiration-ml]] -==== {stack} {ml-features} +Licenses are valid for a specific time period. +30 days before the license expiration date, {es} starts logging expiration warnings. +If monitoring is enabled, expiration warnings are displayed prominently in {kib}. -* APIs to create {anomaly-jobs}, open jobs, send data to jobs, create {dfeeds}, -and start {dfeeds} are disabled. -* All started {dfeeds} are stopped. -* All open {anomaly-jobs} are closed. -* APIs to create and start {dfanalytics-jobs} are disabled. -* Existing {anomaly-job} and {dfanalytics-job} results continue to be available -by using {kib} or APIs. +If your license expires, your subscription level reverts to Basic and +you will no longer be able to use https://www.elastic.co/subscriptions[Platinum or Enterprise features]. [discrete] -[[expiration-monitoring]] -==== {stack} {monitor-features} - -* The agent stops collecting cluster and indices metrics. -* The agent stops automatically cleaning indices older than -`xpack.monitoring.history.duration`. - -[discrete] -[[expiration-security]] -==== {stack} {security-features} - -* Cluster health, cluster stats, and indices stats operations are blocked. -* All data operations (read and write) continue to work. - -Once the license expires, calls to the cluster health, cluster stats, and index -stats APIs fail with a `security_exception` and return a 403 HTTP status code. - -[source,sh] ------------------------------------------------------ -{ - "error": { - "root_cause": [ - { - "type": "security_exception", - "reason": "current license is non-compliant for [security]", - "license.expired.feature": "security" - } - ], - "type": "security_exception", - "reason": "current license is non-compliant for [security]", - "license.expired.feature": "security" - }, - "status": 403 -} ------------------------------------------------------ - -This message enables automatic monitoring systems to easily detect the license -failure without immediately impacting other users. - -[discrete] -[[expiration-logstash]] -==== {ls} pipeline management - -* Cannot create new pipelines or edit or delete existing pipelines from the UI. -* Cannot list or view existing pipelines from the UI. -* Cannot run Logstash instances which are registered to listen to existing pipelines. -//TBD: * Logstash will continue to poll centrally-managed pipelines - -[discrete] -[[expiration-kibana]] -==== {kib} - -* Users can still log into {kib}. -* {kib} works for data exploration and visualization, but some features -are disabled. -* The license management UI is available to easily upgrade your license. See -<> and <>. - -[discrete] -[[expiration-reporting]] -==== {kib} {report-features} - -* Reporting is no longer available in {kib}. -* Report generation URLs stop working. -* Existing reports are no longer accessible. - -[discrete] -[[expiration-rollups]] -==== {rollups-cap} - -* {rollup-jobs-cap} cannot be created or started. -* Existing {rollup-jobs} can be stopped and deleted. -* The get rollup caps and rollup search APIs continue to function. +[[update-license]] +=== Update your license -[discrete] -[[expiration-transforms]] -==== {transforms-cap} +Licenses are provided as a _JSON_ file and have an effective date and an expiration date. +You cannot install a new license before its effective date. +License updates take effect immediately and do not require restarting {es}. -* {transforms-cap} cannot be created, previewed, started, or updated. -* Existing {transforms} can be stopped and deleted. -* Existing {transform} results continue to be available. +You can update your license from *Stack Management > License Management* or through the +{ref}/update-license.html[update license API]. From 41635e288f790a8e79e8294f65d43212fa479366 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Fri, 20 May 2022 13:35:30 -0700 Subject: [PATCH 057/120] fixed search highlighting. was only showing highlighted text w/o context (#132650) Co-authored-by: mitodrummer --- .../public/components/process_tree_node/index.test.tsx | 10 ++++++++-- .../public/components/process_tree_node/index.tsx | 2 +- .../public/components/process_tree_node/styles.ts | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx index 1316313427c5e..cff05c5c1003b 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx @@ -295,13 +295,19 @@ describe('ProcessTreeNode component', () => { describe('Search', () => { it('highlights text within the process node line item if it matches the searchQuery', () => { // set a mock search matched indicator for the process (typically done by ProcessTree/helpers.ts) - processMock.searchMatched = '/vagrant'; + processMock.searchMatched = '/vagr'; renderResult = mockedContext.render(); expect( renderResult.getByTestId('sessionView:processNodeSearchHighlight').textContent - ).toEqual('/vagrant'); + ).toEqual('/vagr'); + + // ensures we are showing the rest of the info, and not replacing it with just the match. + const { process } = props.process.getDetails(); + expect(renderResult.container.textContent).toContain( + process?.working_directory + '\xA0' + (process?.args && process.args.join(' ')) + ); }); }); }); diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index 4d6074497af5a..f65cb0f25530a 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -146,7 +146,7 @@ export function ProcessTreeNode({ }); // eslint-disable-next-line no-unsanitized/property - textRef.current.innerHTML = html; + textRef.current.innerHTML = '' + html + ''; } } }, [searchMatched, styles.searchHighlight]); diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts index b68df480064b3..54dbdb1bc4565 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts @@ -117,7 +117,6 @@ export const useStyles = ({ fontSize: FONT_SIZE, lineHeight: LINE_HEIGHT, verticalAlign: 'middle', - display: 'inline-block', }, }; @@ -165,6 +164,7 @@ export const useStyles = ({ paddingLeft: size.xxl, position: 'relative', lineHeight: LINE_HEIGHT, + marginTop: '1px', }; const alertDetails: CSSObject = { From e0ea600d54ec68159fc6fa89eb761b36988cc1a6 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 20 May 2022 14:55:31 -0600 Subject: [PATCH 058/120] Add group 6 to FTR config (#132655) --- .buildkite/ftr_configs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index e070baa844ea9..4a59641e29af2 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -68,6 +68,7 @@ enabled: - test/functional/apps/dashboard/group3/config.ts - test/functional/apps/dashboard/group4/config.ts - test/functional/apps/dashboard/group5/config.ts + - test/functional/apps/dashboard/group6/config.ts - test/functional/apps/discover/config.ts - test/functional/apps/getting_started/config.ts - test/functional/apps/home/config.ts From eb6a061a930a0c48fa4a28b66197b7681d3fd5cf Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 20 May 2022 16:57:49 -0400 Subject: [PATCH 059/120] [docs] Add 'yarn dev-docs' for managing and starting dev docs (#132647) --- package.json | 1 + scripts/dev_docs.sh | 103 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100755 scripts/dev_docs.sh diff --git a/package.json b/package.json index 9b01ec9decdcb..36a1cd9a5ffad 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "cover:report": "nyc report --temp-dir target/kibana-coverage/functional --report-dir target/coverage/report --reporter=lcov && open ./target/coverage/report/lcov-report/index.html", "debug": "node --nolazy --inspect scripts/kibana --dev", "debug-break": "node --nolazy --inspect-brk scripts/kibana --dev", + "dev-docs": "scripts/dev_docs.sh", "docs:acceptApiChanges": "node --max-old-space-size=6144 scripts/check_published_api_changes.js --accept", "es": "node scripts/es", "preinstall": "node ./preinstall_check", diff --git a/scripts/dev_docs.sh b/scripts/dev_docs.sh new file mode 100755 index 0000000000000..55d8f4d51e8dc --- /dev/null +++ b/scripts/dev_docs.sh @@ -0,0 +1,103 @@ +#!/bin/bash +set -euo pipefail + +KIBANA_DIR=$(cd "$(dirname "$0")"/.. && pwd) +WORKSPACE=$(cd "$KIBANA_DIR/.." && pwd)/kibana-docs +export NVM_DIR="$WORKSPACE/.nvm" + +DOCS_DIR="$WORKSPACE/docs.elastic.dev" + +# These are the other repos with docs currently required to build the docs in this repo and not get errors +# For example, kibana docs link to docs in these repos, and if they aren't built, you'll get errors +DEV_DIR="$WORKSPACE/dev" +TEAM_DIR="$WORKSPACE/kibana-team" + +cd "$KIBANA_DIR" +origin=$(git remote get-url origin || true) +GIT_PREFIX="git@github.com:" +if [[ "$origin" == "https"* ]]; then + GIT_PREFIX="https://github.com/" +fi + +mkdir -p "$WORKSPACE" +cd "$WORKSPACE" + +if [[ ! -d "$NVM_DIR" ]]; then + echo "Installing a separate copy of nvm" + git clone https://github.com/nvm-sh/nvm.git "$NVM_DIR" + cd "$NVM_DIR" + git checkout "$(git describe --abbrev=0 --tags --match "v[0-9]*" "$(git rev-list --tags --max-count=1)")" + cd "$WORKSPACE" +fi +source "$NVM_DIR/nvm.sh" + +if [[ ! -d "$DOCS_DIR" ]]; then + echo "Cloning docs.elastic.dev repo..." + git clone --depth 1 "${GIT_PREFIX}elastic/docs.elastic.dev.git" +else + cd "$DOCS_DIR" + git pull + cd "$WORKSPACE" +fi + +if [[ ! -d "$DEV_DIR" ]]; then + echo "Cloning dev repo..." + git clone --depth 1 "${GIT_PREFIX}elastic/dev.git" +else + cd "$DEV_DIR" + git pull + cd "$WORKSPACE" +fi + +if [[ ! -d "$TEAM_DIR" ]]; then + echo "Cloning kibana-team repo..." + git clone --depth 1 "${GIT_PREFIX}elastic/kibana-team.git" +else + cd "$TEAM_DIR" + git pull + cd "$WORKSPACE" +fi + +# The minimum sources required to build kibana docs +cat << EOF > "$DOCS_DIR/sources-dev.json" +{ + "sources": [ + { + "type": "file", + "location": "$KIBANA_DIR" + }, + { + "type": "file", + "location": "$DEV_DIR" + }, + { + "type": "file", + "location": "$TEAM_DIR" + } + ] +} +EOF + +cd "$DOCS_DIR" +nvm install + +if ! which yarn; then + npm install -g yarn +fi + +yarn + +if [[ ! -d .docsmobile ]]; then + yarn init-docs +fi + +echo "" +echo "The docs.elastic.dev project is located at:" +echo "$DOCS_DIR" +echo "" + +if [[ "${1:-}" ]]; then + yarn "$@" +else + yarn dev +fi From 642290b0f11a6647aef0170648b1a24da712ba56 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 20 May 2022 15:11:15 -0600 Subject: [PATCH 060/120] [maps] convert ESPewPewSource to typescript (#132656) * [maps] convert ESPewPewSource to typescript * move @ts-expect-error moved by fix --- .../security/create_layer_descriptors.ts | 2 - ...ew_pew_source.js => es_pew_pew_source.tsx} | 102 ++++++++++++------ .../es_pew_pew_source/{index.js => index.ts} | 0 .../point_2_point_layer_wizard.tsx | 9 +- 4 files changed, 73 insertions(+), 40 deletions(-) rename x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/{es_pew_pew_source.js => es_pew_pew_source.tsx} (67%) rename x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/{index.js => index.ts} (100%) diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.ts b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.ts index 5792d861f6f5c..f295464126c96 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.ts +++ b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.ts @@ -24,9 +24,7 @@ import { } from '../../../../../../common/constants'; import { GeoJsonVectorLayer } from '../../../vector_layer'; import { VectorStyle } from '../../../../styles/vector/vector_style'; -// @ts-ignore import { ESSearchSource } from '../../../../sources/es_search_source'; -// @ts-ignore import { ESPewPewSource } from '../../../../sources/es_pew_pew_source'; import { getDefaultDynamicProperties } from '../../../../styles/vector/vector_style_defaults'; import { APM_INDEX_PATTERN_TITLE } from '../observability'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.tsx similarity index 67% rename from x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.tsx index a38c769205304..910181d6a2868 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.tsx @@ -8,17 +8,35 @@ import React from 'react'; import turfBbox from '@turf/bbox'; import { multiPoint } from '@turf/helpers'; +import { Adapters } from '@kbn/inspector-plugin/common/adapters'; +import { type Filter, buildExistsFilter } from '@kbn/es-query'; +import { lastValueFrom } from 'rxjs'; +import type { + AggregationsGeoBoundsAggregate, + LatLonGeoLocation, + TopLeftBottomRightGeoBounds, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; +// @ts-expect-error +import { UpdateSourceEditor } from './update_source_editor'; import { SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; import { getDataSourceLabel, getDataViewLabel } from '../../../../common/i18n_getters'; +// @ts-expect-error import { convertToLines } from './convert_to_lines'; import { AbstractESAggSource } from '../es_agg_source'; import { registerSource } from '../source_registry'; import { turfBboxToBounds } from '../../../../common/elasticsearch_util'; import { DataRequestAbortError } from '../../util/data_request'; import { makePublicExecutionContext } from '../../../util'; +import { SourceEditorArgs } from '../source'; +import { + ESPewPewSourceDescriptor, + MapExtent, + VectorSourceRequestMeta, +} from '../../../../common/descriptor_types'; +import { isValidStringConfig } from '../../util/valid_string_config'; +import { BoundsRequestMeta, GeoJsonWithMeta } from '../vector_source'; const MAX_GEOTILE_LEVEL = 29; @@ -27,20 +45,30 @@ export const sourceTitle = i18n.translate('xpack.maps.source.pewPewTitle', { }); export class ESPewPewSource extends AbstractESAggSource { - static type = SOURCE_TYPES.ES_PEW_PEW; + readonly _descriptor: ESPewPewSourceDescriptor; - static createDescriptor(descriptor) { + static createDescriptor(descriptor: Partial): ESPewPewSourceDescriptor { const normalizedDescriptor = AbstractESAggSource.createDescriptor(descriptor); + if (!isValidStringConfig(descriptor.sourceGeoField)) { + throw new Error('Cannot create ESPewPewSourceDescriptor, sourceGeoField is not provided'); + } + if (!isValidStringConfig(descriptor.destGeoField)) { + throw new Error('Cannot create ESPewPewSourceDescriptor, destGeoField is not provided'); + } return { ...normalizedDescriptor, - type: ESPewPewSource.type, - indexPatternId: descriptor.indexPatternId, - sourceGeoField: descriptor.sourceGeoField, - destGeoField: descriptor.destGeoField, + type: SOURCE_TYPES.ES_PEW_PEW, + sourceGeoField: descriptor.sourceGeoField!, + destGeoField: descriptor.destGeoField!, }; } - renderSourceSettingsEditor({ onChange }) { + constructor(descriptor: ESPewPewSourceDescriptor) { + super(descriptor); + this._descriptor = descriptor; + } + + renderSourceSettingsEditor({ onChange }: SourceEditorArgs) { return ( void) => void, + isRequestStillActive: () => boolean, + inspectorAdapters: Adapters + ): Promise { const indexPattern = await this.getIndexPattern(); const searchSource = await this.makeSearchSource(searchFilters, 0); searchSource.setField('trackTotalHits', false); @@ -151,14 +179,10 @@ export class ESPewPewSource extends AbstractESAggSource { // Some underlying indices may not contain geo fields // Filter out documents without geo fields to avoid shard failures for those indices searchSource.setField('filter', [ - ...searchSource.getField('filter'), + ...(searchSource.getField('filter') as Filter[]), // destGeoField exists ensured by buffer filter // so only need additional check for sourceGeoField - { - exists: { - field: this._descriptor.sourceGeoField, - }, - }, + buildExistsFilter({ name: this._descriptor.sourceGeoField, type: 'geo_point' }, indexPattern), ]); const esResponse = await this._runEsQuery({ @@ -188,7 +212,10 @@ export class ESPewPewSource extends AbstractESAggSource { return this._descriptor.destGeoField; } - async getBoundsForFilters(boundsFilters, registerCancelCallback) { + async getBoundsForFilters( + boundsFilters: BoundsRequestMeta, + registerCancelCallback: (callback: () => void) => void + ): Promise { const searchSource = await this.makeSearchSource(boundsFilters, 0); searchSource.setField('trackTotalHits', false); searchSource.setField('aggs', { @@ -208,31 +235,36 @@ export class ESPewPewSource extends AbstractESAggSource { try { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); - const { rawResponse: esResp } = await searchSource - .fetch$({ + const { rawResponse: esResp } = await lastValueFrom( + searchSource.fetch$({ abortSignal: abortController.signal, legacyHitsTotal: false, executionContext: makePublicExecutionContext('es_pew_pew_source:bounds'), }) - .toPromise(); - if (esResp.aggregations.destFitToBounds.bounds) { + ); + const destBounds = (esResp.aggregations?.destFitToBounds as AggregationsGeoBoundsAggregate) + .bounds as TopLeftBottomRightGeoBounds; + if (destBounds) { corners.push([ - esResp.aggregations.destFitToBounds.bounds.top_left.lon, - esResp.aggregations.destFitToBounds.bounds.top_left.lat, + (destBounds.top_left as LatLonGeoLocation).lon, + (destBounds.top_left as LatLonGeoLocation).lat, ]); corners.push([ - esResp.aggregations.destFitToBounds.bounds.bottom_right.lon, - esResp.aggregations.destFitToBounds.bounds.bottom_right.lat, + (destBounds.bottom_right as LatLonGeoLocation).lon, + (destBounds.bottom_right as LatLonGeoLocation).lat, ]); } - if (esResp.aggregations.sourceFitToBounds.bounds) { + const sourceBounds = ( + esResp.aggregations?.sourceFitToBounds as AggregationsGeoBoundsAggregate + ).bounds as TopLeftBottomRightGeoBounds; + if (sourceBounds) { corners.push([ - esResp.aggregations.sourceFitToBounds.bounds.top_left.lon, - esResp.aggregations.sourceFitToBounds.bounds.top_left.lat, + (sourceBounds.top_left as LatLonGeoLocation).lon, + (sourceBounds.top_left as LatLonGeoLocation).lat, ]); corners.push([ - esResp.aggregations.sourceFitToBounds.bounds.bottom_right.lon, - esResp.aggregations.sourceFitToBounds.bounds.bottom_right.lat, + (sourceBounds.bottom_right as LatLonGeoLocation).lon, + (sourceBounds.bottom_right as LatLonGeoLocation).lat, ]); } } catch (error) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/index.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/index.ts similarity index 100% rename from x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/index.js rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/index.ts diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx index 37ecbfdebab11..aa128e3c7d8ff 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; import { GeoJsonVectorLayer } from '../../layers/vector_layer'; -// @ts-ignore import { ESPewPewSource, sourceTitle } from './es_pew_pew_source'; import { VectorStyle } from '../../styles/vector/vector_style'; import { @@ -24,7 +23,11 @@ import { NUMERICAL_COLOR_PALETTES } from '../../styles/color_palettes'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; import { LayerWizard, RenderWizardArguments } from '../../layers'; -import { ColorDynamicOptions, SizeDynamicOptions } from '../../../../common/descriptor_types'; +import { + ColorDynamicOptions, + ESPewPewSourceDescriptor, + SizeDynamicOptions, +} from '../../../../common/descriptor_types'; import { Point2PointLayerIcon } from '../../layers/wizards/icons/point_2_point_layer_icon'; export const point2PointLayerWizardConfig: LayerWizard = { @@ -36,7 +39,7 @@ export const point2PointLayerWizardConfig: LayerWizard = { }), icon: Point2PointLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { - const onSourceConfigChange = (sourceConfig: unknown) => { + const onSourceConfigChange = (sourceConfig: Partial) => { if (!sourceConfig) { previewLayers([]); return; From 51ae0208dc381c82d8ba224f155b7ced2ba73d1b Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 20 May 2022 14:30:36 -0700 Subject: [PATCH 061/120] Upgrade EUI to v55.1.3 (#132451) * Upgrade EUI to 55.1.3 backport * [Deprecation] Remove `watchedItemProps` from EuiContextMenu usage - should no longer be necessary * Update snapshots with new data-popover attr * Fix failing FTR test - Now that EuiContextMenu focus is restored correctly, there is a tooltip around the popover toggle that's blocking an above item that the test wants to click - swapping the order so that the tooltip does not block the clicked item should work * Fix 2nd maps FTR test with blocking tooltip Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- src/dev/license_checker/config.ts | 2 +- .../public/components/field_picker/field_search.tsx | 1 - .../saved_objects/public/finder/saved_object_finder.tsx | 6 +----- .../footer/settings/__snapshots__/settings.test.tsx.snap | 6 +++++- .../markdown_editor/plugins/lens/saved_objects_finder.tsx | 6 +----- .../lens/public/indexpattern_datasource/datapanel.tsx | 1 - .../application/components/anomalies_table/links_menu.tsx | 6 +----- .../edit_role/spaces_popover_list/spaces_popover_list.tsx | 1 - .../spaces/public/nav_control/components/spaces_menu.tsx | 1 - .../test/functional/apps/maps/group1/layer_visibility.js | 2 ++ x-pack/test/functional/apps/maps/group1/sample_data.js | 2 +- yarn.lock | 8 ++++---- 13 files changed, 17 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 36a1cd9a5ffad..e5fffb5b3a394 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.2.0-canary.2", "@elastic/ems-client": "8.3.2", - "@elastic/eui": "55.1.2", + "@elastic/eui": "55.1.3", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.1", diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index f10fb0231352d..66e2664b2e8b4 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -77,6 +77,6 @@ export const LICENSE_OVERRIDES = { 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint '@elastic/ems-client@8.3.2': ['Elastic License 2.0'], - '@elastic/eui@55.1.2': ['SSPL-1.0 OR Elastic License 2.0'], + '@elastic/eui@55.1.3': ['SSPL-1.0 OR Elastic License 2.0'], 'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry }; diff --git a/src/plugins/presentation_util/public/components/field_picker/field_search.tsx b/src/plugins/presentation_util/public/components/field_picker/field_search.tsx index 3f3dcfdef5c8b..d3307f71988f1 100644 --- a/src/plugins/presentation_util/public/components/field_picker/field_search.tsx +++ b/src/plugins/presentation_util/public/components/field_picker/field_search.tsx @@ -103,7 +103,6 @@ export function FieldSearch({ })} ( } > - + {this.props.showFilter && ( ( can navigate Autoplay Settings 1`] = ` aria-live="off" aria-modal="true" class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--noShadow euiPopover__panel euiPopover__panel--top" + data-popover-panel="true" role="dialog" style="top: -16px; left: -22px; will-change: transform, opacity; z-index: 2000;" tabindex="0" @@ -108,6 +109,7 @@ exports[` can navigate Autoplay Settings 2`] = ` aria-live="off" aria-modal="true" class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--noShadow euiPopover__panel euiPopover__panel--top euiPopover__panel-isOpen" + data-popover-panel="true" role="dialog" style="top: -16px; left: -22px; z-index: 2000;" tabindex="0" @@ -359,6 +361,7 @@ exports[` can navigate Toolbar Settings, closes when activated 1`] = aria-live="off" aria-modal="true" class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--noShadow euiPopover__panel euiPopover__panel--top" + data-popover-panel="true" role="dialog" style="top: -16px; left: -22px; will-change: transform, opacity; z-index: 2000;" tabindex="0" @@ -457,6 +460,7 @@ exports[` can navigate Toolbar Settings, closes when activated 2`] = aria-live="off" aria-modal="true" class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--noShadow euiPopover__panel euiPopover__panel--top euiPopover__panel-isOpen" + data-popover-panel="true" role="dialog" style="top: -16px; left: -22px; z-index: 2000;" tabindex="0" @@ -631,4 +635,4 @@ exports[` can navigate Toolbar Settings, closes when activated 2`] = `; -exports[` can navigate Toolbar Settings, closes when activated 3`] = `"

You are in a dialog. To close this dialog, hit escape.

Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"

You are in a dialog. To close this dialog, hit escape.

Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx index 7d7ce5d638489..3f2b3c2420629 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx @@ -394,10 +394,7 @@ export class SavedObjectFinderUi extends React.Component< } > - + {this.props.showFilter && ( ( ( { ]); return ( - + ); }; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx index 7e1c5eb545a28..9ddc698ef2c2b 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx @@ -73,7 +73,6 @@ export class SpacesPopoverList extends Component { title: i18n.translate('xpack.security.management.editRole.spacesPopoverList.popoverTitle', { defaultMessage: 'Spaces', }), - watchedItemProps: ['data-search-term'], }; if (this.props.spaces.length >= SPACE_SEARCH_COUNT_THRESHOLD) { diff --git a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx index 6e268d4711bb5..6f5158423ca51 100644 --- a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx +++ b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx @@ -69,7 +69,6 @@ class SpacesMenuUI extends Component { id: 'xpack.spaces.navControl.spacesMenu.changeCurrentSpaceTitle', defaultMessage: 'Change current space', }), - watchedItemProps: ['data-search-term'], }; if (this.props.spaces.length >= SPACE_SEARCH_COUNT_THRESHOLD) { diff --git a/x-pack/test/functional/apps/maps/group1/layer_visibility.js b/x-pack/test/functional/apps/maps/group1/layer_visibility.js index cf6051cde8be7..a9bbefbff86ca 100644 --- a/x-pack/test/functional/apps/maps/group1/layer_visibility.js +++ b/x-pack/test/functional/apps/maps/group1/layer_visibility.js @@ -10,6 +10,7 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); const inspector = getService('inspector'); + const testSubjects = getService('testSubjects'); const security = getService('security'); describe('layer visibility', () => { @@ -31,6 +32,7 @@ export default function ({ getPageObjects, getService }) { it('should fetch layer data when layer is made visible', async () => { await PageObjects.maps.toggleLayerVisibility('logstash'); + await testSubjects.click('mapLayerTOC'); // Tooltip blocks clicks otherwise const hits = await PageObjects.maps.getHits(); expect(hits).to.equal('5'); }); diff --git a/x-pack/test/functional/apps/maps/group1/sample_data.js b/x-pack/test/functional/apps/maps/group1/sample_data.js index cf8bd4c85cf26..62df1d3859a45 100644 --- a/x-pack/test/functional/apps/maps/group1/sample_data.js +++ b/x-pack/test/functional/apps/maps/group1/sample_data.js @@ -165,8 +165,8 @@ export default function ({ getPageObjects, getService, updateBaselines }) { describe('web logs', () => { before(async () => { await PageObjects.maps.loadSavedMap('[Logs] Total Requests and Bytes'); - await PageObjects.maps.toggleLayerVisibility('Road map - desaturated'); await PageObjects.maps.toggleLayerVisibility('Total Requests by Destination'); + await PageObjects.maps.toggleLayerVisibility('Road map - desaturated'); await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); await PageObjects.maps.enterFullScreen(); await PageObjects.maps.closeLegend(); diff --git a/yarn.lock b/yarn.lock index 88a23a226d0e8..35c60d9444f32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1503,10 +1503,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@55.1.2": - version "55.1.2" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-55.1.2.tgz#dd0b42f5b26c5800d6a9cb2d4c2fe1afce9d3f07" - integrity sha512-wwZz5KxMIMFlqEsoCRiQBJDc4CrluS1d0sCOmQ5lhIzKhYc91MdxnqCk2i6YkhL4sSDf2Y9KAEuMXa+uweOWUA== +"@elastic/eui@55.1.3": + version "55.1.3" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-55.1.3.tgz#976142b88156caab2ce896102a1e35fecdaa2647" + integrity sha512-Hf6eN9YKOKAQMMS9OV5pHLUkzpKKAxGYNVSfc/KK7hN9BlhlHH4OaZIQP3Psgf5GKoqhZrldT/N65hujk3rlLA== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" From 788dd2e718bb4af112c43319cffcb900281d7073 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Fri, 20 May 2022 16:02:05 -0600 Subject: [PATCH 062/120] [Security Solution] Fixes sorting and tooltips on columns for non-ECS fields that are only one level deep (#132570) ## [Security Solution] Fixes sorting and tooltips on columns for non-ECS fields that are only one level deep This PR fixes , an issue where Timeline columns for non-ECS fields that are only one level deep couldn't be sorted, and displayed incomplete metadata in the column's tooltip. ### Before ![test_field_1_actual_tooltip](https://user-images.githubusercontent.com/4459398/169208299-51d9296a-15e1-4eb0-bc31-a0df6a63f0c5.png) _Before: The column is **not** sortable, and the tooltip displays incomplete metadata_ ### After ![after](https://user-images.githubusercontent.com/4459398/169414767-7274a795-015f-4805-8c3f-b233ead994ea.png) _After: The column is sortable, and the tooltip displays the expected metadata_ ### Desk testing See the _Steps to reproduce_ section of for testing details. --- .../body/column_headers/helpers.test.ts | 232 +++++++++++++++++- .../timeline/body/column_headers/helpers.ts | 27 +- .../components/t_grid/body/helpers.test.tsx | 2 +- 3 files changed, 251 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts index 84cc6e60d928c..2a23b5e993637 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts @@ -6,11 +6,12 @@ */ import { mockBrowserFields } from '../../../../../common/containers/source/mock'; - -import { defaultHeaders } from './default_headers'; -import { getColumnWidthFromType, getColumnHeaders } from './helpers'; -import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants'; import '../../../../../common/mock/match_media'; +import { BrowserFields } from '../../../../../../common/search_strategy'; +import { ColumnHeaderOptions } from '../../../../../../common/types'; +import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants'; +import { defaultHeaders } from './default_headers'; +import { getColumnWidthFromType, getColumnHeaders, getRootCategory } from './helpers'; describe('helpers', () => { describe('getColumnWidthFromType', () => { @@ -23,6 +24,32 @@ describe('helpers', () => { }); }); + describe('getRootCategory', () => { + const baseFields = ['@timestamp', '_id', 'message']; + + baseFields.forEach((field) => { + test(`it returns the 'base' category for the ${field} field`, () => { + expect( + getRootCategory({ + field, + browserFields: mockBrowserFields, + }) + ).toEqual('base'); + }); + }); + + test(`it echos the field name for a field that's NOT in the base category`, () => { + const field = 'test_field_1'; + + expect( + getRootCategory({ + field, + browserFields: mockBrowserFields, + }) + ).toEqual(field); + }); + }); + describe('getColumnHeaders', () => { test('should return a full object of ColumnHeader from the default header', () => { const expectedData = [ @@ -80,5 +107,202 @@ describe('helpers', () => { ); expect(getColumnHeaders(mockHeader, mockBrowserFields)).toEqual(expectedData); }); + + test('it should return the expected metadata for the `_id` field, which is one level deep, and belongs to the `base` category', () => { + const headers: ColumnHeaderOptions[] = [ + { + columnHeaderType: 'not-filtered', + id: '_id', + initialWidth: 180, + }, + ]; + + expect(getColumnHeaders(headers, mockBrowserFields)).toEqual([ + { + aggregatable: false, + category: 'base', + columnHeaderType: 'not-filtered', + description: 'Each document has an _id that uniquely identifies it', + esTypes: [], + example: 'Y-6TfmcB0WOhS6qyMv3s', + id: '_id', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + initialWidth: 180, + name: '_id', + searchable: true, + type: 'string', + }, + ]); + }); + + test('it should return the expected metadata for a field one level deep that does NOT belong to the `base` category', () => { + const headers: ColumnHeaderOptions[] = [ + { + columnHeaderType: 'not-filtered', + id: 'test_field_1', // one level deep, but does NOT belong to the `base` category + initialWidth: 180, + }, + ]; + + const oneLevelDeep: BrowserFields = { + test_field_1: { + fields: { + test_field_1: { + aggregatable: true, + category: 'test_field_1', + esTypes: ['keyword'], + format: 'string', + indexes: [ + '-*elastic-cloud-logs-*', + '.alerts-security.alerts-default', + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'traces-apm*', + 'winlogbeat-*', + ], + name: 'test_field_1', + readFromDocValues: true, + searchable: true, + type: 'string', + }, + }, + }, + }; + + expect(getColumnHeaders(headers, oneLevelDeep)).toEqual([ + { + aggregatable: true, + category: 'test_field_1', + columnHeaderType: 'not-filtered', + esTypes: ['keyword'], + format: 'string', + id: 'test_field_1', + indexes: [ + '-*elastic-cloud-logs-*', + '.alerts-security.alerts-default', + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'traces-apm*', + 'winlogbeat-*', + ], + initialWidth: 180, + name: 'test_field_1', + readFromDocValues: true, + searchable: true, + type: 'string', + }, + ]); + }); + + test('it should return the expected metadata for a field that is more than one level deep', () => { + const headers: ColumnHeaderOptions[] = [ + { + columnHeaderType: 'not-filtered', + id: 'foo.bar', // two levels deep + initialWidth: 180, + }, + ]; + + const twoLevelsDeep: BrowserFields = { + foo: { + fields: { + 'foo.bar': { + aggregatable: true, + category: 'foo', + esTypes: ['keyword'], + format: 'string', + indexes: [ + '-*elastic-cloud-logs-*', + '.alerts-security.alerts-default', + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'traces-apm*', + 'winlogbeat-*', + ], + name: 'foo.bar', + readFromDocValues: true, + searchable: true, + type: 'string', + }, + }, + }, + }; + + expect(getColumnHeaders(headers, twoLevelsDeep)).toEqual([ + { + aggregatable: true, + category: 'foo', + columnHeaderType: 'not-filtered', + esTypes: ['keyword'], + format: 'string', + id: 'foo.bar', + indexes: [ + '-*elastic-cloud-logs-*', + '.alerts-security.alerts-default', + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'traces-apm*', + 'winlogbeat-*', + ], + initialWidth: 180, + name: 'foo.bar', + readFromDocValues: true, + searchable: true, + type: 'string', + }, + ]); + }); + + test('it should return the expected metadata for an UNKNOWN field one level deep', () => { + const headers: ColumnHeaderOptions[] = [ + { + columnHeaderType: 'not-filtered', + id: 'unknown', // one level deep, but not contained in the `BrowserFields` + initialWidth: 180, + }, + ]; + + expect(getColumnHeaders(headers, mockBrowserFields)).toEqual([ + { + columnHeaderType: 'not-filtered', + id: 'unknown', + initialWidth: 180, + }, + ]); + }); + + test('it should return the expected metadata for an UNKNOWN field that is more than one level deep', () => { + const headers: ColumnHeaderOptions[] = [ + { + columnHeaderType: 'not-filtered', + id: 'unknown.more.than.one.level', // more than one level deep, and not contained in the `BrowserFields` + initialWidth: 180, + }, + ]; + + expect(getColumnHeaders(headers, mockBrowserFields)).toEqual([ + { + columnHeaderType: 'not-filtered', + id: 'unknown.more.than.one.level', + initialWidth: 180, + }, + ]); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts index b1ea4899615a6..1779c39ce7b31 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts @@ -5,12 +5,28 @@ * 2.0. */ -import { get } from 'lodash/fp'; +import { has, get } from 'lodash/fp'; import { ColumnHeaderOptions } from '../../../../../../common/types'; import { BrowserFields } from '../../../../../common/containers/source'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants'; +/** + * Returns the root category for fields that are only one level, e.g. `_id` or `test_field_1` + * + * The `base` category will be returned for fields that are members of `base`, + * e.g. the `@timestamp`, `_id`, and `message` fields. + * + * The field name will be echoed-back for all other fields, e.g. `test_field_1` + */ +export const getRootCategory = ({ + browserFields, + field, +}: { + browserFields: BrowserFields; + field: string; +}): string => (has(`base.fields.${field}`, browserFields) ? 'base' : field); + /** Enriches the column headers with field details from the specified browserFields */ export const getColumnHeaders = ( headers: ColumnHeaderOptions[], @@ -19,13 +35,14 @@ export const getColumnHeaders = ( return headers ? headers.map((header) => { const splitHeader = header.id.split('.'); // source.geo.city_name -> [source, geo, city_name] + const category = + splitHeader.length > 1 + ? splitHeader[0] + : getRootCategory({ field: header.id, browserFields }); return { ...header, - ...get( - [splitHeader.length > 1 ? splitHeader[0] : 'base', 'fields', header.id], - browserFields - ), + ...get([category, 'fields', header.id], browserFields), }; }) : []; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx index 444ba878d6709..253c3ca78b487 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx @@ -138,7 +138,7 @@ describe('helpers', () => { ]); }); - test('it defaults to a `columnType` of empty string when a column does NOT has a corresponding entry in `columnHeaders`', () => { + test('it defaults to a `columnType` of empty string when a column does NOT have a corresponding entry in `columnHeaders`', () => { const withUnknownColumn: Array<{ id: string; direction: 'asc' | 'desc'; From fb1eeb0945e25928a2516133055f048ff098166f Mon Sep 17 00:00:00 2001 From: Georgii Gorbachev Date: Sat, 21 May 2022 00:21:53 +0200 Subject: [PATCH 063/120] [Security Solution][Detections] Add new fields to the rule model: Related Integrations, Required Fields, and Setup (#132409) **Addresses partially:** https://github.com/elastic/security-team/issues/2083, https://github.com/elastic/security-team/issues/558, https://github.com/elastic/security-team/issues/2856, https://github.com/elastic/security-team/issues/1801 (internal tickets) ## Summary **TL;DR:** With this PR, it's now possible to specify `related_integrations`, `required_fields`, and `setup` fields in prebuilt rules in https://github.com/elastic/detection-rules. They are returned within rules in the API responses. This PR: - Adds 3 new fields to the model of Security detection rules. These fields are common to all of the rule types we have. - **Related Integrations**. It's a list of Fleet integrations associated with a given rule. It's assumed that if the user installs them, the rule might start to work properly because it will start receiving source events potentially matching the rule's query. - **Required Fields**. It's a list of event fields that must be present in the source indices of a given rule. - **Setup Guide**. It's any instructions for the user for setting up their environment in order to start receiving source events for a given rule. It's a text. Markdown is supported. It's similar to the Investigation Guide that we show on the Details page. - Adjusts API endpoints accordingly: - These fields are for prebuilt rules only and are supposed to be read-only in the UI. - Specifying these fields in the request parameters of the create/update/patch rule API endpoints is not supported. - These fields are returned in all responses that contain rules. If they are missing in a rule, default values are returned (empty array, empty string). - When duplicating a prebuilt rule, these fields are being reset to their default value (empty array, empty string). - Export/Import is supported. Edge case / supported hack: it's possible to specify these fields manually in a ndjson doc and import with a rule. - The fields are being copied to `kibana.alert.rule.parameters` field of an alert document, which is mapped as a flattened field type. No special handling for the new fields was needed there. - Adjusts tests accordingly. ## Related Integrations Example (part of a rule returned from the API): ```json { "related_integrations": [ { "package": "windows", "version": "1.5.x" }, { "package": "azure", "integration": "activitylogs", "version": "~1.1.6" } ], } ``` Schema: ```ts /** * Related integration is a potential dependency of a rule. It's assumed that if the user installs * one of the related integrations of a rule, the rule might start to work properly because it will * have source events (generated by this integration) potentially matching the rule's query. * * NOTE: Proper work is not guaranteed, because a related integration, if installed, can be * configured differently or generate data that is not necessarily relevant for this rule. * * Related integration is a combination of a Fleet package and (optionally) one of the * package's "integrations" that this package contains. It is represented by 3 properties: * * - `package`: name of the package (required, unique id) * - `version`: version of the package (required, semver-compatible) * - `integration`: name of the integration of this package (optional, id within the package) * * There are Fleet packages like `windows` that contain only one integration; in this case, * `integration` should be unspecified. There are also packages like `aws` and `azure` that contain * several integrations; in this case, `integration` should be specified. * * @example * const x: RelatedIntegration = { * package: 'windows', * version: '1.5.x', * }; * * @example * const x: RelatedIntegration = { * package: 'azure', * version: '~1.1.6', * integration: 'activitylogs', * }; */ export type RelatedIntegration = t.TypeOf; export const RelatedIntegration = t.exact( t.intersection([ t.type({ package: NonEmptyString, version: NonEmptyString, }), t.partial({ integration: NonEmptyString, }), ]) ); ``` ## Required Fields Example (part of a rule returned from the API): ```json { "required_fields": [ { "name": "event.action", "type": "keyword", "ecs": true }, { "name": "event.code", "type": "keyword", "ecs": true }, { "name": "winlog.event_data.AttributeLDAPDisplayName", "type": "keyword", "ecs": false } ], } ``` Schema: ```ts /** * Almost all types of Security rules check source event documents for a match to some kind of * query or filter. If a document has certain field with certain values, then it's a match and * the rule will generate an alert. * * Required field is an event field that must be present in the source indices of a given rule. * * @example * const standardEcsField: RequiredField = { * name: 'event.action', * type: 'keyword', * ecs: true, * }; * * @example * const nonEcsField: RequiredField = { * name: 'winlog.event_data.AttributeLDAPDisplayName', * type: 'keyword', * ecs: false, * }; */ export type RequiredField = t.TypeOf; export const RequiredField = t.exact( t.type({ name: NonEmptyString, type: NonEmptyString, ecs: t.boolean, }) ); ``` ## Setup Guide Example (part of a rule returned from the API): ```json { "setup": "## Config\n\nThe 'PowerShell Script Block Logging' logging policy must be enabled.\nSteps to implement the logging policy with with Advanced Audit Configuration:\n\n```\nComputer Configuration > \nAdministrative Templates > \nWindows PowerShell > \nTurn on PowerShell Script Block Logging (Enable)\n```\n\nSteps to implement the logging policy via registry:\n\n```\nreg add \"hklm\\SOFTWARE\\Policies\\Microsoft\\Windows\\PowerShell\\ScriptBlockLogging\" /v EnableScriptBlockLogging /t REG_DWORD /d 1\n```\n", } ``` Schema: ```ts /** * Any instructions for the user for setting up their environment in order to start receiving * source events for a given rule. * * It's a multiline text. Markdown is supported. */ export type SetupGuide = t.TypeOf; export const SetupGuide = t.string; ``` ## Details on the schema This PR adjusts all the 6 rule schemas we have: 1. Alerting Framework rule `params` schema: - `security_solution/server/lib/detection_engine/schemas/rule_schemas.ts` - `security_solution/server/lib/detection_engine/schemas/rule_converters.ts` 2. HTTP API main old schema: - `security_solution/common/detection_engine/schemas/response/rules_schema.ts` 3. HTTP API main new schema: - `security_solution/common/detection_engine/schemas/request/rule_schemas.ts` 4. Prebuilt rule schema: - `security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts` 5. Import rule schema: - `security_solution/common/detection_engine/schemas/request/import_rules_schema.ts` 6. Rule schema used on the frontend side: - `security_solution/public/detections/containers/detection_engine/rules/types.ts` Names of the fields on the HTTP API level: - `related_integrations` - `required_fields` - `setup` Names of the fields on the Alerting Framework level: - `params.relatedIntegrations` - `params.requiredFields` - `params.setup` ## Next steps - Create a new endpoint for returning installed Fleet integrations (gonna be a separate PR). - Rebase https://github.com/elastic/kibana/pull/131475 on top of this PR after merge. - Cover the new fields with dedicated tests (gonna be a separate PR). - Update API docs (gonna be a separate PR). - Address the tech debt of having 6 different schemas (gonna create a ticket for that). ### Checklist - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [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 --- .../detection_engine/schemas/common/index.ts | 1 + .../schemas/common/rule_params.ts | 146 +++++++ .../request/add_prepackaged_rules_schema.ts | 8 +- .../schemas/request/import_rules_schema.ts | 8 +- .../schemas/request/patch_rules_schema.ts | 2 +- .../schemas/request/rule_schemas.ts | 11 + .../schemas/response/rules_schema.mocks.ts | 6 + .../schemas/response/rules_schema.ts | 6 + .../security_solution/cypress/objects/rule.ts | 9 +- .../containers/detection_engine/rules/mock.ts | 9 + .../detection_engine/rules/types.ts | 6 + .../detection_engine/rules/use_rule.test.tsx | 3 + .../rules/use_rule_with_fallback.test.tsx | 3 + .../rules/all/__mocks__/mock.ts | 6 + .../schedule_notification_actions.test.ts | 3 + ...dule_throttle_notification_actions.test.ts | 3 + .../routes/__mocks__/utils.ts | 3 + .../routes/rules/utils/import_rules_utils.ts | 9 + .../routes/rules/validate.test.ts | 3 + .../factories/utils/build_alert.test.ts | 6 + .../rules/create_rules.mock.ts | 9 + .../detection_engine/rules/create_rules.ts | 6 + .../rules/duplicate_rule.test.ts | 391 +++++++++++++----- .../detection_engine/rules/duplicate_rule.ts | 16 +- .../rules/get_export_all.test.ts | 3 + .../rules/get_export_by_object_ids.test.ts | 6 + .../rules/install_prepacked_rules.ts | 6 + .../lib/detection_engine/rules/patch_rules.ts | 9 + .../lib/detection_engine/rules/types.ts | 9 + .../rules/update_prepacked_rules.ts | 9 + .../detection_engine/rules/update_rules.ts | 3 + .../lib/detection_engine/rules/utils.test.ts | 9 + .../lib/detection_engine/rules/utils.ts | 8 +- .../schemas/rule_converters.ts | 6 + .../schemas/rule_schemas.mock.ts | 3 + .../detection_engine/schemas/rule_schemas.ts | 8 +- .../signals/__mocks__/es_results.ts | 9 + .../basic/tests/create_rules.ts | 3 + .../group1/create_rules.ts | 3 + .../group6/alerts/alerts_compatibility.ts | 6 + .../utils/get_complex_rule_output.ts | 3 + .../utils/get_simple_rule_output.ts | 3 + 42 files changed, 660 insertions(+), 119 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_params.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts index 4ef5d6178d5a5..615eb3f05876e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts @@ -6,4 +6,5 @@ */ export * from './rule_monitoring'; +export * from './rule_params'; export * from './schemas'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_params.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_params.ts new file mode 100644 index 0000000000000..b9588a26bb35b --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_params.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; + +// ------------------------------------------------------------------------------------------------- +// Related integrations + +/** + * Related integration is a potential dependency of a rule. It's assumed that if the user installs + * one of the related integrations of a rule, the rule might start to work properly because it will + * have source events (generated by this integration) potentially matching the rule's query. + * + * NOTE: Proper work is not guaranteed, because a related integration, if installed, can be + * configured differently or generate data that is not necessarily relevant for this rule. + * + * Related integration is a combination of a Fleet package and (optionally) one of the + * package's "integrations" that this package contains. It is represented by 3 properties: + * + * - `package`: name of the package (required, unique id) + * - `version`: version of the package (required, semver-compatible) + * - `integration`: name of the integration of this package (optional, id within the package) + * + * There are Fleet packages like `windows` that contain only one integration; in this case, + * `integration` should be unspecified. There are also packages like `aws` and `azure` that contain + * several integrations; in this case, `integration` should be specified. + * + * @example + * const x: RelatedIntegration = { + * package: 'windows', + * version: '1.5.x', + * }; + * + * @example + * const x: RelatedIntegration = { + * package: 'azure', + * version: '~1.1.6', + * integration: 'activitylogs', + * }; + */ +export type RelatedIntegration = t.TypeOf; +export const RelatedIntegration = t.exact( + t.intersection([ + t.type({ + package: NonEmptyString, + version: NonEmptyString, + }), + t.partial({ + integration: NonEmptyString, + }), + ]) +); + +/** + * Array of related integrations. + * + * @example + * const x: RelatedIntegrationArray = [ + * { + * package: 'windows', + * version: '1.5.x', + * }, + * { + * package: 'azure', + * version: '~1.1.6', + * integration: 'activitylogs', + * }, + * ]; + */ +export type RelatedIntegrationArray = t.TypeOf; +export const RelatedIntegrationArray = t.array(RelatedIntegration); + +// ------------------------------------------------------------------------------------------------- +// Required fields + +/** + * Almost all types of Security rules check source event documents for a match to some kind of + * query or filter. If a document has certain field with certain values, then it's a match and + * the rule will generate an alert. + * + * Required field is an event field that must be present in the source indices of a given rule. + * + * @example + * const standardEcsField: RequiredField = { + * name: 'event.action', + * type: 'keyword', + * ecs: true, + * }; + * + * @example + * const nonEcsField: RequiredField = { + * name: 'winlog.event_data.AttributeLDAPDisplayName', + * type: 'keyword', + * ecs: false, + * }; + */ +export type RequiredField = t.TypeOf; +export const RequiredField = t.exact( + t.type({ + name: NonEmptyString, + type: NonEmptyString, + ecs: t.boolean, + }) +); + +/** + * Array of event fields that must be present in the source indices of a given rule. + * + * @example + * const x: RequiredFieldArray = [ + * { + * name: 'event.action', + * type: 'keyword', + * ecs: true, + * }, + * { + * name: 'event.code', + * type: 'keyword', + * ecs: true, + * }, + * { + * name: 'winlog.event_data.AttributeLDAPDisplayName', + * type: 'keyword', + * ecs: false, + * }, + * ]; + */ +export type RequiredFieldArray = t.TypeOf; +export const RequiredFieldArray = t.array(RequiredField); + +// ------------------------------------------------------------------------------------------------- +// Setup guide + +/** + * Any instructions for the user for setting up their environment in order to start receiving + * source events for a given rule. + * + * It's a multiline text. Markdown is supported. + */ +export type SetupGuide = t.TypeOf; +export const SetupGuide = t.string; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts index 618aee3379316..27ebf9a608ffa 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts @@ -72,7 +72,10 @@ import { Author, event_category_override, namespace, -} from '../common/schemas'; + RelatedIntegrationArray, + RequiredFieldArray, + SetupGuide, +} from '../common'; /** * Big differences between this schema and the createRulesSchema @@ -117,8 +120,11 @@ export const addPrepackagedRulesSchema = t.intersection([ meta, // defaults to "undefined" if not set during decode machine_learning_job_id, // defaults to "undefined" if not set during decode max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode + related_integrations: RelatedIntegrationArray, // defaults to "undefined" if not set during decode + required_fields: RequiredFieldArray, // defaults to "undefined" if not set during decode risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode rule_name_override, // defaults to "undefined" if not set during decode + setup: SetupGuide, // defaults to "undefined" if not set during decode severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts index 63c41e45e42d0..8cee4183d6ee7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts @@ -80,7 +80,10 @@ import { timestamp_override, Author, event_category_override, -} from '../common/schemas'; + RelatedIntegrationArray, + RequiredFieldArray, + SetupGuide, +} from '../common'; /** * Differences from this and the createRulesSchema are @@ -129,8 +132,11 @@ export const importRulesSchema = t.intersection([ meta, // defaults to "undefined" if not set during decode machine_learning_job_id, // defaults to "undefined" if not set during decode max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode + related_integrations: RelatedIntegrationArray, // defaults to "undefined" if not set during decode + required_fields: RequiredFieldArray, // defaults to "undefined" if not set during decode risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode rule_name_override, // defaults to "undefined" if not set during decode + setup: SetupGuide, // defaults to "undefined" if not set during decode severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts index 8c801e75af08c..6678681471b38 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts @@ -61,7 +61,7 @@ import { rule_name_override, timestamp_override, event_category_override, -} from '../common/schemas'; +} from '../common'; /** * All of the patch elements should default to undefined if not set diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 69a748c3bd95c..9aef9ac8f2651 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -67,6 +67,9 @@ import { created_by, namespace, ruleExecutionSummary, + RelatedIntegrationArray, + RequiredFieldArray, + SetupGuide, } from '../common'; export const createSchema = < @@ -412,6 +415,14 @@ const responseRequiredFields = { updated_by, created_at, created_by, + + // NOTE: For now, Related Integrations, Required Fields and Setup Guide are supported for prebuilt + // rules only. We don't want to allow users to edit these 3 fields via the API. If we added them + // to baseParams.defaultable, they would become a part of the request schema as optional fields. + // This is why we add them here, in order to add them only to the response schema. + related_integrations: RelatedIntegrationArray, + required_fields: RequiredFieldArray, + setup: SetupGuide, }; const responseOptionalFields = { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index 0642481b62a6a..eeaab6dc50021 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -68,6 +68,9 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem rule_id: 'query-rule-id', interval: '5m', exceptions_list: getListArrayMock(), + related_integrations: [], + required_fields: [], + setup: '', }); export const getRulesMlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { @@ -132,6 +135,9 @@ export const getThreatMatchingSchemaPartialMock = (enabled = false): Partial; diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 32c55e22ae7c9..de2de9bd78160 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -442,7 +442,9 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response severity, query, } = ruleResponse.body; - const rule = { + + // NOTE: Order of the properties in this object matters for the tests to work. + const rule: RulesSchema = { id, updated_at: updatedAt, updated_by: updatedBy, @@ -469,6 +471,9 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response version: 1, exceptions_list: [], immutable: false, + related_integrations: [], + required_fields: [], + setup: '', type: 'query', language: 'kuery', index: getIndexPatterns(), @@ -476,6 +481,8 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response throttle: 'no_actions', actions: [], }; + + // NOTE: Order of the properties in this object matters for the tests to work. const details = { exported_count: 1, exported_rules_count: 1, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts index 8c1737a4519a7..8a23cbf9e4318 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts @@ -38,6 +38,9 @@ export const savedRuleMock: Rule = { max_signals: 100, query: "user.email: 'root@elastic.co'", references: [], + related_integrations: [], + required_fields: [], + setup: '', severity: 'high', severity_mapping: [], tags: ['APM'], @@ -80,6 +83,9 @@ export const rulesMock: FetchRulesResponse = { 'event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection', filters: [], references: [], + related_integrations: [], + required_fields: [], + setup: '', severity: 'high', severity_mapping: [], updated_by: 'elastic', @@ -115,6 +121,9 @@ export const rulesMock: FetchRulesResponse = { query: 'event.kind:alert and event.module:endgame and event.action:rules_engine_event', filters: [], references: [], + related_integrations: [], + required_fields: [], + setup: '', severity: 'medium', severity_mapping: [], updated_by: 'elastic', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index ddd65674274be..d6e278599d62d 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -34,6 +34,9 @@ import { BulkAction, BulkActionEditPayload, ruleExecutionSummary, + RelatedIntegrationArray, + RequiredFieldArray, + SetupGuide, } from '../../../../../common/detection_engine/schemas/common'; import { @@ -102,11 +105,14 @@ export const RuleSchema = t.intersection([ name: t.string, max_signals: t.number, references: t.array(t.string), + related_integrations: RelatedIntegrationArray, + required_fields: RequiredFieldArray, risk_score: t.number, risk_score_mapping, rule_id: t.string, severity, severity_mapping, + setup: SetupGuide, tags: t.array(t.string), type, to: t.string, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx index 096463872fc01..3ca18552a85ef 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx @@ -67,9 +67,12 @@ describe('useRule', () => { max_signals: 100, query: "user.email: 'root@elastic.co'", references: [], + related_integrations: [], + required_fields: [], risk_score: 75, risk_score_mapping: [], rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', + setup: '', severity: 'high', severity_mapping: [], tags: ['APM'], diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx index d7c4ad8772bd2..1816fd4c5a7af 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx @@ -78,9 +78,12 @@ describe('useRuleWithFallback', () => { "name": "Test rule", "query": "user.email: 'root@elastic.co'", "references": Array [], + "related_integrations": Array [], + "required_fields": Array [], "risk_score": 75, "risk_score_mapping": Array [], "rule_id": "bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf", + "setup": "", "severity": "high", "severity_mapping": Array [], "tags": Array [ diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts index 77de8902be33a..d9f16242a544a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -70,6 +70,9 @@ export const mockRule = (id: string): Rule => ({ timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', timeline_title: 'Untitled timeline', meta: { from: '0m' }, + related_integrations: [], + required_fields: [], + setup: '', severity: 'low', severity_mapping: [], updated_by: 'elastic', @@ -133,6 +136,9 @@ export const mockRuleWithEverything = (id: string): Rule => ({ timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', timeline_title: 'Titled timeline', meta: { from: '0m' }, + related_integrations: [], + required_fields: [], + setup: '', severity: 'low', severity_mapping: [], updated_by: 'elastic', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts index d97eff43aeb8d..04e8f2130e88f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts @@ -51,6 +51,9 @@ describe('schedule_notification_actions', () => { note: '# sample markdown', version: 1, exceptionsList: [], + relatedIntegrations: [], + requiredFields: [], + setup: '', }; it('Should schedule actions with unflatted and legacy context', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts index d7293275c9c49..72ddb96301c47 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts @@ -59,6 +59,9 @@ describe('schedule_throttle_notification_actions', () => { note: '# sample markdown', version: 1, exceptionsList: [], + relatedIntegrations: [], + requiredFields: [], + setup: '', }; }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 2622493a51dc1..54bf6133f9e37 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -90,4 +90,7 @@ export const getOutputRuleAlertForRest = (): Omit< note: '# Investigative notes', version: 1, execution_summary: undefined, + related_integrations: [], + required_fields: [], + setup: '', }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts index d603784fc7081..8f87c1cdc0467 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts @@ -117,10 +117,13 @@ export const importRules = async ({ index, interval, max_signals: maxSignals, + related_integrations: relatedIntegrations, + required_fields: requiredFields, risk_score: riskScore, risk_score_mapping: riskScoreMapping, rule_name_override: ruleNameOverride, name, + setup, severity, severity_mapping: severityMapping, tags, @@ -192,9 +195,12 @@ export const importRules = async ({ interval, maxSignals, name, + relatedIntegrations, + requiredFields, riskScore, riskScoreMapping, ruleNameOverride, + setup, severity, severityMapping, tags, @@ -250,10 +256,13 @@ export const importRules = async ({ index, interval, maxSignals, + relatedIntegrations, + requiredFields, riskScore, riskScoreMapping, ruleNameOverride, name, + setup, severity, severityMapping, tags, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index 0b8c49cdb4d17..833361e7e22bf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -63,6 +63,9 @@ export const ruleOutput = (): RulesSchema => ({ note: '# Investigative notes', timeline_title: 'some-timeline-title', timeline_id: 'some-timeline-id', + related_integrations: [], + required_fields: [], + setup: '', }); describe('validate', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts index 5768306999f79..083f495366480 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts @@ -126,6 +126,9 @@ describe('buildAlert', () => { ], to: 'now', references: ['http://example.com', 'https://example.com'], + related_integrations: [], + required_fields: [], + setup: '', version: 1, exceptions_list: [ { @@ -303,6 +306,9 @@ describe('buildAlert', () => { ], to: 'now', references: ['http://example.com', 'https://example.com'], + related_integrations: [], + required_fields: [], + setup: '', version: 1, exceptions_list: [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts index 1a41adb4f6da5..3c7acccae703a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts @@ -32,11 +32,14 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({ index: ['index-123'], interval: '5m', maxSignals: 100, + relatedIntegrations: undefined, + requiredFields: undefined, riskScore: 80, riskScoreMapping: [], ruleNameOverride: undefined, outputIndex: 'output-1', name: 'Query with a rule id', + setup: undefined, severity: 'high', severityMapping: [], tags: [], @@ -85,11 +88,14 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({ index: ['index-123'], interval: '5m', maxSignals: 100, + relatedIntegrations: undefined, + requiredFields: undefined, riskScore: 80, riskScoreMapping: [], ruleNameOverride: undefined, outputIndex: 'output-1', name: 'Machine Learning Job', + setup: undefined, severity: 'high', severityMapping: [], tags: [], @@ -141,12 +147,15 @@ export const getCreateThreatMatchRulesOptionsMock = (): CreateRulesOptions => ({ outputIndex: 'output-1', query: 'user.name: root or user.name: admin', references: ['http://www.example.com'], + relatedIntegrations: undefined, + requiredFields: undefined, riskScore: 80, riskScoreMapping: [], ruleId: 'rule-1', ruleNameOverride: undefined, rulesClient: rulesClientMock.create(), savedId: 'savedId-123', + setup: undefined, severity: 'high', severityMapping: [], tags: [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts index 24017adc20626..726964cdf3596 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts @@ -46,11 +46,14 @@ export const createRules = async ({ index, interval, maxSignals, + relatedIntegrations, + requiredFields, riskScore, riskScoreMapping, ruleNameOverride, outputIndex, name, + setup, severity, severityMapping, tags, @@ -109,9 +112,12 @@ export const createRules = async ({ : undefined, filters, maxSignals, + relatedIntegrations, + requiredFields, riskScore, riskScoreMapping, ruleNameOverride, + setup, severity, severityMapping, threat, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts index 04d8e66a076fb..cab22e136f529 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts @@ -6,6 +6,8 @@ */ import uuid from 'uuid'; +import { SanitizedRule } from '@kbn/alerting-plugin/common'; +import { RuleParams } from '../schemas/rule_schemas'; import { duplicateRule } from './duplicate_rule'; jest.mock('uuid', () => ({ @@ -13,120 +15,287 @@ jest.mock('uuid', () => ({ })); describe('duplicateRule', () => { - it('should return a copy of rule with new ruleId', () => { - (uuid.v4 as jest.Mock).mockReturnValue('newId'); - - expect( - duplicateRule({ - id: 'oldTestRuleId', - notifyWhen: 'onActiveAlert', - name: 'test', - tags: ['test'], - alertTypeId: 'siem.signals', - consumer: 'siem', - params: { - savedId: undefined, - author: [], - description: 'test', - ruleId: 'oldTestRuleId', - falsePositives: [], - from: 'now-360s', - immutable: false, - license: '', - outputIndex: '.siem-signals-default', - meta: undefined, - maxSignals: 100, - riskScore: 42, - riskScoreMapping: [], - severity: 'low', - severityMapping: [], - threat: [], - to: 'now', - references: [], - version: 1, - exceptionsList: [], - type: 'query', - language: 'kuery', - index: [], - query: 'process.args : "chmod"', - filters: [], - buildingBlockType: undefined, - namespace: undefined, - note: undefined, - timelineId: undefined, - timelineTitle: undefined, - ruleNameOverride: undefined, - timestampOverride: undefined, - }, - schedule: { - interval: '5m', - }, + const createTestRule = (): SanitizedRule => ({ + id: 'some id', + notifyWhen: 'onActiveAlert', + name: 'Some rule', + tags: ['some tag'], + alertTypeId: 'siem.queryRule', + consumer: 'siem', + params: { + savedId: undefined, + author: [], + description: 'Some description.', + ruleId: 'some ruleId', + falsePositives: [], + from: 'now-360s', + immutable: false, + license: '', + outputIndex: '.siem-signals-default', + meta: undefined, + maxSignals: 100, + relatedIntegrations: [], + requiredFields: [], + riskScore: 42, + riskScoreMapping: [], + severity: 'low', + severityMapping: [], + setup: 'Some setup guide.', + threat: [], + to: 'now', + references: [], + version: 1, + exceptionsList: [], + type: 'query', + language: 'kuery', + index: [], + query: 'process.args : "chmod"', + filters: [], + buildingBlockType: undefined, + namespace: undefined, + note: undefined, + timelineId: undefined, + timelineTitle: undefined, + ruleNameOverride: undefined, + timestampOverride: undefined, + }, + schedule: { + interval: '5m', + }, + enabled: false, + actions: [], + throttle: null, + apiKeyOwner: 'kibana', + createdBy: 'kibana', + updatedBy: 'kibana', + muteAll: false, + mutedInstanceIds: [], + updatedAt: new Date(2021, 0), + createdAt: new Date(2021, 0), + scheduledTaskId: undefined, + executionStatus: { + lastExecutionDate: new Date(2021, 0), + status: 'ok', + }, + }); + + beforeAll(() => { + (uuid.v4 as jest.Mock).mockReturnValue('new ruleId'); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('returns an object with fields copied from a given rule', () => { + const rule = createTestRule(); + const result = duplicateRule(rule); + + expect(result).toEqual({ + name: expect.anything(), // covered in a separate test + params: { + ...rule.params, + ruleId: expect.anything(), // covered in a separate test + }, + tags: rule.tags, + alertTypeId: rule.alertTypeId, + consumer: rule.consumer, + schedule: rule.schedule, + actions: rule.actions, + throttle: null, // TODO: fix? + notifyWhen: null, // TODO: fix? + enabled: false, // covered in a separate test + }); + }); + + it('appends [Duplicate] to the name', () => { + const rule = createTestRule(); + rule.name = 'PowerShell Keylogging Script'; + const result = duplicateRule(rule); + + expect(result).toEqual( + expect.objectContaining({ + name: 'PowerShell Keylogging Script [Duplicate]', + }) + ); + }); + + it('generates a new ruleId', () => { + const rule = createTestRule(); + const result = duplicateRule(rule); + + expect(result).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + ruleId: 'new ruleId', + }), + }) + ); + }); + + it('makes sure the duplicated rule is disabled', () => { + const rule = createTestRule(); + rule.enabled = true; + const result = duplicateRule(rule); + + expect(result).toEqual( + expect.objectContaining({ enabled: false, - actions: [], - throttle: null, - apiKeyOwner: 'kibana', - createdBy: 'kibana', - updatedBy: 'kibana', - muteAll: false, - mutedInstanceIds: [], - updatedAt: new Date(2021, 0), - createdAt: new Date(2021, 0), - scheduledTaskId: undefined, - executionStatus: { - lastExecutionDate: new Date(2021, 0), - status: 'ok', - }, }) - ).toMatchInlineSnapshot(` - Object { - "actions": Array [], - "alertTypeId": "siem.queryRule", - "consumer": "siem", - "enabled": false, - "name": "test [Duplicate]", - "notifyWhen": null, - "params": Object { - "author": Array [], - "buildingBlockType": undefined, - "description": "test", - "exceptionsList": Array [], - "falsePositives": Array [], - "filters": Array [], - "from": "now-360s", - "immutable": false, - "index": Array [], - "language": "kuery", - "license": "", - "maxSignals": 100, - "meta": undefined, - "namespace": undefined, - "note": undefined, - "outputIndex": ".siem-signals-default", - "query": "process.args : \\"chmod\\"", - "references": Array [], - "riskScore": 42, - "riskScoreMapping": Array [], - "ruleId": "newId", - "ruleNameOverride": undefined, - "savedId": undefined, - "severity": "low", - "severityMapping": Array [], - "threat": Array [], - "timelineId": undefined, - "timelineTitle": undefined, - "timestampOverride": undefined, - "to": "now", - "type": "query", - "version": 1, + ); + }); + + describe('when duplicating a prebuilt (immutable) rule', () => { + const createPrebuiltRule = () => { + const rule = createTestRule(); + rule.params.immutable = true; + return rule; + }; + + it('transforms it to a custom (mutable) rule', () => { + const rule = createPrebuiltRule(); + const result = duplicateRule(rule); + + expect(result).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + immutable: false, + }), + }) + ); + }); + + it('resets related integrations to an empty array', () => { + const rule = createPrebuiltRule(); + rule.params.relatedIntegrations = [ + { + package: 'aws', + version: '~1.2.3', + integration: 'route53', }, - "schedule": Object { - "interval": "5m", + ]; + + const result = duplicateRule(rule); + + expect(result).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + relatedIntegrations: [], + }), + }) + ); + }); + + it('resets required fields to an empty array', () => { + const rule = createPrebuiltRule(); + rule.params.requiredFields = [ + { + name: 'event.action', + type: 'keyword', + ecs: true, }, - "tags": Array [ - "test", - ], - "throttle": null, - } - `); + ]; + + const result = duplicateRule(rule); + + expect(result).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + requiredFields: [], + }), + }) + ); + }); + + it('resets setup guide to an empty string', () => { + const rule = createPrebuiltRule(); + rule.params.setup = `## Config\n\nThe 'Audit Detailed File Share' audit policy must be configured...`; + const result = duplicateRule(rule); + + expect(result).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + setup: '', + }), + }) + ); + }); + }); + + describe('when duplicating a custom (mutable) rule', () => { + const createCustomRule = () => { + const rule = createTestRule(); + rule.params.immutable = false; + return rule; + }; + + it('keeps it custom', () => { + const rule = createCustomRule(); + const result = duplicateRule(rule); + + expect(result).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + immutable: false, + }), + }) + ); + }); + + it('copies related integrations as is', () => { + const rule = createCustomRule(); + rule.params.relatedIntegrations = [ + { + package: 'aws', + version: '~1.2.3', + integration: 'route53', + }, + ]; + + const result = duplicateRule(rule); + + expect(result).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + relatedIntegrations: rule.params.relatedIntegrations, + }), + }) + ); + }); + + it('copies required fields as is', () => { + const rule = createCustomRule(); + rule.params.requiredFields = [ + { + name: 'event.action', + type: 'keyword', + ecs: true, + }, + ]; + + const result = duplicateRule(rule); + + expect(result).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + requiredFields: rule.params.requiredFields, + }), + }) + ); + }); + + it('copies setup guide as is', () => { + const rule = createCustomRule(); + rule.params.setup = `## Config\n\nThe 'Audit Detailed File Share' audit policy must be configured...`; + const result = duplicateRule(rule); + + expect(result).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + setup: rule.params.setup, + }), + }) + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts index 4ef21d0450517..81af1533498ee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts @@ -22,7 +22,16 @@ const DUPLICATE_TITLE = i18n.translate( ); export const duplicateRule = (rule: SanitizedRule): InternalRuleCreate => { - const newRuleId = uuid.v4(); + // Generate a new static ruleId + const ruleId = uuid.v4(); + + // If it's a prebuilt rule, reset Related Integrations, Required Fields and Setup Guide. + // We do this because for now we don't allow the users to edit these fields for custom rules. + const isPrebuilt = rule.params.immutable; + const relatedIntegrations = isPrebuilt ? [] : rule.params.relatedIntegrations; + const requiredFields = isPrebuilt ? [] : rule.params.requiredFields; + const setup = isPrebuilt ? '' : rule.params.setup; + return { name: `${rule.name} [${DUPLICATE_TITLE}]`, tags: rule.tags, @@ -31,7 +40,10 @@ export const duplicateRule = (rule: SanitizedRule): InternalRuleCrea params: { ...rule.params, immutable: false, - ruleId: newRuleId, + ruleId, + relatedIntegrations, + requiredFields, + setup, }, schedule: rule.schedule, enabled: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts index de80a8ba8c26b..68fad65a8ff7e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts @@ -85,6 +85,9 @@ describe('getExportAll', () => { name: 'Detect Root/Admin Users', query: 'user.name: root or user.name: admin', references: ['http://example.com', 'https://example.com'], + related_integrations: [], + required_fields: [], + setup: '', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', meta: { someMeta: 'someField' }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index f297f375dda0b..e31c1444cd9fc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -82,6 +82,9 @@ describe('get_export_by_object_ids', () => { name: 'Detect Root/Admin Users', query: 'user.name: root or user.name: admin', references: ['http://example.com', 'https://example.com'], + related_integrations: [], + required_fields: [], + setup: '', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', meta: { someMeta: 'someField' }, @@ -191,6 +194,9 @@ describe('get_export_by_object_ids', () => { name: 'Detect Root/Admin Users', query: 'user.name: root or user.name: admin', references: ['http://example.com', 'https://example.com'], + related_integrations: [], + required_fields: [], + setup: '', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', meta: { someMeta: 'someField' }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts index bffa0bc39eb91..1ef4f14b17b6b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -39,10 +39,13 @@ export const installPrepackagedRules = ( index, interval, max_signals: maxSignals, + related_integrations: relatedIntegrations, + required_fields: requiredFields, risk_score: riskScore, risk_score_mapping: riskScoreMapping, rule_name_override: ruleNameOverride, name, + setup, severity, severity_mapping: severityMapping, tags, @@ -95,10 +98,13 @@ export const installPrepackagedRules = ( index, interval, maxSignals, + relatedIntegrations, + requiredFields, riskScore, riskScoreMapping, ruleNameOverride, name, + setup, severity, severityMapping, tags, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index ad2443b34fa95..e5f87b7cdb2e2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -54,11 +54,14 @@ export const patchRules = async ({ index, interval, maxSignals, + relatedIntegrations, + requiredFields, riskScore, riskScoreMapping, ruleNameOverride, rule, name, + setup, severity, severityMapping, tags, @@ -108,10 +111,13 @@ export const patchRules = async ({ index, interval, maxSignals, + relatedIntegrations, + requiredFields, riskScore, riskScoreMapping, ruleNameOverride, name, + setup, severity, severityMapping, tags, @@ -158,9 +164,12 @@ export const patchRules = async ({ filters, index, maxSignals, + relatedIntegrations, + requiredFields, riskScore, riskScoreMapping, ruleNameOverride, + setup, severity, severityMapping, threat, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 8b560d0edea0f..eeb0e88e53d47 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -93,6 +93,9 @@ import { RuleNameOverrideOrUndefined, EventCategoryOverrideOrUndefined, NamespaceOrUndefined, + RelatedIntegrationArray, + RequiredFieldArray, + SetupGuide, } from '../../../../common/detection_engine/schemas/common'; import { PartialFilter } from '../types'; @@ -161,11 +164,14 @@ export interface CreateRulesOptions { interval: Interval; license: LicenseOrUndefined; maxSignals: MaxSignals; + relatedIntegrations: RelatedIntegrationArray | undefined; + requiredFields: RequiredFieldArray | undefined; riskScore: RiskScore; riskScoreMapping: RiskScoreMapping; ruleNameOverride: RuleNameOverrideOrUndefined; outputIndex: OutputIndex; name: Name; + setup: SetupGuide | undefined; severity: Severity; severityMapping: SeverityMapping; tags: Tags; @@ -225,11 +231,14 @@ interface PatchRulesFieldsOptions { interval: IntervalOrUndefined; license: LicenseOrUndefined; maxSignals: MaxSignalsOrUndefined; + relatedIntegrations: RelatedIntegrationArray | undefined; + requiredFields: RequiredFieldArray | undefined; riskScore: RiskScoreOrUndefined; riskScoreMapping: RiskScoreMappingOrUndefined; ruleNameOverride: RuleNameOverrideOrUndefined; outputIndex: OutputIndexOrUndefined; name: NameOrUndefined; + setup: SetupGuide | undefined; severity: SeverityOrUndefined; severityMapping: SeverityMappingOrUndefined; tags: TagsOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts index ad35e11d35668..079af5b82d608 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -83,10 +83,13 @@ export const createPromises = ( index, interval, max_signals: maxSignals, + related_integrations: relatedIntegrations, + required_fields: requiredFields, risk_score: riskScore, risk_score_mapping: riskScoreMapping, rule_name_override: ruleNameOverride, name, + setup, severity, severity_mapping: severityMapping, tags, @@ -169,10 +172,13 @@ export const createPromises = ( index, interval, maxSignals, + relatedIntegrations, + requiredFields, riskScore, riskScoreMapping, ruleNameOverride, name, + setup, severity, severityMapping, tags, @@ -220,10 +226,13 @@ export const createPromises = ( index, interval, maxSignals, + relatedIntegrations, + requiredFields, riskScore, riskScoreMapping, ruleNameOverride, name, + setup, severity, severityMapping, tags, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index ba65b76f01c4a..7c981a5481ff9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -54,9 +54,12 @@ export const updateRules = async ({ timelineTitle: ruleUpdate.timeline_title, meta: ruleUpdate.meta, maxSignals: ruleUpdate.max_signals ?? DEFAULT_MAX_SIGNALS, + relatedIntegrations: existingRule.params.relatedIntegrations, + requiredFields: existingRule.params.requiredFields, riskScore: ruleUpdate.risk_score, riskScoreMapping: ruleUpdate.risk_score_mapping ?? [], ruleNameOverride: ruleUpdate.rule_name_override, + setup: existingRule.params.setup, severity: ruleUpdate.severity, severityMapping: ruleUpdate.severity_mapping ?? [], threat: ruleUpdate.threat ?? [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts index 0952da3182e01..43ac38f447abc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts @@ -127,10 +127,13 @@ describe('utils', () => { index: undefined, interval: undefined, maxSignals: undefined, + relatedIntegrations: undefined, + requiredFields: undefined, riskScore: undefined, riskScoreMapping: undefined, ruleNameOverride: undefined, name: undefined, + setup: undefined, severity: undefined, severityMapping: undefined, tags: undefined, @@ -179,10 +182,13 @@ describe('utils', () => { index: undefined, interval: undefined, maxSignals: undefined, + relatedIntegrations: undefined, + requiredFields: undefined, riskScore: undefined, riskScoreMapping: undefined, ruleNameOverride: undefined, name: undefined, + setup: undefined, severity: undefined, severityMapping: undefined, tags: undefined, @@ -231,10 +237,13 @@ describe('utils', () => { index: undefined, interval: undefined, maxSignals: undefined, + relatedIntegrations: undefined, + requiredFields: undefined, riskScore: undefined, riskScoreMapping: undefined, ruleNameOverride: undefined, name: undefined, + setup: undefined, severity: undefined, severityMapping: undefined, tags: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index dd25676a758e4..4ac138e1629f3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -56,7 +56,10 @@ import { TimestampOverrideOrUndefined, EventCategoryOverrideOrUndefined, NamespaceOrUndefined, -} from '../../../../common/detection_engine/schemas/common/schemas'; + RelatedIntegrationArray, + RequiredFieldArray, + SetupGuide, +} from '../../../../common/detection_engine/schemas/common'; import { PartialFilter } from '../types'; import { RuleParams } from '../schemas/rule_schemas'; import { @@ -107,11 +110,14 @@ export interface UpdateProperties { index: IndexOrUndefined; interval: IntervalOrUndefined; maxSignals: MaxSignalsOrUndefined; + relatedIntegrations: RelatedIntegrationArray | undefined; + requiredFields: RequiredFieldArray | undefined; riskScore: RiskScoreOrUndefined; riskScoreMapping: RiskScoreMappingOrUndefined; ruleNameOverride: RuleNameOverrideOrUndefined; outputIndex: OutputIndexOrUndefined; name: NameOrUndefined; + setup: SetupGuide | undefined; severity: SeverityOrUndefined; severityMapping: SeverityMappingOrUndefined; tags: TagsOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts index fd80bec1f6ad9..356436058b55c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -161,6 +161,9 @@ export const convertCreateAPIToInternalSchema = ( note: input.note, version: input.version ?? 1, exceptionsList: input.exceptions_list ?? [], + relatedIntegrations: [], + requiredFields: [], + setup: '', ...typeSpecificParams, }, schedule: { interval: input.interval ?? '5m' }, @@ -276,6 +279,9 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => { version: params.version, exceptions_list: params.exceptionsList, immutable: params.immutable, + related_integrations: params.relatedIntegrations ?? [], + required_fields: params.requiredFields ?? [], + setup: params.setup ?? '', }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.mock.ts index edaacf38d7712..9e3fa6a906da9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.mock.ts @@ -51,6 +51,9 @@ const getBaseRuleParams = (): BaseRuleParams => { threat: getThreatMock(), version: 1, exceptionsList: getListArrayMock(), + relatedIntegrations: [], + requiredFields: [], + setup: '', }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts index 47e49e5f9c467..d1776136f6513 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts @@ -72,7 +72,10 @@ import { updatedByOrNull, created_at, updated_at, -} from '../../../../common/detection_engine/schemas/common/schemas'; + RelatedIntegrationArray, + RequiredFieldArray, + SetupGuide, +} from '../../../../common/detection_engine/schemas/common'; import { SERVER_APP_ID } from '../../../../common/constants'; const nonEqlLanguages = t.keyof({ kuery: null, lucene: null }); @@ -105,6 +108,9 @@ export const baseRuleParams = t.exact( references, version, exceptionsList: listArray, + relatedIntegrations: t.union([RelatedIntegrationArray, t.undefined]), + requiredFields: t.union([RequiredFieldArray, t.undefined]), + setup: t.union([SetupGuide, t.undefined]), }) ); export type BaseRuleParams = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 9213d6c5b278c..03074b9560553 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -157,6 +157,9 @@ export const expectedRule = (): RulesSchema => { timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', exceptions_list: getListArrayMock(), + related_integrations: [], + required_fields: [], + setup: '', }; }; @@ -624,6 +627,9 @@ export const sampleSignalHit = (): SignalHit => ({ rule_id: 'query-rule-id', interval: '5m', exceptions_list: getListArrayMock(), + related_integrations: [], + required_fields: [], + setup: '', }, depth: 1, }, @@ -685,6 +691,9 @@ export const sampleThresholdSignalHit = (): SignalHit => ({ rule_id: 'query-rule-id', interval: '5m', exceptions_list: getListArrayMock(), + related_integrations: [], + required_fields: [], + setup: '', }, depth: 1, }, diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts index 7d32af43d1913..aff63d635c976 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts @@ -89,6 +89,9 @@ export default ({ getService }: FtrProviderContext) => { name: 'Simple Rule Query', query: 'user.name: root or user.name: admin', references: [], + related_integrations: [], + required_fields: [], + setup: '', severity: 'high', severity_mapping: [], updated_by: 'elastic', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts index 1b7e22fb21c57..966420c90b8d2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts @@ -171,6 +171,9 @@ export default ({ getService }: FtrProviderContext) => { name: 'Simple Rule Query', query: 'user.name: root or user.name: admin', references: [], + related_integrations: [], + required_fields: [], + setup: '', severity: 'high', severity_mapping: [], updated_by: 'elastic', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts index 865185387c57c..5382ba5fd18f4 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts @@ -353,6 +353,9 @@ export default ({ getService }: FtrProviderContext) => { language: 'kuery', index: ['.siem-signals-*'], query: '*:*', + related_integrations: [], + required_fields: [], + setup: '', }, 'kibana.alert.rule.actions': [], 'kibana.alert.rule.created_by': 'elastic', @@ -518,6 +521,9 @@ export default ({ getService }: FtrProviderContext) => { language: 'kuery', index: ['.alerts-security.alerts-default'], query: '*:*', + related_integrations: [], + required_fields: [], + setup: '', }, 'kibana.alert.rule.actions': [], 'kibana.alert.rule.created_by': 'elastic', diff --git a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts index 98fdfa99cbd3c..81a169636605b 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts @@ -97,4 +97,7 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => version: 1, query: 'user.name: root or user.name: admin', exceptions_list: [], + related_integrations: [], + required_fields: [], + setup: '', }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts index 30dc7eecb9256..ca8b04e66f3fc 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts @@ -26,11 +26,14 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial language: 'kuery', output_index: '.siem-signals-default', max_signals: 100, + related_integrations: [], + required_fields: [], risk_score: 1, risk_score_mapping: [], name: 'Simple Rule Query', query: 'user.name: root or user.name: admin', references: [], + setup: '', severity: 'high', severity_mapping: [], updated_by: 'elastic', From 383239e77c165bfb77100a68c4a92c4ed7fb8fe2 Mon Sep 17 00:00:00 2001 From: Kfir Peled <61654899+kfirpeled@users.noreply.github.com> Date: Sun, 22 May 2022 13:18:42 +0300 Subject: [PATCH 064/120] [Cloud Posture] Findings - Group by resource - Fixed bug not showing results (#132529) --- .../findings_by_resource_table.test.tsx | 30 ++++++++---- .../findings_by_resource_table.tsx | 48 +++++++++++++++---- .../use_findings_by_resource.ts | 34 +++++++++---- .../create_indices/latest_findings_mapping.ts | 26 +++++++--- 4 files changed, 103 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx index a6b8f3b863401..9cc87d98e54f8 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx @@ -21,14 +21,23 @@ import { TestProvider } from '../../../test/test_provider'; const chance = new Chance(); -const getFakeFindingsByResource = (): CspFindingsByResource => ({ - resource_id: chance.guid(), - cis_sections: [chance.word(), chance.word()], - failed_findings: { - total: chance.integer(), - normalized: chance.integer({ min: 0, max: 1 }), - }, -}); +const getFakeFindingsByResource = (): CspFindingsByResource => { + const count = chance.integer(); + const total = chance.integer() + count + 1; + const normalized = count / total; + + return { + resource_id: chance.guid(), + resource_name: chance.word(), + resource_subtype: chance.word(), + cis_sections: [chance.word(), chance.word()], + failed_findings: { + count, + normalized, + total_findings: total, + }, + }; +}; type TableProps = PropsOf; @@ -74,8 +83,11 @@ describe('', () => { ); expect(row).toBeInTheDocument(); expect(within(row).getByText(item.resource_id)).toBeInTheDocument(); + if (item.resource_name) expect(within(row).getByText(item.resource_name)).toBeInTheDocument(); + if (item.resource_subtype) + expect(within(row).getByText(item.resource_subtype)).toBeInTheDocument(); expect(within(row).getByText(item.cis_sections.join(', '))).toBeInTheDocument(); - expect(within(row).getByText(formatNumber(item.failed_findings.total))).toBeInTheDocument(); + expect(within(row).getByText(formatNumber(item.failed_findings.count))).toBeInTheDocument(); expect( within(row).getByText(new RegExp(numeral(item.failed_findings.normalized).format('0%'))) ).toBeInTheDocument(); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx index 2e96306ad3a69..80da922225893 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx @@ -9,12 +9,12 @@ import { EuiEmptyPrompt, EuiBasicTable, EuiTextColor, - EuiFlexGroup, - EuiFlexItem, type EuiTableFieldDataColumnType, type CriteriaWithPagination, type Pagination, + EuiToolTip, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import numeral from '@elastic/numeral'; import { Link, generatePath } from 'react-router-dom'; @@ -81,6 +81,26 @@ const columns: Array> = [ ), }, + { + field: 'resource_subtype', + truncateText: true, + name: ( + + ), + }, + { + field: 'resource_name', + truncateText: true, + name: ( + + ), + }, { field: 'cis_sections', truncateText: true, @@ -102,14 +122,22 @@ const columns: Array> = [ /> ), render: (failedFindings: CspFindingsByResource['failed_findings']) => ( - - - {formatNumber(failedFindings.total)} - - - ({numeral(failedFindings.normalized).format('0%')}) - - + + <> + + {formatNumber(failedFindings.count)} + + ({numeral(failedFindings.normalized).format('0%')}) + + ), }, ]; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts index 880b2be868e6f..e2da77c8ba2a2 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts @@ -14,7 +14,7 @@ import { showErrorToast } from '../latest_findings/use_latest_findings'; import type { FindingsBaseEsQuery, FindingsQueryResult } from '../types'; // a large number to probably get all the buckets -const MAX_BUCKETS = 60 * 1000; +const MAX_BUCKETS = 1000 * 1000; interface UseResourceFindingsOptions extends FindingsBaseEsQuery { from: NonNullable; @@ -43,6 +43,8 @@ interface FindingsByResourceAggs { interface FindingsAggBucket extends estypes.AggregationsStringRareTermsBucketKeys { failed_findings: estypes.AggregationsMultiBucketBase; + name: estypes.AggregationsMultiBucketAggregateBase; + subtype: estypes.AggregationsMultiBucketAggregateBase; cis_sections: estypes.AggregationsMultiBucketAggregateBase; } @@ -57,10 +59,16 @@ export const getFindingsByResourceAggQuery = ({ query, size: 0, aggs: { - resource_total: { cardinality: { field: 'resource.id.keyword' } }, + resource_total: { cardinality: { field: 'resource.id' } }, resources: { - terms: { field: 'resource.id.keyword', size: MAX_BUCKETS }, + terms: { field: 'resource.id', size: MAX_BUCKETS }, aggs: { + name: { + terms: { field: 'resource.name', size: 1 }, + }, + subtype: { + terms: { field: 'resource.sub_type', size: 1 }, + }, cis_sections: { terms: { field: 'rule.section.keyword' }, }, @@ -117,16 +125,24 @@ export const useFindingsByResource = ({ index, query, from, size }: UseResourceF ); }; -const createFindingsByResource = (bucket: FindingsAggBucket) => { - if (!Array.isArray(bucket.cis_sections.buckets)) +const createFindingsByResource = (resource: FindingsAggBucket) => { + if ( + !Array.isArray(resource.cis_sections.buckets) || + !Array.isArray(resource.name.buckets) || + !Array.isArray(resource.subtype.buckets) + ) throw new Error('expected buckets to be an array'); return { - resource_id: bucket.key, - cis_sections: bucket.cis_sections.buckets.map((v) => v.key), + resource_id: resource.key, + resource_name: resource.name.buckets.map((v) => v.key).at(0), + resource_subtype: resource.subtype.buckets.map((v) => v.key).at(0), + cis_sections: resource.cis_sections.buckets.map((v) => v.key), failed_findings: { - total: bucket.failed_findings.doc_count, - normalized: bucket.doc_count > 0 ? bucket.failed_findings.doc_count / bucket.doc_count : 0, + count: resource.failed_findings.doc_count, + normalized: + resource.doc_count > 0 ? resource.failed_findings.doc_count / resource.doc_count : 0, + total_findings: resource.doc_count, }, }; }; diff --git a/x-pack/plugins/cloud_security_posture/server/create_indices/latest_findings_mapping.ts b/x-pack/plugins/cloud_security_posture/server/create_indices/latest_findings_mapping.ts index 9ebe4c3cf4038..57305fd2df7c4 100644 --- a/x-pack/plugins/cloud_security_posture/server/create_indices/latest_findings_mapping.ts +++ b/x-pack/plugins/cloud_security_posture/server/create_indices/latest_findings_mapping.ts @@ -50,20 +50,32 @@ export const latestFindingsMapping: MappingTypeMapping = { properties: { type: { type: 'keyword', - ignore_above: 256, + ignore_above: 1024, }, id: { - type: 'text', + type: 'keyword', + ignore_above: 1024, + fields: { + text: { + type: 'text', + }, + }, }, name: { - type: 'text', + type: 'keyword', + ignore_above: 1024, + fields: { + text: { + type: 'text', + }, + }, }, sub_type: { - type: 'text', + ignore_above: 1024, + type: 'keyword', fields: { - keyword: { - ignore_above: 1024, - type: 'keyword', + text: { + type: 'text', }, }, }, From fbaf0588d0ed72ba5f1f405252b93bb6584333f8 Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Sun, 22 May 2022 17:14:23 -0700 Subject: [PATCH 065/120] [RAM] Add shareable rules list (#132437) * Shareable rules list * Hide snooze panel in rules list * Address comments and added tests * Fix tests * Fix tests * Fix lint * Address design comments and fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/experimental_features.ts | 4 +- .../hooks/use_load_rule_aggregations.test.ts | 111 +++ .../hooks/use_load_rule_aggregations.ts | 83 ++ .../application/hooks/use_load_rules.test.ts | 378 +++++++ .../application/hooks/use_load_rules.ts | 185 ++++ .../application/hooks/use_load_tags.test.ts | 54 + .../public/application/hooks/use_load_tags.ts | 45 + .../rule_event_log_list_sandbox.tsx | 3 +- .../rules_list_sandbox.tsx | 16 + .../shareable_components_sandbox.tsx | 2 + .../application/lib/rule_api/aggregate.ts | 20 +- .../public/application/lib/rule_api/index.ts | 2 + .../public/application/lib/rule_api/rules.ts | 24 +- .../public/application/sections/index.tsx | 3 + .../components/action_type_filter.tsx | 89 +- .../rule_execution_status_filter.tsx | 108 +- .../components/rule_status_dropdown.tsx | 100 +- .../components/rule_status_filter.test.tsx | 14 +- .../components/rule_status_filter.tsx | 33 +- .../rules_list/components/rule_tag_filter.tsx | 50 +- .../rules_list/components/rules_list.test.tsx | 90 +- .../rules_list/components/rules_list.tsx | 941 +++--------------- .../rules_list_auto_refresh.test.tsx | 87 ++ .../components/rules_list_auto_refresh.tsx | 122 +++ .../components/rules_list_notify_badge.tsx | 224 +++++ .../components/rules_list_table.tsx | 724 ++++++++++++++ .../rules_list/components/type_filter.tsx | 102 +- .../public/common/get_rules_list.tsx | 13 + .../triggers_actions_ui/public/mocks.ts | 4 + .../triggers_actions_ui/public/plugin.ts | 5 + .../apps/triggers_actions_ui/index.ts | 1 + .../triggers_actions_ui/rule_tag_filter.ts | 20 - .../apps/triggers_actions_ui/rules_list.ts | 34 + 33 files changed, 2624 insertions(+), 1067 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rules_list_sandbox.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_notify_badge.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/get_rules_list.tsx create mode 100644 x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list.ts diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts index 33f5fdc44afcd..3265469bea640 100644 --- a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts +++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts @@ -15,8 +15,8 @@ export const allowedExperimentalValues = Object.freeze({ rulesListDatagrid: true, internalAlertsTable: false, internalShareableComponentsSandbox: false, - ruleTagFilter: false, - ruleStatusFilter: false, + ruleTagFilter: true, + ruleStatusFilter: true, rulesDetailLogs: true, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.test.ts new file mode 100644 index 0000000000000..b00101da6be83 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useLoadRuleAggregations } from './use_load_rule_aggregations'; +import { RuleStatus } from '../../types'; + +const MOCK_TAGS = ['a', 'b', 'c']; + +const MOCK_AGGS = { + ruleEnabledStatus: { enabled: 2, disabled: 0 }, + ruleExecutionStatus: { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 }, + ruleMutedStatus: { muted: 0, unmuted: 2 }, + ruleTags: MOCK_TAGS, +}; + +jest.mock('../lib/rule_api', () => ({ + loadRuleAggregations: jest.fn(), +})); + +const { loadRuleAggregations } = jest.requireMock('../lib/rule_api'); + +const onError = jest.fn(); + +describe('useLoadRuleAggregations', () => { + beforeEach(() => { + loadRuleAggregations.mockResolvedValue(MOCK_AGGS); + jest.clearAllMocks(); + }); + + it('should call loadRuleAggregations API and handle result', async () => { + const params = { + searchText: '', + typesFilter: [], + actionTypesFilter: [], + ruleExecutionStatusesFilter: [], + ruleStatusesFilter: [], + tagsFilter: [], + }; + + const { result, waitForNextUpdate } = renderHook(() => + useLoadRuleAggregations({ + ...params, + onError, + }) + ); + + await act(async () => { + result.current.loadRuleAggregations(); + await waitForNextUpdate(); + }); + + expect(loadRuleAggregations).toBeCalledWith(expect.objectContaining(params)); + expect(result.current.rulesStatusesTotal).toEqual(MOCK_AGGS.ruleExecutionStatus); + }); + + it('should call loadRuleAggregation API with params and handle result', async () => { + const params = { + searchText: 'test', + typesFilter: ['type1', 'type2'], + actionTypesFilter: ['action1', 'action2'], + ruleExecutionStatusesFilter: ['status1', 'status2'], + ruleStatusesFilter: ['enabled', 'snoozed'] as RuleStatus[], + tagsFilter: ['tag1', 'tag2'], + }; + + const { result, waitForNextUpdate } = renderHook(() => + useLoadRuleAggregations({ + ...params, + onError, + }) + ); + + await act(async () => { + result.current.loadRuleAggregations(); + await waitForNextUpdate(); + }); + + expect(loadRuleAggregations).toBeCalledWith(expect.objectContaining(params)); + expect(result.current.rulesStatusesTotal).toEqual(MOCK_AGGS.ruleExecutionStatus); + }); + + it('should call onError if API fails', async () => { + loadRuleAggregations.mockRejectedValue(''); + const params = { + searchText: '', + typesFilter: [], + actionTypesFilter: [], + ruleExecutionStatusesFilter: [], + ruleStatusesFilter: [], + tagsFilter: [], + }; + + const { result } = renderHook(() => + useLoadRuleAggregations({ + ...params, + onError, + }) + ); + + await act(async () => { + result.current.loadRuleAggregations(); + }); + + expect(onError).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.ts new file mode 100644 index 0000000000000..75f9e18ec2328 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useState, useCallback, useMemo } from 'react'; +import { RuleExecutionStatusValues } from '@kbn/alerting-plugin/common'; +import { loadRuleAggregations, LoadRuleAggregationsProps } from '../lib/rule_api'; +import { useKibana } from '../../common/lib/kibana'; + +type UseLoadRuleAggregationsProps = Omit & { + onError: (message: string) => void; +}; + +export function useLoadRuleAggregations({ + searchText, + typesFilter, + actionTypesFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + tagsFilter, + onError, +}: UseLoadRuleAggregationsProps) { + const { http } = useKibana().services; + + const [rulesStatusesTotal, setRulesStatusesTotal] = useState>( + RuleExecutionStatusValues.reduce>( + (prev: Record, status: string) => ({ + ...prev, + [status]: 0, + }), + {} + ) + ); + + const internalLoadRuleAggregations = useCallback(async () => { + try { + const rulesAggs = await loadRuleAggregations({ + http, + searchText, + typesFilter, + actionTypesFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + tagsFilter, + }); + if (rulesAggs?.ruleExecutionStatus) { + setRulesStatusesTotal(rulesAggs.ruleExecutionStatus); + } + } catch (e) { + onError( + i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleStatusInfoMessage', + { + defaultMessage: 'Unable to load rule status info', + } + ) + ); + } + }, [ + http, + searchText, + typesFilter, + actionTypesFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + tagsFilter, + onError, + setRulesStatusesTotal, + ]); + + return useMemo( + () => ({ + loadRuleAggregations: internalLoadRuleAggregations, + rulesStatusesTotal, + setRulesStatusesTotal, + }), + [internalLoadRuleAggregations, rulesStatusesTotal, setRulesStatusesTotal] + ); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.test.ts new file mode 100644 index 0000000000000..a309beeca58aa --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.test.ts @@ -0,0 +1,378 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useLoadRules } from './use_load_rules'; +import { + RuleExecutionStatusErrorReasons, + RuleExecutionStatusWarningReasons, +} from '@kbn/alerting-plugin/common'; +import { RuleStatus } from '../../types'; + +jest.mock('../lib/rule_api', () => ({ + loadRules: jest.fn(), +})); + +const { loadRules } = jest.requireMock('../lib/rule_api'); + +const onError = jest.fn(); +const onPage = jest.fn(); + +const mockedRulesData = [ + { + id: '1', + name: 'test rule', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '1s' }, + actions: [], + params: { name: 'test rule type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastDuration: 500, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + monitoring: { + execution: { + history: [ + { + success: true, + duration: 1000000, + }, + { + success: true, + duration: 200000, + }, + { + success: false, + duration: 300000, + }, + ], + calculated_metrics: { + success_ratio: 0.66, + p50: 200000, + p95: 300000, + p99: 300000, + }, + }, + }, + }, + { + id: '2', + name: 'test rule ok', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test rule type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'ok', + lastDuration: 61000, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + monitoring: { + execution: { + history: [ + { + success: true, + duration: 100000, + }, + { + success: true, + duration: 500000, + }, + ], + calculated_metrics: { + success_ratio: 1, + p50: 0, + p95: 100000, + p99: 500000, + }, + }, + }, + }, + { + id: '3', + name: 'test rule pending', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test rule type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'pending', + lastDuration: 30234, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + monitoring: { + execution: { + history: [{ success: false, duration: 100 }], + calculated_metrics: { + success_ratio: 0, + }, + }, + }, + }, + { + id: '4', + name: 'test rule error', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'error', + lastDuration: 122000, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: { + reason: RuleExecutionStatusErrorReasons.Unknown, + message: 'test', + }, + }, + }, + { + id: '5', + name: 'test rule license error', + tags: [], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'error', + lastDuration: 500, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: { + reason: RuleExecutionStatusErrorReasons.License, + message: 'test', + }, + }, + }, + { + id: '6', + name: 'test rule warning', + tags: [], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'warning', + lastDuration: 500, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + warning: { + reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS, + message: 'test', + }, + }, + }, +]; + +const MOCK_RULE_DATA = { + page: 1, + perPage: 10000, + total: 4, + data: mockedRulesData, +}; + +describe('useLoadRules', () => { + beforeEach(() => { + loadRules.mockResolvedValue(MOCK_RULE_DATA); + jest.clearAllMocks(); + }); + + it('should call loadRules API and handle result', async () => { + const params = { + page: { + index: 0, + size: 25, + }, + searchText: '', + typesFilter: [], + actionTypesFilter: [], + ruleExecutionStatusesFilter: [], + ruleStatusesFilter: [], + tagsFilter: [], + }; + + const { result, waitForNextUpdate } = renderHook(() => + useLoadRules({ + ...params, + onPage, + onError, + }) + ); + + expect(result.current.initialLoad).toBeTruthy(); + expect(result.current.noData).toBeTruthy(); + expect(result.current.rulesState.isLoading).toBeFalsy(); + + await act(async () => { + result.current.loadRules(); + await waitForNextUpdate(); + }); + + expect(result.current.initialLoad).toBeFalsy(); + expect(result.current.noData).toBeFalsy(); + expect(result.current.rulesState.isLoading).toBeFalsy(); + + expect(onPage).toBeCalledTimes(0); + expect(loadRules).toBeCalledWith(expect.objectContaining(params)); + expect(result.current.rulesState.data).toEqual(expect.arrayContaining(MOCK_RULE_DATA.data)); + expect(result.current.rulesState.totalItemCount).toEqual(MOCK_RULE_DATA.total); + }); + + it('should call loadRules API with params and handle result', async () => { + const params = { + page: { + index: 0, + size: 25, + }, + searchText: 'test', + typesFilter: ['type1', 'type2'], + actionTypesFilter: ['action1', 'action2'], + ruleExecutionStatusesFilter: ['status1', 'status2'], + ruleStatusesFilter: ['enabled', 'snoozed'] as RuleStatus[], + tagsFilter: ['tag1', 'tag2'], + }; + + const { result, waitForNextUpdate } = renderHook(() => + useLoadRules({ + ...params, + onPage, + onError, + }) + ); + + await act(async () => { + result.current.loadRules(); + await waitForNextUpdate(); + }); + + expect(loadRules).toBeCalledWith(expect.objectContaining(params)); + }); + + it('should reset the page if the data is fetched while paged', async () => { + loadRules.mockResolvedValue({ + ...MOCK_RULE_DATA, + data: [], + }); + + const params = { + page: { + index: 1, + size: 25, + }, + searchText: '', + typesFilter: [], + actionTypesFilter: [], + ruleExecutionStatusesFilter: [], + ruleStatusesFilter: [], + tagsFilter: [], + }; + + const { result, waitForNextUpdate } = renderHook(() => + useLoadRules({ + ...params, + onPage, + onError, + }) + ); + + await act(async () => { + result.current.loadRules(); + await waitForNextUpdate(); + }); + + expect(onPage).toHaveBeenCalledWith({ + index: 0, + size: 25, + }); + }); + + it('should call onError if API fails', async () => { + loadRules.mockRejectedValue(''); + const params = { + page: { + index: 0, + size: 25, + }, + searchText: '', + typesFilter: [], + actionTypesFilter: [], + ruleExecutionStatusesFilter: [], + ruleStatusesFilter: [], + tagsFilter: [], + }; + + const { result } = renderHook(() => + useLoadRules({ + ...params, + onPage, + onError, + }) + ); + + await act(async () => { + result.current.loadRules(); + }); + + expect(onError).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.ts new file mode 100644 index 0000000000000..4afdfd4f26a72 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.ts @@ -0,0 +1,185 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useMemo, useCallback, useReducer } from 'react'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { Rule, Pagination } from '../../types'; +import { loadRules, LoadRulesProps } from '../lib/rule_api'; +import { useKibana } from '../../common/lib/kibana'; + +interface RuleState { + isLoading: boolean; + data: Rule[]; + totalItemCount: number; +} + +type UseLoadRulesProps = Omit & { + onPage: (pagination: Pagination) => void; + onError: (message: string) => void; +}; + +interface UseLoadRulesState { + rulesState: RuleState; + noData: boolean; + initialLoad: boolean; +} + +enum ActionTypes { + SET_RULE_STATE = 'SET_RULE_STATE', + SET_LOADING = 'SET_LOADING', + SET_INITIAL_LOAD = 'SET_INITIAL_LOAD', + SET_NO_DATA = 'SET_NO_DATA', +} + +interface Action { + type: ActionTypes; + payload: boolean | RuleState; +} + +const initialState: UseLoadRulesState = { + rulesState: { + isLoading: false, + data: [], + totalItemCount: 0, + }, + noData: true, + initialLoad: true, +}; + +const reducer = (state: UseLoadRulesState, action: Action) => { + const { type, payload } = action; + switch (type) { + case ActionTypes.SET_RULE_STATE: + return { + ...state, + rulesState: payload as RuleState, + }; + case ActionTypes.SET_LOADING: + return { + ...state, + rulesState: { + ...state.rulesState, + isLoading: payload as boolean, + }, + }; + case ActionTypes.SET_INITIAL_LOAD: + return { + ...state, + initialLoad: payload as boolean, + }; + case ActionTypes.SET_NO_DATA: + return { + ...state, + noData: payload as boolean, + }; + default: + return state; + } +}; + +export function useLoadRules({ + page, + searchText, + typesFilter, + actionTypesFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + tagsFilter, + sort, + onPage, + onError, +}: UseLoadRulesProps) { + const { http } = useKibana().services; + const [state, dispatch] = useReducer(reducer, initialState); + + const setRulesState = useCallback( + (rulesState: RuleState) => { + dispatch({ + type: ActionTypes.SET_RULE_STATE, + payload: rulesState, + }); + }, + [dispatch] + ); + + const internalLoadRules = useCallback(async () => { + dispatch({ type: ActionTypes.SET_LOADING, payload: true }); + + try { + const rulesResponse = await loadRules({ + http, + page, + searchText, + typesFilter, + actionTypesFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + tagsFilter, + sort, + }); + + dispatch({ + type: ActionTypes.SET_RULE_STATE, + payload: { + isLoading: false, + data: rulesResponse.data, + totalItemCount: rulesResponse.total, + }, + }); + + if (!rulesResponse.data?.length && page.index > 0) { + onPage({ ...page, index: 0 }); + } + + const isFilterApplied = !( + isEmpty(searchText) && + isEmpty(typesFilter) && + isEmpty(actionTypesFilter) && + isEmpty(ruleExecutionStatusesFilter) && + isEmpty(ruleStatusesFilter) && + isEmpty(tagsFilter) + ); + + dispatch({ + type: ActionTypes.SET_NO_DATA, + payload: rulesResponse.data.length === 0 && !isFilterApplied, + }); + } catch (e) { + onError( + i18n.translate('xpack.triggersActionsUI.sections.rulesList.unableToLoadRulesMessage', { + defaultMessage: 'Unable to load rules', + }) + ); + dispatch({ type: ActionTypes.SET_LOADING, payload: false }); + } + dispatch({ type: ActionTypes.SET_INITIAL_LOAD, payload: false }); + }, [ + http, + page, + searchText, + typesFilter, + actionTypesFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + tagsFilter, + sort, + dispatch, + onPage, + onError, + ]); + + return useMemo( + () => ({ + rulesState: state.rulesState, + noData: state.noData, + initialLoad: state.initialLoad, + loadRules: internalLoadRules, + setRulesState, + }), + [state, setRulesState, internalLoadRules] + ); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.test.ts new file mode 100644 index 0000000000000..8973d869e0724 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useLoadTags } from './use_load_tags'; + +const MOCK_TAGS = ['a', 'b', 'c']; + +jest.mock('../lib/rule_api', () => ({ + loadRuleTags: jest.fn(), +})); + +const { loadRuleTags } = jest.requireMock('../lib/rule_api'); + +const onError = jest.fn(); + +describe('useLoadTags', () => { + beforeEach(() => { + loadRuleTags.mockResolvedValue({ + ruleTags: MOCK_TAGS, + }); + jest.clearAllMocks(); + }); + + it('should call loadRuleTags API and handle result', async () => { + const { result, waitForNextUpdate } = renderHook(() => useLoadTags({ onError })); + + await act(async () => { + result.current.loadTags(); + await waitForNextUpdate(); + }); + + expect(loadRuleTags).toBeCalled(); + expect(result.current.tags).toEqual(MOCK_TAGS); + }); + + it('should call onError if API fails', async () => { + loadRuleTags.mockRejectedValue(''); + + const { result } = renderHook(() => useLoadTags({ onError })); + + await act(async () => { + result.current.loadTags(); + }); + + expect(loadRuleTags).toBeCalled(); + expect(onError).toBeCalled(); + expect(result.current.tags).toEqual([]); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.ts new file mode 100644 index 0000000000000..3357f43a012f1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useState, useCallback, useMemo } from 'react'; +import { loadRuleTags } from '../lib/rule_api'; +import { useKibana } from '../../common/lib/kibana'; + +interface UseLoadTagsProps { + onError: (message: string) => void; +} + +export function useLoadTags(props: UseLoadTagsProps) { + const { onError } = props; + const { http } = useKibana().services; + const [tags, setTags] = useState([]); + + const internalLoadTags = useCallback(async () => { + try { + const ruleTagsAggs = await loadRuleTags({ http }); + if (ruleTagsAggs?.ruleTags) { + setTags(ruleTagsAggs.ruleTags); + } + } catch (e) { + onError( + i18n.translate('xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTags', { + defaultMessage: 'Unable to load rule tags', + }) + ); + } + }, [http, setTags, onError]); + + return useMemo( + () => ({ + tags, + loadTags: internalLoadTags, + setTags, + }), + [tags, internalLoadTags, setTags] + ); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_event_log_list_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_event_log_list_sandbox.tsx index 4af95523dce29..ba45800e49bcb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_event_log_list_sandbox.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_event_log_list_sandbox.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import React from 'react'; import { getRuleEventLogListLazy } from '../../../common/get_rule_event_log_list'; export const RuleEventLogListSandbox = () => { @@ -39,5 +40,5 @@ export const RuleEventLogListSandbox = () => { }), }; - return getRuleEventLogListLazy(props); + return
{getRuleEventLogListLazy(props)}
; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rules_list_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rules_list_sandbox.tsx new file mode 100644 index 0000000000000..7702b914cfd36 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rules_list_sandbox.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { getRulesListLazy } from '../../../common/get_rules_list'; + +const style = { + flex: 1, +}; + +export const RulesListSandbox = () => { + return
{getRulesListLazy()}
; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx index af5a05acdf19a..018f0a8794c33 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx @@ -11,6 +11,7 @@ import { RuleTagFilterSandbox } from './rule_tag_filter_sandbox'; import { RuleStatusFilterSandbox } from './rule_status_filter_sandbox'; import { RuleTagBadgeSandbox } from './rule_tag_badge_sandbox'; import { RuleEventLogListSandbox } from './rule_event_log_list_sandbox'; +import { RulesListSandbox } from './rules_list_sandbox'; export const InternalShareableComponentsSandbox: React.FC<{}> = () => { return ( @@ -19,6 +20,7 @@ export const InternalShareableComponentsSandbox: React.FC<{}> = () => { + ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts index 1df6177443657..5df7cfc374f89 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts @@ -44,6 +44,16 @@ export async function loadRuleTags({ http }: { http: HttpSetup }): Promise { +}: LoadRuleAggregationsProps): Promise { const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts index d0e7728498c5b..64d6b18b7ca5c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts @@ -7,6 +7,7 @@ export { alertingFrameworkHealth } from './health'; export { mapFiltersToKql } from './map_filters_to_kql'; +export type { LoadRuleAggregationsProps } from './aggregate'; export { loadRuleAggregations, loadRuleTags } from './aggregate'; export { createRule } from './create'; export { deleteRules } from './delete'; @@ -17,6 +18,7 @@ export { loadRuleSummary } from './rule_summary'; export { muteAlertInstance } from './mute_alert'; export { muteRule, muteRules } from './mute'; export { loadRuleTypes } from './rule_types'; +export type { LoadRulesProps } from './rules'; export { loadRules } from './rules'; export { loadRuleState } from './state'; export type { LoadExecutionLogAggregationsProps } from './load_execution_log_aggregations'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts index 6e527989cc91f..3db1cb8b0214d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts @@ -11,6 +11,18 @@ import { Rule, Pagination, Sorting, RuleStatus } from '../../../types'; import { mapFiltersToKql } from './map_filters_to_kql'; import { transformRule } from './common_transformations'; +export interface LoadRulesProps { + http: HttpSetup; + page: Pagination; + searchText?: string; + typesFilter?: string[]; + actionTypesFilter?: string[]; + tagsFilter?: string[]; + ruleExecutionStatusesFilter?: string[]; + ruleStatusesFilter?: RuleStatus[]; + sort?: Sorting; +} + const rewriteResponseRes = (results: Array>): Rule[] => { return results.map((item) => transformRule(item)); }; @@ -25,17 +37,7 @@ export async function loadRules({ ruleStatusesFilter, tagsFilter, sort = { field: 'name', direction: 'asc' }, -}: { - http: HttpSetup; - page: Pagination; - searchText?: string; - typesFilter?: string[]; - actionTypesFilter?: string[]; - tagsFilter?: string[]; - ruleExecutionStatusesFilter?: string[]; - ruleStatusesFilter?: RuleStatus[]; - sort?: Sorting; -}): Promise<{ +}: LoadRulesProps): Promise<{ page: number; perPage: number; total: number; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index 979630d2a5a99..bd2ef041535f3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -44,3 +44,6 @@ export const RuleTagBadge = suspendedComponentWithProps( export const RuleEventLogList = suspendedComponentWithProps( lazy(() => import('./rule_details/components/rule_event_log_list')) ); +export const RulesList = suspendedComponentWithProps( + lazy(() => import('./rules_list/components/rules_list')) +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/action_type_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/action_type_filter.tsx index a136413d53e42..38d1a62de699a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/action_type_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/action_type_filter.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiFilterGroup, EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; +import { EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; import { ActionType } from '../../../../types'; interface ActionTypeFilterProps { @@ -29,47 +29,52 @@ export const ActionTypeFilter: React.FunctionComponent = // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedValues]); + const onClick = useCallback( + (item: ActionType) => { + return () => { + const isPreviouslyChecked = selectedValues.includes(item.id); + if (isPreviouslyChecked) { + setSelectedValues(selectedValues.filter((val) => val !== item.id)); + } else { + setSelectedValues(selectedValues.concat(item.id)); + } + }; + }, + [selectedValues, setSelectedValues] + ); + return ( - - setIsPopoverOpen(false)} - button={ - 0} - numActiveFilters={selectedValues.length} - numFilters={selectedValues.length} - onClick={() => setIsPopoverOpen(!isPopoverOpen)} - data-test-subj="actionTypeFilterButton" + setIsPopoverOpen(false)} + button={ + 0} + numActiveFilters={selectedValues.length} + numFilters={selectedValues.length} + onClick={() => setIsPopoverOpen(!isPopoverOpen)} + data-test-subj="actionTypeFilterButton" + > + + + } + > +
+ {actionTypes.map((item) => ( + - - - } - > -
- {actionTypes.map((item) => ( - { - const isPreviouslyChecked = selectedValues.includes(item.id); - if (isPreviouslyChecked) { - setSelectedValues(selectedValues.filter((val) => val !== item.id)); - } else { - setSelectedValues(selectedValues.concat(item.id)); - } - }} - checked={selectedValues.includes(item.id) ? 'on' : undefined} - data-test-subj={`actionType${item.id}FilterOption`} - > - {item.name} - - ))} -
- - + {item.name} +
+ ))} +
+
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx index 9acb8489fa09a..e5bb7ffd1b0e4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx @@ -5,15 +5,9 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiFilterGroup, - EuiPopover, - EuiFilterButton, - EuiFilterSelectItem, - EuiHealth, -} from '@elastic/eui'; +import { EuiPopover, EuiFilterButton, EuiFilterSelectItem, EuiHealth } from '@elastic/eui'; import { RuleExecutionStatuses, RuleExecutionStatusValues } from '@kbn/alerting-plugin/common'; import { rulesStatusesTranslationsMapping } from '../translations'; @@ -22,6 +16,8 @@ interface RuleExecutionStatusFilterProps { onChange?: (selectedRuleStatusesIds: string[]) => void; } +const sortedRuleExecutionStatusValues = [...RuleExecutionStatusValues].sort(); + export const RuleExecutionStatusFilter: React.FunctionComponent = ({ selectedStatuses, onChange, @@ -29,6 +25,14 @@ export const RuleExecutionStatusFilter: React.FunctionComponent(selectedStatuses); const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const onTogglePopover = useCallback(() => { + setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen); + }, [setIsPopoverOpen]); + + const onClosePopover = useCallback(() => { + setIsPopoverOpen(false); + }, [setIsPopoverOpen]); + useEffect(() => { if (onChange) { onChange(selectedValues); @@ -41,51 +45,49 @@ export const RuleExecutionStatusFilter: React.FunctionComponent - setIsPopoverOpen(false)} - button={ - 0} - numActiveFilters={selectedValues.length} - numFilters={selectedValues.length} - onClick={() => setIsPopoverOpen(!isPopoverOpen)} - data-test-subj="ruleExecutionStatusFilterButton" - > - - - } - > -
- {[...RuleExecutionStatusValues].sort().map((item: RuleExecutionStatuses) => { - const healthColor = getHealthColor(item); - return ( - { - const isPreviouslyChecked = selectedValues.includes(item); - if (isPreviouslyChecked) { - setSelectedValues(selectedValues.filter((val) => val !== item)); - } else { - setSelectedValues(selectedValues.concat(item)); - } - }} - checked={selectedValues.includes(item) ? 'on' : undefined} - data-test-subj={`ruleExecutionStatus${item}FilterOption`} - > - {rulesStatusesTranslationsMapping[item]} - - ); - })} -
-
-
+ 0} + numActiveFilters={selectedValues.length} + numFilters={selectedValues.length} + onClick={onTogglePopover} + data-test-subj="ruleExecutionStatusFilterButton" + > + + + } + > +
+ {sortedRuleExecutionStatusValues.map((item: RuleExecutionStatuses) => { + const healthColor = getHealthColor(item); + return ( + { + const isPreviouslyChecked = selectedValues.includes(item); + if (isPreviouslyChecked) { + setSelectedValues(selectedValues.filter((val) => val !== item)); + } else { + setSelectedValues(selectedValues.concat(item)); + } + }} + checked={selectedValues.includes(item) ? 'on' : undefined} + data-test-subj={`ruleExecutionStatus${item}FilterOption`} + > + {rulesStatusesTranslationsMapping[item]} + + ); + })} +
+
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx index 7c6a71e893f96..194bf86030e56 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx @@ -33,7 +33,7 @@ import { parseInterval } from '../../../../../common'; import { Rule } from '../../../../types'; -type SnoozeUnit = 'm' | 'h' | 'd' | 'w' | 'M'; +export type SnoozeUnit = 'm' | 'h' | 'd' | 'w' | 'M'; const SNOOZE_END_TIME_FORMAT = 'LL @ LT'; type DropdownRuleRecord = Pick; @@ -48,6 +48,7 @@ export interface ComponentOpts { isEditable: boolean; previousSnoozeInterval?: string | null; direction?: 'column' | 'row'; + hideSnoozeOption?: boolean; } const COMMON_SNOOZE_TIMES: Array<[number, SnoozeUnit]> = [ @@ -58,9 +59,9 @@ const COMMON_SNOOZE_TIMES: Array<[number, SnoozeUnit]> = [ ]; const PREV_SNOOZE_INTERVAL_KEY = 'triggersActionsUi_previousSnoozeInterval'; -const usePreviousSnoozeInterval: (p?: string | null) => [string | null, (n: string) => void] = ( - propsInterval -) => { +export const usePreviousSnoozeInterval: ( + p?: string | null +) => [string | null, (n: string) => void] = (propsInterval) => { const intervalFromStorage = localStorage.getItem(PREV_SNOOZE_INTERVAL_KEY); const usePropsInterval = typeof propsInterval !== 'undefined'; const interval = usePropsInterval ? propsInterval : intervalFromStorage; @@ -74,7 +75,7 @@ const usePreviousSnoozeInterval: (p?: string | null) => [string | null, (n: stri return [previousSnoozeInterval, storeAndSetPreviousSnoozeInterval]; }; -const isRuleSnoozed = (rule: { isSnoozedUntil?: Date | null; muteAll: boolean }) => +export const isRuleSnoozed = (rule: { isSnoozedUntil?: Date | null; muteAll: boolean }) => Boolean( (rule.isSnoozedUntil && new Date(rule.isSnoozedUntil).getTime() > Date.now()) || rule.muteAll ); @@ -88,6 +89,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ unsnoozeRule, isEditable, previousSnoozeInterval: propsPreviousSnoozeInterval, + hideSnoozeOption = false, direction = 'column', }: ComponentOpts) => { const [isEnabled, setIsEnabled] = useState(rule.enabled); @@ -224,6 +226,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ isSnoozed={isSnoozed} snoozeEndTime={rule.isSnoozedUntil} previousSnoozeInterval={previousSnoozeInterval} + hideSnoozeOption={hideSnoozeOption} />
) : ( @@ -245,6 +248,7 @@ interface RuleStatusMenuProps { isSnoozed: boolean; snoozeEndTime?: Date | null; previousSnoozeInterval: string | null; + hideSnoozeOption?: boolean; } const RuleStatusMenu: React.FunctionComponent = ({ @@ -255,6 +259,7 @@ const RuleStatusMenu: React.FunctionComponent = ({ isSnoozed, snoozeEndTime, previousSnoozeInterval, + hideSnoozeOption = false, }) => { const enableRule = useCallback(() => { if (isSnoozed) { @@ -290,6 +295,44 @@ const RuleStatusMenu: React.FunctionComponent = ({ ); } + const getSnoozeMenuItem = () => { + if (!hideSnoozeOption) { + return [ + { + name: snoozeButtonTitle, + icon: isEnabled && isSnoozed ? 'check' : 'empty', + panel: 1, + disabled: !isEnabled, + 'data-test-subj': 'statusDropdownSnoozeItem', + }, + ]; + } + return []; + }; + + const getSnoozePanel = () => { + if (!hideSnoozeOption) { + return [ + { + id: 1, + width: 360, + title: SNOOZE, + content: ( + + + + ), + }, + ]; + } + return []; + }; + const panels = [ { id: 0, @@ -307,28 +350,10 @@ const RuleStatusMenu: React.FunctionComponent = ({ onClick: disableRule, 'data-test-subj': 'statusDropdownDisabledItem', }, - { - name: snoozeButtonTitle, - icon: isEnabled && isSnoozed ? 'check' : 'empty', - panel: 1, - disabled: !isEnabled, - 'data-test-subj': 'statusDropdownSnoozeItem', - }, + ...getSnoozeMenuItem(), ], }, - { - id: 1, - width: 360, - title: SNOOZE, - content: ( - - ), - }, + ...getSnoozePanel(), ]; return ; @@ -336,13 +361,15 @@ const RuleStatusMenu: React.FunctionComponent = ({ interface SnoozePanelProps { interval?: string; + isLoading?: boolean; applySnooze: (value: number | -1, unit?: SnoozeUnit) => void; showCancel: boolean; previousSnoozeInterval: string | null; } -const SnoozePanel: React.FunctionComponent = ({ +export const SnoozePanel: React.FunctionComponent = ({ interval = '3d', + isLoading = false, applySnooze, showCancel, previousSnoozeInterval, @@ -394,9 +421,9 @@ const SnoozePanel: React.FunctionComponent = ({ ); return ( - + <> - + = ({ /> - + {i18n.translate('xpack.triggersActionsUI.sections.rulesList.applySnooze', { defaultMessage: 'Apply', })} @@ -471,7 +502,12 @@ const SnoozePanel: React.FunctionComponent = ({ - + Cancel snooze @@ -479,11 +515,11 @@ const SnoozePanel: React.FunctionComponent = ({ )} - + ); }; -const futureTimeToInterval = (time?: Date | null) => { +export const futureTimeToInterval = (time?: Date | null) => { if (!time) return; const relativeTime = moment(time).locale('en').fromNow(true); const [valueStr, unitStr] = relativeTime.split(' '); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx index f1f2957f9cada..a7d3bdfb8e2e0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; +import { EuiFilterButton, EuiSelectableListItem } from '@elastic/eui'; import { RuleStatusFilter } from './rule_status_filter'; const onChangeMock = jest.fn(); -describe('rule_state_filter', () => { +describe('RuleStatusFilter', () => { beforeEach(() => { onChangeMock.mockReset(); }); @@ -22,7 +22,7 @@ describe('rule_state_filter', () => { ); - expect(wrapper.find(EuiFilterSelectItem).exists()).toBeFalsy(); + expect(wrapper.find(EuiSelectableListItem).exists()).toBeFalsy(); expect(wrapper.find(EuiFilterButton).exists()).toBeTruthy(); expect(wrapper.find('.euiNotificationBadge').text()).toEqual('0'); @@ -37,7 +37,7 @@ describe('rule_state_filter', () => { wrapper.find(EuiFilterButton).simulate('click'); - const statusItems = wrapper.find(EuiFilterSelectItem); + const statusItems = wrapper.find(EuiSelectableListItem); expect(statusItems.length).toEqual(3); }); @@ -48,17 +48,17 @@ describe('rule_state_filter', () => { wrapper.find(EuiFilterButton).simulate('click'); - wrapper.find(EuiFilterSelectItem).at(0).simulate('click'); + wrapper.find(EuiSelectableListItem).at(0).simulate('click'); expect(onChangeMock).toHaveBeenCalledWith(['enabled']); wrapper.setProps({ selectedStatuses: ['enabled'], }); - wrapper.find(EuiFilterSelectItem).at(0).simulate('click'); + wrapper.find(EuiSelectableListItem).at(0).simulate('click'); expect(onChangeMock).toHaveBeenCalledWith([]); - wrapper.find(EuiFilterSelectItem).at(1).simulate('click'); + wrapper.find(EuiSelectableListItem).at(1).simulate('click'); expect(onChangeMock).toHaveBeenCalledWith(['enabled', 'disabled']); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx index 6d286ec6d09d7..f26b3f54c587e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx @@ -6,7 +6,13 @@ */ import React, { useState, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiFilterButton, EuiPopover, EuiFilterGroup, EuiFilterSelectItem } from '@elastic/eui'; +import { + EuiFilterButton, + EuiPopover, + EuiFilterGroup, + EuiSelectableListItem, + EuiButtonEmpty, +} from '@elastic/eui'; import { RuleStatus } from '../../../../types'; const statuses: RuleStatus[] = ['enabled', 'disabled', 'snoozed']; @@ -53,6 +59,24 @@ export const RuleStatusFilter = (props: RuleStatusFilterProps) => { setIsPopoverOpen((prevIsOpen) => !prevIsOpen); }, [setIsPopoverOpen]); + const renderClearAll = () => { + return ( +
+ onChange([])} + > + Clear all + +
+ ); + }; + return ( { > } @@ -77,7 +101,7 @@ export const RuleStatusFilter = (props: RuleStatusFilterProps) => {
{statuses.map((status) => { return ( - { checked={selectedStatuses.includes(status) ? 'on' : undefined} > {status} - + ); })} + {renderClearAll()}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx index 636bcaf1acb22..47b93ff19c6ea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx @@ -9,7 +9,6 @@ import React, { useMemo, useState, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiSelectable, - EuiFilterGroup, EuiFilterButton, EuiPopover, EuiSelectableProps, @@ -103,29 +102,32 @@ export const RuleTagFilter = (props: RuleTagFilterProps) => { }; return ( - - - - {(list, search) => ( - <> - {search} - - {list} - - )} - - - + + + {(list, search) => ( + <> + {search} + + {list} + + )} + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 7827033138fbb..893d6cf7bc5ad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -365,7 +365,7 @@ describe('Update Api Key', () => { fireEvent.click(screen.getByText('Update')); }); expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); - expect(loadRules).toHaveBeenCalledTimes(2); + expect(loadRules).toHaveBeenCalledTimes(3); expect(screen.queryByText("You can't recover the old API key")).not.toBeInTheDocument(); expect(addSuccess).toHaveBeenCalledWith('API key has been updated'); }); @@ -390,7 +390,7 @@ describe('Update Api Key', () => { fireEvent.click(screen.getByText('Update')); }); expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); - expect(loadRules).toHaveBeenCalledTimes(2); + expect(loadRules).toHaveBeenCalledTimes(3); expect( screen.queryByText('You will not be able to recover the old API key') ).not.toBeInTheDocument(); @@ -514,7 +514,6 @@ describe('rules_list component with items', () => { // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; wrapper = mountWithIntl(); - await act(async () => { await nextTick(); wrapper.update(); @@ -561,7 +560,7 @@ describe('rules_list component with items', () => { .simulate('mouseOver'); // Run the timers so the EuiTooltip will be visible - jest.runAllTimers(); + jest.runOnlyPendingTimers(); wrapper.update(); expect(wrapper.find('.euiToolTipPopover').text()).toBe('Start time of the last run.'); @@ -580,7 +579,7 @@ describe('rules_list component with items', () => { wrapper.find('[data-test-subj="ruleInterval-config-tooltip-0"]').first().simulate('mouseOver'); // Run the timers so the EuiTooltip will be visible - jest.runAllTimers(); + jest.runOnlyPendingTimers(); wrapper.update(); expect(wrapper.find('.euiToolTipPopover').text()).toBe( @@ -605,7 +604,7 @@ describe('rules_list component with items', () => { wrapper.find('[data-test-subj="rulesTableCell-durationTooltip"]').first().simulate('mouseOver'); // Run the timers so the EuiTooltip will be visible - jest.runAllTimers(); + jest.runOnlyPendingTimers(); wrapper.update(); expect(wrapper.find('.euiToolTipPopover').text()).toBe( @@ -627,7 +626,7 @@ describe('rules_list component with items', () => { wrapper.find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]').length ).toEqual(1); - expect(wrapper.find('[data-test-subj="refreshRulesButton"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="rulesListAutoRefresh"]').exists()).toBeTruthy(); expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').first().text()).toEqual( 'Error' @@ -724,7 +723,7 @@ describe('rules_list component with items', () => { .first() .simulate('click'); - jest.runAllTimers(); + jest.runOnlyPendingTimers(); wrapper.update(); // Percentile Selection @@ -740,7 +739,7 @@ describe('rules_list component with items', () => { // Select P95 percentileOptions.at(1).simulate('click'); - jest.runAllTimers(); + jest.runOnlyPendingTimers(); wrapper.update(); expect( @@ -795,18 +794,6 @@ describe('rules_list component with items', () => { jest.clearAllMocks(); }); - it('loads rules when refresh button is clicked', async () => { - await setup(); - wrapper.find('[data-test-subj="refreshRulesButton"]').first().simulate('click'); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(loadRules).toHaveBeenCalled(); - }); - it('renders license errors and manage license modal on click', async () => { global.open = jest.fn(); await setup(); @@ -854,7 +841,7 @@ describe('rules_list component with items', () => { it('sorts rules when clicking the status control column', async () => { await setup(); wrapper - .find('[data-test-subj="tableHeaderCell_enabled_8"] .euiTableHeaderButton') + .find('[data-test-subj="tableHeaderCell_enabled_9"] .euiTableHeaderButton') .first() .simulate('click'); @@ -923,21 +910,37 @@ describe('rules_list component with items', () => { loadRules.mockReset(); await setup(); - expect(loadRules.mock.calls[0][0].ruleStatusesFilter).toEqual([]); + expect(loadRules).toHaveBeenLastCalledWith( + expect.objectContaining({ + ruleStatusesFilter: [], + }) + ); wrapper.find('[data-test-subj="ruleStatusFilterButton"] button').simulate('click'); wrapper.find('[data-test-subj="ruleStatusFilterOption-enabled"]').first().simulate('click'); - expect(loadRules.mock.calls[1][0].ruleStatusesFilter).toEqual(['enabled']); + expect(loadRules).toHaveBeenLastCalledWith( + expect.objectContaining({ + ruleStatusesFilter: ['enabled'], + }) + ); wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click'); - expect(loadRules.mock.calls[2][0].ruleStatusesFilter).toEqual(['enabled', 'snoozed']); + expect(loadRules).toHaveBeenLastCalledWith( + expect.objectContaining({ + ruleStatusesFilter: ['enabled', 'snoozed'], + }) + ); wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click'); - expect(loadRules.mock.calls[3][0].ruleStatusesFilter).toEqual(['enabled']); + expect(loadRules).toHaveBeenLastCalledWith( + expect.objectContaining({ + ruleStatusesFilter: ['enabled'], + }) + ); }); it('does not render the tag filter is the feature flag is off', async () => { @@ -956,7 +959,11 @@ describe('rules_list component with items', () => { loadRules.mockReset(); await setup(); - expect(loadRules.mock.calls[0][0].tagsFilter).toEqual([]); + expect(loadRules).toHaveBeenLastCalledWith( + expect.objectContaining({ + tagsFilter: [], + }) + ); wrapper.find('[data-test-subj="ruleTagFilterButton"] button').simulate('click'); @@ -967,11 +974,19 @@ describe('rules_list component with items', () => { tagFilterListItems.at(0).simulate('click'); - expect(loadRules.mock.calls[1][0].tagsFilter).toEqual(['a']); + expect(loadRules).toHaveBeenLastCalledWith( + expect.objectContaining({ + tagsFilter: ['a'], + }) + ); tagFilterListItems.at(1).simulate('click'); - expect(loadRules.mock.calls[2][0].tagsFilter).toEqual(['a', 'b']); + expect(loadRules).toHaveBeenLastCalledWith( + expect.objectContaining({ + tagsFilter: ['a', 'b'], + }) + ); }); }); @@ -1255,4 +1270,21 @@ describe('rules_list with disabled items', () => { wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().content ).toEqual('This rule type requires a Platinum license.'); }); + + it('clicking the notify badge shows the snooze panel', async () => { + await setup(); + + expect(wrapper.find('[data-test-subj="snoozePanel"]').exists()).toBeFalsy(); + + wrapper + .find('[data-test-subj="rulesTableCell-rulesListNotify"]') + .first() + .simulate('mouseenter'); + + expect(wrapper.find('[data-test-subj="rulesListNotifyBadge"]').exists()).toBeTruthy(); + + wrapper.find('[data-test-subj="rulesListNotifyBadge"]').first().simulate('click'); + + expect(wrapper.find('[data-test-subj="snoozePanel"]').exists()).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 9c3f1415e6641..b8afb2d3124ef 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -8,49 +8,36 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { i18n } from '@kbn/i18n'; -import { capitalize, sortBy } from 'lodash'; import moment from 'moment'; +import { capitalize, sortBy } from 'lodash'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useEffect, useState, useMemo, ReactNode, useCallback } from 'react'; +import React, { useEffect, useState, ReactNode, useCallback, useMemo } from 'react'; import { - EuiBasicTable, EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, - EuiIconTip, + EuiFilterGroup, EuiSpacer, EuiLink, EuiEmptyPrompt, - EuiButtonEmpty, EuiHealth, EuiText, - EuiToolTip, EuiTableSortingType, EuiButtonIcon, EuiHorizontalRule, EuiSelectableOption, EuiIcon, - EuiScreenReaderOnly, - RIGHT_ALIGNMENT, EuiDescriptionList, - EuiTableFieldDataColumnType, - EuiTableComputedColumnType, - EuiTableActionsColumnType, EuiCallOut, } from '@elastic/eui'; import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option'; import { useHistory } from 'react-router-dom'; -import { isEmpty } from 'lodash'; import { RuleExecutionStatus, - RuleExecutionStatusValues, ALERTS_FEATURE_ID, RuleExecutionStatusErrorReasons, - formatDuration, - parseDuration, - MONITORING_HISTORY_LIMIT, } from '@kbn/alerting-plugin/common'; import { ActionType, @@ -69,11 +56,8 @@ import { RuleQuickEditButtonsWithApi as RuleQuickEditButtons } from '../../commo import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; import { TypeFilter } from './type_filter'; import { ActionTypeFilter } from './action_type_filter'; -import { RuleExecutionStatusFilter, getHealthColor } from './rule_execution_status_filter'; +import { RuleExecutionStatusFilter } from './rule_execution_status_filter'; import { - loadRules, - loadRuleAggregations, - loadRuleTags, loadRuleTypes, disableRule, enableRule, @@ -87,23 +71,21 @@ import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capab import { routeToRuleDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; import { EmptyPrompt } from '../../../components/prompts/empty_prompt'; -import { rulesStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR } from '../translations'; +import { ALERT_STATUS_LICENSE_ERROR } from '../translations'; import { useKibana } from '../../../../common/lib/kibana'; import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../common/constants'; import './rules_list.scss'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; import { ManageLicenseModal } from './manage_license_modal'; -import { checkRuleTypeEnabled } from '../../../lib/check_rule_type_enabled'; -import { RuleStatusDropdown } from './rule_status_dropdown'; -import { RuleTagBadge } from './rule_tag_badge'; -import { PercentileSelectablePopover } from './percentile_selectable_popover'; -import { RuleDurationFormat } from './rule_duration_format'; -import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils'; -import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils'; import { triggersActionsUiConfig } from '../../../../common/lib/config_api'; import { RuleTagFilter } from './rule_tag_filter'; import { RuleStatusFilter } from './rule_status_filter'; import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; +import { useLoadRules } from '../../../hooks/use_load_rules'; +import { useLoadTags } from '../../../hooks/use_load_tags'; +import { useLoadRuleAggregations } from '../../../hooks/use_load_rule_aggregations'; +import { RulesListTable, convertRulesToTableItems } from './rules_list_table'; +import { RulesListAutoRefresh } from './rules_list_auto_refresh'; import { UpdateApiKeyModalConfirmation } from '../../../components/update_api_key_modal_confirmation'; const ENTER_KEY = 13; @@ -113,17 +95,6 @@ interface RuleTypeState { isInitialized: boolean; data: RuleTypeIndex; } -interface RuleState { - isLoading: boolean; - data: Rule[]; - totalItemCount: number; -} - -const percentileOrdinals = { - [Percentiles.P50]: '50th', - [Percentiles.P95]: '95th', - [Percentiles.P99]: '99th', -}; export const percentileFields = { [Percentiles.P50]: 'monitoring.execution.calculated_metrics.p50', @@ -149,8 +120,6 @@ export const RulesList: React.FunctionComponent = () => { } = useKibana().services; const canExecuteActions = hasExecuteActionsCapability(capabilities); - const [initialLoad, setInitialLoad] = useState(true); - const [noData, setNoData] = useState(true); const [config, setConfig] = useState({ isUsingSecurity: false }); const [actionTypes, setActionTypes] = useState([]); const [selectedIds, setSelectedIds] = useState([]); @@ -162,16 +131,15 @@ export const RulesList: React.FunctionComponent = () => { const [actionTypesFilter, setActionTypesFilter] = useState([]); const [ruleExecutionStatusesFilter, setRuleExecutionStatusesFilter] = useState([]); const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]); - const [tags, setTags] = useState([]); const [tagsFilter, setTagsFilter] = useState([]); const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); - const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState(-1); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( {} ); const [showErrors, setShowErrors] = useState(false); + const [lastUpdate, setLastUpdate] = useState(''); const isRuleTagFilterEnabled = getIsExperimentalFeatureEnabled('ruleTagFilter'); const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter'); @@ -185,13 +153,6 @@ export const RulesList: React.FunctionComponent = () => { const [percentileOptions, setPercentileOptions] = useState(initialPercentileOptions); - const selectedPercentile = useMemo(() => { - const selectedOption = percentileOptions.find((option) => option.checked === 'on'); - if (selectedOption) { - return Percentiles[selectedOption.key as Percentiles]; - } - }, [percentileOptions]); - const [sort, setSort] = useState['sort']>({ field: 'name', direction: 'asc', @@ -200,27 +161,52 @@ export const RulesList: React.FunctionComponent = () => { licenseType: string; ruleTypeId: string; } | null>(null); - const [rulesStatusesTotal, setRulesStatusesTotal] = useState>( - RuleExecutionStatusValues.reduce( - (prev: Record, status: string) => - ({ - ...prev, - [status]: 0, - } as Record), - {} - ) - ); const [ruleTypesState, setRuleTypesState] = useState({ isLoading: false, isInitialized: false, data: new Map(), }); - const [rulesState, setRulesState] = useState({ - isLoading: false, - data: [], - totalItemCount: 0, - }); + const [rulesToDelete, setRulesToDelete] = useState([]); + + const hasAnyAuthorizedRuleType = useMemo(() => { + return ruleTypesState.isInitialized && ruleTypesState.data.size > 0; + }, [ruleTypesState]); + + const onError = useCallback( + (message: string) => { + toasts.addDanger(message); + }, + [toasts] + ); + + const { rulesState, setRulesState, loadRules, noData, initialLoad } = useLoadRules({ + page, + searchText, + typesFilter, + actionTypesFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + tagsFilter, + sort, + onPage: setPage, + onError, + }); + + const { tags, loadTags } = useLoadTags({ + onError, + }); + + const { loadRuleAggregations, rulesStatusesTotal } = useLoadRuleAggregations({ + searchText, + typesFilter, + actionTypesFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + tagsFilter, + onError, + }); + const [rulesToUpdateAPIKey, setRulesToUpdateAPIKey] = useState([]); const onRuleEdit = (ruleItem: RuleTableItem) => { setEditFlyoutVisibility(true); @@ -230,20 +216,30 @@ export const RulesList: React.FunctionComponent = () => { const isRuleTypeEditableInContext = (ruleTypeId: string) => ruleTypeRegistry.has(ruleTypeId) ? !ruleTypeRegistry.get(ruleTypeId).requiresAppContext : false; - useEffect(() => { - loadRulesData(); + const loadData = useCallback(async () => { + if (!ruleTypesState || !hasAnyAuthorizedRuleType) { + return; + } + await loadRules(); + await loadRuleAggregations(); + if (isRuleStatusFilterEnabled) { + await loadTags(); + } + setLastUpdate(moment().format()); }, [ + loadRules, + loadTags, + loadRuleAggregations, + setLastUpdate, + isRuleStatusFilterEnabled, + hasAnyAuthorizedRuleType, ruleTypesState, - page, - searchText, - percentileOptions, - JSON.stringify(typesFilter), - JSON.stringify(actionTypesFilter), - JSON.stringify(ruleExecutionStatusesFilter), - JSON.stringify(ruleStatusesFilter), - JSON.stringify(tagsFilter), ]); + useEffect(() => { + loadData(); + }, [loadData, percentileOptions]); + useEffect(() => { (async () => { try { @@ -289,218 +285,6 @@ export const RulesList: React.FunctionComponent = () => { })(); }, []); - async function loadRulesData() { - const hasAnyAuthorizedRuleType = ruleTypesState.isInitialized && ruleTypesState.data.size > 0; - if (hasAnyAuthorizedRuleType) { - setRulesState({ ...rulesState, isLoading: true }); - try { - const rulesResponse = await loadRules({ - http, - page, - searchText, - typesFilter, - actionTypesFilter, - ruleExecutionStatusesFilter, - ruleStatusesFilter, - tagsFilter, - sort, - }); - await loadRuleTagsAggs(); - await loadRuleAggs(); - setRulesState({ - isLoading: false, - data: rulesResponse.data, - totalItemCount: rulesResponse.total, - }); - - if (!rulesResponse.data?.length && page.index > 0) { - setPage({ ...page, index: 0 }); - } - - const isFilterApplied = !( - isEmpty(searchText) && - isEmpty(typesFilter) && - isEmpty(actionTypesFilter) && - isEmpty(ruleExecutionStatusesFilter) && - isEmpty(ruleStatusesFilter) && - isEmpty(tagsFilter) - ); - - setNoData(rulesResponse.data.length === 0 && !isFilterApplied); - } catch (e) { - toasts.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.unableToLoadRulesMessage', - { - defaultMessage: 'Unable to load rules', - } - ), - }); - setRulesState({ ...rulesState, isLoading: false }); - } - setInitialLoad(false); - } - } - - async function loadRuleAggs() { - try { - const rulesAggs = await loadRuleAggregations({ - http, - searchText, - typesFilter, - actionTypesFilter, - ruleExecutionStatusesFilter, - ruleStatusesFilter, - tagsFilter, - }); - if (rulesAggs?.ruleExecutionStatus) { - setRulesStatusesTotal(rulesAggs.ruleExecutionStatus); - } - } catch (e) { - toasts.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleStatusInfoMessage', - { - defaultMessage: 'Unable to load rule status info', - } - ), - }); - } - } - - async function loadRuleTagsAggs() { - if (!isRuleTagFilterEnabled) { - return; - } - try { - const ruleTagsAggs = await loadRuleTags({ http }); - if (ruleTagsAggs?.ruleTags) { - setTags(ruleTagsAggs.ruleTags); - } - } catch (e) { - toasts.addDanger({ - title: i18n.translate('xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTags', { - defaultMessage: 'Unable to load rule tags', - }), - }); - } - } - - const renderRuleStatusDropdown = (ruleEnabled: boolean | undefined, item: RuleTableItem) => { - return ( - await disableRule({ http, id: item.id })} - enableRule={async () => await enableRule({ http, id: item.id })} - snoozeRule={async (snoozeEndTime: string | -1, interval: string | null) => { - await snoozeRule({ http, id: item.id, snoozeEndTime }); - }} - unsnoozeRule={async () => await unsnoozeRule({ http, id: item.id })} - rule={item} - onRuleChanged={() => loadRulesData()} - isEditable={item.isEditable && isRuleTypeEditableInContext(item.ruleTypeId)} - /> - ); - }; - - const renderRuleExecutionStatus = (executionStatus: RuleExecutionStatus, item: RuleTableItem) => { - const healthColor = getHealthColor(executionStatus.status); - const tooltipMessage = - executionStatus.status === 'error' ? `Error: ${executionStatus?.error?.message}` : null; - const isLicenseError = - executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License; - const statusMessage = isLicenseError - ? ALERT_STATUS_LICENSE_ERROR - : rulesStatusesTranslationsMapping[executionStatus.status]; - - const health = ( - - {statusMessage} - - ); - - const healthWithTooltip = tooltipMessage ? ( - - {health} - - ) : ( - health - ); - - return ( - - {healthWithTooltip} - {isLicenseError && ( - - - setManageLicenseModalOpts({ - licenseType: ruleTypesState.data.get(item.ruleTypeId)?.minimumLicenseRequired!, - ruleTypeId: item.ruleTypeId, - }) - } - > - - - - )} - - ); - }; - - const renderPercentileColumnName = () => { - return ( - - - - {selectedPercentile}  - - - - - - ); - }; - - const renderPercentileCellValue = (value: number) => { - return ( - - - - ); - }; - - const getPercentileColumn = () => { - return { - mobileOptions: { header: false }, - field: percentileFields[selectedPercentile!], - width: '16%', - name: renderPercentileColumnName(), - 'data-test-subj': 'rulesTableCell-ruleExecutionPercentile', - sortable: true, - truncateText: false, - render: renderPercentileCellValue, - }; - }; - const buildErrorListItems = (_executionStatus: RuleExecutionStatus) => { const hasErrorMessage = _executionStatus.status === 'error'; const errorMessage = _executionStatus?.error?.message; @@ -563,383 +347,6 @@ export const RulesList: React.FunctionComponent = () => { }); }, [showErrors, rulesState]); - const getRulesTableColumns = (): Array< - | EuiTableFieldDataColumnType - | EuiTableComputedColumnType - | EuiTableActionsColumnType - > => { - return [ - { - field: 'name', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.nameTitle', - { defaultMessage: 'Name' } - ), - sortable: true, - truncateText: true, - width: '30%', - 'data-test-subj': 'rulesTableCell-name', - render: (name: string, rule: RuleTableItem) => { - const ruleType = ruleTypesState.data.get(rule.ruleTypeId); - const checkEnabledResult = checkRuleTypeEnabled(ruleType); - const link = ( - <> - - - - - { - history.push(routeToRuleDetails.replace(`:ruleId`, rule.id)); - }} - > - {name} - - - - {!checkEnabledResult.isEnabled && ( - - )} - - - - - - {rule.ruleType} - - - - - ); - return <>{link}; - }, - }, - { - field: 'tags', - name: '', - sortable: false, - width: '50px', - 'data-test-subj': 'rulesTableCell-tagsPopover', - render: (ruleTags: string[], item: RuleTableItem) => { - return ruleTags.length > 0 ? ( - setTagPopoverOpenIndex(item.index)} - onClose={() => setTagPopoverOpenIndex(-1)} - /> - ) : null; - }, - }, - { - field: 'executionStatus.lastExecutionDate', - name: ( - - - Last run{' '} - - - - ), - sortable: true, - width: '15%', - 'data-test-subj': 'rulesTableCell-lastExecutionDate', - render: (date: Date) => { - if (date) { - return ( - <> - - - {moment(date).format('MMM D, YYYY HH:mm:ssa')} - - - - {moment(date).fromNow()} - - - - - ); - } - }, - }, - { - field: 'schedule.interval', - width: '6%', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.scheduleTitle', - { defaultMessage: 'Interval' } - ), - sortable: false, - truncateText: false, - 'data-test-subj': 'rulesTableCell-interval', - render: (interval: string, item: RuleTableItem) => { - const durationString = formatDuration(interval); - return ( - <> - - {durationString} - - {item.showIntervalWarning && ( - - { - if (item.isEditable && isRuleTypeEditableInContext(item.ruleTypeId)) { - onRuleEdit(item); - } - }} - iconType="flag" - aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.intervalIconAriaLabel', - { defaultMessage: 'Below configured minimum interval' } - )} - /> - - )} - - - - ); - }, - }, - { - field: 'executionStatus.lastDuration', - width: '12%', - name: ( - - - Duration{' '} - - - - ), - sortable: true, - truncateText: false, - 'data-test-subj': 'rulesTableCell-duration', - render: (value: number, item: RuleTableItem) => { - const showDurationWarning = shouldShowDurationWarning( - ruleTypesState.data.get(item.ruleTypeId), - value - ); - - return ( - <> - {} - {showDurationWarning && ( - - )} - - ); - }, - }, - getPercentileColumn(), - { - field: 'monitoring.execution.calculated_metrics.success_ratio', - width: '12%', - name: ( - - - Success ratio{' '} - - - - ), - sortable: true, - truncateText: false, - 'data-test-subj': 'rulesTableCell-successRatio', - render: (value: number) => { - return ( - - {value !== undefined ? getFormattedSuccessRatio(value) : 'N/A'} - - ); - }, - }, - { - field: 'executionStatus.status', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastResponseTitle', - { defaultMessage: 'Last response' } - ), - sortable: true, - truncateText: false, - width: '120px', - 'data-test-subj': 'rulesTableCell-lastResponse', - render: (_executionStatus: RuleExecutionStatus, item: RuleTableItem) => { - return renderRuleExecutionStatus(item.executionStatus, item); - }, - }, - { - field: 'enabled', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.stateTitle', - { defaultMessage: 'State' } - ), - sortable: true, - truncateText: false, - width: '10%', - 'data-test-subj': 'rulesTableCell-status', - render: (_enabled: boolean | undefined, item: RuleTableItem) => { - return renderRuleStatusDropdown(item.enabled, item); - }, - }, - { - name: '', - width: '90px', - render(item: RuleTableItem) { - return ( - - - - {item.isEditable && isRuleTypeEditableInContext(item.ruleTypeId) ? ( - - onRuleEdit(item)} - iconType={'pencil'} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editAriaLabel', - { defaultMessage: 'Edit' } - )} - /> - - ) : null} - {item.isEditable ? ( - - setRulesToDelete([item.id])} - iconType={'trash'} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteAriaLabel', - { defaultMessage: 'Delete' } - )} - /> - - ) : null} - - - - loadRulesData()} - setRulesToDelete={setRulesToDelete} - onEditRule={() => onRuleEdit(item)} - onUpdateAPIKey={setRulesToUpdateAPIKey} - /> - - - ); - }, - }, - { - align: RIGHT_ALIGNMENT, - width: '40px', - isExpander: true, - name: ( - - Expand rows - - ), - render: (item: RuleTableItem) => { - const _executionStatus = item.executionStatus; - const hasErrorMessage = _executionStatus.status === 'error'; - const isLicenseError = - _executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License; - - return isLicenseError || hasErrorMessage ? ( - toggleErrorMessage(_executionStatus, item)} - aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'} - iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} - /> - ) : null; - }, - }, - ]; - }; - const authorizedRuleTypes = [...ruleTypesState.data.values()]; const authorizedToCreateAnyRules = authorizedRuleTypes.some( (ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.all @@ -979,13 +386,29 @@ export const RulesList: React.FunctionComponent = () => { return []; }; - const getRuleStatusFilter = () => { + const renderRuleStatusFilter = () => { if (isRuleStatusFilterEnabled) { - return [ - , - ]; + return ( + + ); } - return []; + return null; + }; + + const onDisableRule = (rule: RuleTableItem) => { + return disableRule({ http, id: rule.id }); + }; + + const onEnableRule = (rule: RuleTableItem) => { + return enableRule({ http, id: rule.id }); + }; + + const onSnoozeRule = (rule: RuleTableItem, snoozeEndTime: string | -1) => { + return snoozeRule({ http, id: rule.id, snoozeEndTime }); + }; + + const onUnsnoozeRule = (rule: RuleTableItem) => { + return unsnoozeRule({ http, id: rule.id }); }; const toolsRight = [ @@ -999,8 +422,6 @@ export const RulesList: React.FunctionComponent = () => { }) )} />, - ...getRuleTagFilter(), - ...getRuleStatusFilter(), { selectedStatuses={ruleExecutionStatusesFilter} onChange={(ids: string[]) => setRuleExecutionStatusesFilter(ids)} />, - - - , + ...getRuleTagFilter(), ]; const authorizedToModifySelectedRules = selectedIds.length @@ -1074,7 +484,7 @@ export const RulesList: React.FunctionComponent = () => { })} onPerformingAction={() => setIsPerformingAction(true)} onActionPerformed={() => { - loadRulesData(); + loadData(); setIsPerformingAction(false); }} setRulesToDelete={setRulesToDelete} @@ -1119,20 +529,19 @@ export const RulesList: React.FunctionComponent = () => { )} /> + {renderRuleStatusFilter()} - + {toolsRight.map((tool, index: number) => ( - - {tool} - + {tool} ))} - + - + { /> + {rulesStatusesTotal.error > 0 && ( @@ -1235,64 +645,66 @@ export const RulesList: React.FunctionComponent = () => { )} - - ({ - 'data-test-subj': 'rule-row', - className: !ruleTypesState.data.get(item.ruleTypeId)?.enabledInLicense - ? 'actRulesList__tableRowDisabled' - : '', - })} - cellProps={(item: RuleTableItem) => ({ - 'data-test-subj': 'cell', - className: !ruleTypesState.data.get(item.ruleTypeId)?.enabledInLicense - ? 'actRulesList__tableCellDisabled' - : '', - })} - data-test-subj="rulesList" - pagination={{ - pageIndex: page.index, - pageSize: page.size, - /* Don't display rule count until we have the rule types initialized */ - totalItemCount: ruleTypesState.isInitialized === false ? 0 : rulesState.totalItemCount, - }} - selection={{ - selectable: (rule: RuleTableItem) => rule.isEditable, - onSelectionChange(updatedSelectedItemsList: RuleTableItem[]) { - setSelectedIds(updatedSelectedItemsList.map((item) => item.id)); - }, + loadData()} + onRuleClick={(rule) => { + history.push(routeToRuleDetails.replace(`:ruleId`, rule.id)); }} - onChange={({ - page: changedPage, - sort: changedSort, - }: { - page?: Pagination; - sort?: EuiTableSortingType['sort']; - }) => { - if (changedPage) { - setPage(changedPage); - } - if (changedSort) { - setSort(changedSort); + onRuleEditClick={(rule) => { + if (rule.isEditable && isRuleTypeEditableInContext(rule.ruleTypeId)) { + onRuleEdit(rule); } }} - itemIdToExpandedRowMap={itemIdToExpandedRowMap} - isExpandable={true} + onRuleDeleteClick={(rule) => setRulesToDelete([rule.id])} + onManageLicenseClick={(rule) => + setManageLicenseModalOpts({ + licenseType: ruleTypesState.data.get(rule.ruleTypeId)?.minimumLicenseRequired!, + ruleTypeId: rule.ruleTypeId, + }) + } + onSelectionChange={(updatedSelectedItemsList) => + setSelectedIds(updatedSelectedItemsList.map((item) => item.id)) + } + onPercentileOptionsChange={setPercentileOptions} + onDisableRule={onDisableRule} + onEnableRule={onEnableRule} + onSnoozeRule={onSnoozeRule} + onUnsnoozeRule={onUnsnoozeRule} + renderCollapsedItemActions={(rule) => ( + loadData()} + setRulesToDelete={setRulesToDelete} + onEditRule={() => onRuleEdit(rule)} + onUpdateAPIKey={setRulesToUpdateAPIKey} + /> + )} + renderRuleError={(rule) => { + const _executionStatus = rule.executionStatus; + const hasErrorMessage = _executionStatus.status === 'error'; + const isLicenseError = + _executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License; + + return isLicenseError || hasErrorMessage ? ( + toggleErrorMessage(_executionStatus, rule)} + aria-label={itemIdToExpandedRowMap[rule.id] ? 'Collapse' : 'Expand'} + iconType={itemIdToExpandedRowMap[rule.id] ? 'arrowUp' : 'arrowDown'} + /> + ) : null; + }} + config={config} /> {manageLicenseModalOpts && ( { onDeleted={async () => { setRulesToDelete([]); setSelectedIds([]); - await loadRulesData(); + await loadData(); }} onErrors={async () => { - // Refresh the rules from the server, some rules may have been deleted - await loadRulesData(); + // Refresh the rules from the server, some rules may have beend deleted + await loadData(); setRulesToDelete([]); }} onCancel={() => { @@ -1364,7 +776,7 @@ export const RulesList: React.FunctionComponent = () => { }} onUpdated={async () => { setRulesToUpdateAPIKey([]); - await loadRulesData(); + await loadData(); }} /> @@ -1378,7 +790,7 @@ export const RulesList: React.FunctionComponent = () => { actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} ruleTypeIndex={ruleTypesState.data} - onSave={loadRulesData} + onSave={loadData} /> )} {editFlyoutVisible && currentRuleToEdit && ( @@ -1392,7 +804,7 @@ export const RulesList: React.FunctionComponent = () => { ruleType={ ruleTypesState.data.get(currentRuleToEdit.ruleTypeId) as RuleType } - onSave={loadRulesData} + onSave={loadData} /> )} @@ -1427,30 +839,3 @@ const noPermissionPrompt = ( function filterRulesById(rules: Rule[], ids: string[]): Rule[] { return rules.filter((rule) => ids.includes(rule.id)); } - -interface ConvertRulesToTableItemsOpts { - rules: Rule[]; - ruleTypeIndex: RuleTypeIndex; - canExecuteActions: boolean; - config: TriggersActionsUiConfig; -} - -function convertRulesToTableItems(opts: ConvertRulesToTableItemsOpts): RuleTableItem[] { - const { rules, ruleTypeIndex, canExecuteActions, config } = opts; - const minimumDuration = config.minimumScheduleInterval - ? parseDuration(config.minimumScheduleInterval.value) - : 0; - return rules.map((rule, index: number) => { - return { - ...rule, - index, - actionsCount: rule.actions.length, - ruleType: ruleTypeIndex.get(rule.ruleTypeId)?.name ?? rule.ruleTypeId, - isEditable: - hasAllPrivilege(rule, ruleTypeIndex.get(rule.ruleTypeId)) && - (canExecuteActions || (!canExecuteActions && !rule.actions.length)), - enabledInLicense: !!ruleTypeIndex.get(rule.ruleTypeId)?.enabledInLicense, - showIntervalWarning: parseDuration(rule.schedule.interval) < minimumDuration, - }; - }); -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.test.tsx new file mode 100644 index 0000000000000..9e17561ce652b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import moment from 'moment'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { act } from 'react-dom/test-utils'; +import { RulesListAutoRefresh } from './rules_list_auto_refresh'; + +const onRefresh = jest.fn(); + +describe('RulesListAutoRefresh', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders the update text correctly', async () => { + jest.useFakeTimers('modern').setSystemTime(moment('1990-01-01').toDate()); + + const wrapper = mountWithIntl( + + ); + + expect( + wrapper.find('[data-test-subj="rulesListAutoRefresh-lastUpdateText"]').first().text() + ).toEqual('Updated a few seconds ago'); + + await act(async () => { + jest.advanceTimersByTime(1 * 60 * 1000); + }); + + expect( + wrapper.find('[data-test-subj="rulesListAutoRefresh-lastUpdateText"]').first().text() + ).toEqual('Updated a minute ago'); + + await act(async () => { + jest.advanceTimersByTime(1 * 60 * 1000); + }); + + expect( + wrapper.find('[data-test-subj="rulesListAutoRefresh-lastUpdateText"]').first().text() + ).toEqual('Updated 2 minutes ago'); + + await act(async () => { + jest.runOnlyPendingTimers(); + }); + }); + + it('calls onRefresh when it auto refreshes', async () => { + jest.useFakeTimers('modern').setSystemTime(moment('1990-01-01').toDate()); + + mountWithIntl( + + ); + + expect(onRefresh).toHaveBeenCalledTimes(0); + + await act(async () => { + jest.advanceTimersByTime(1000); + }); + + expect(onRefresh).toHaveBeenCalledTimes(1); + + await act(async () => { + jest.advanceTimersByTime(1000); + }); + + expect(onRefresh).toHaveBeenCalledTimes(2); + + await act(async () => { + jest.advanceTimersByTime(10 * 1000); + }); + + expect(onRefresh).toHaveBeenCalledTimes(12); + + await act(async () => { + jest.runOnlyPendingTimers(); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.tsx new file mode 100644 index 0000000000000..eea8d8e5f1bbe --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useEffect, useState, useRef } from 'react'; +import moment from 'moment'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiAutoRefreshButton } from '@elastic/eui'; + +interface RulesListAutoRefreshProps { + lastUpdate: string; + initialUpdateInterval?: number; + onRefresh: () => void; +} + +const flexGroupStyle = { + marginLeft: 'auto', +}; + +const getLastUpdateText = (lastUpdate: string) => { + if (!moment(lastUpdate).isValid()) { + return ''; + } + return i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.rulesListAutoRefresh.lastUpdateText', + { + defaultMessage: 'Updated {lastUpdateText}', + values: { + lastUpdateText: moment(lastUpdate).fromNow(), + }, + } + ); +}; + +const TEXT_UPDATE_INTERVAL = 60 * 1000; +const DEFAULT_REFRESH_INTERVAL = 5 * 60 * 1000; +const MIN_REFRESH_INTERVAL = 1000; + +export const RulesListAutoRefresh = (props: RulesListAutoRefreshProps) => { + const { lastUpdate, initialUpdateInterval = DEFAULT_REFRESH_INTERVAL, onRefresh } = props; + + const [isPaused, setIsPaused] = useState(false); + const [refreshInterval, setRefreshInterval] = useState( + Math.max(initialUpdateInterval, MIN_REFRESH_INTERVAL) + ); + const [lastUpdateText, setLastUpdateText] = useState(''); + + const cachedOnRefresh = useRef<() => void>(() => {}); + const textUpdateTimeout = useRef(); + const refreshTimeout = useRef(); + + useEffect(() => { + cachedOnRefresh.current = onRefresh; + }, [onRefresh]); + + useEffect(() => { + setLastUpdateText(getLastUpdateText(lastUpdate)); + + const poll = () => { + textUpdateTimeout.current = window.setTimeout(() => { + setLastUpdateText(getLastUpdateText(lastUpdate)); + poll(); + }, TEXT_UPDATE_INTERVAL); + }; + poll(); + + return () => { + if (textUpdateTimeout.current) { + clearTimeout(textUpdateTimeout.current); + } + }; + }, [lastUpdate, setLastUpdateText]); + + useEffect(() => { + if (isPaused) { + return; + } + + const poll = () => { + refreshTimeout.current = window.setTimeout(() => { + cachedOnRefresh.current(); + poll(); + }, refreshInterval); + }; + poll(); + + return () => { + if (refreshTimeout.current) { + clearTimeout(refreshTimeout.current); + } + }; + }, [isPaused, refreshInterval]); + + const onRefreshChange = useCallback( + ({ isPaused: newIsPaused, refreshInterval: newRefreshInterval }) => { + setIsPaused(newIsPaused); + setRefreshInterval(newRefreshInterval); + }, + [setIsPaused, setRefreshInterval] + ); + + return ( + + + + {lastUpdateText} + + + + + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_notify_badge.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_notify_badge.tsx new file mode 100644 index 0000000000000..1f03c76a7de0b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_notify_badge.tsx @@ -0,0 +1,224 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import moment from 'moment'; +import { EuiButton, EuiButtonIcon, EuiPopover, EuiText, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isRuleSnoozed } from './rule_status_dropdown'; +import { RuleTableItem } from '../../../../types'; +import { + SnoozePanel, + futureTimeToInterval, + usePreviousSnoozeInterval, + SnoozeUnit, +} from './rule_status_dropdown'; + +export interface RulesListNotifyBadgeProps { + rule: RuleTableItem; + isOpen: boolean; + previousSnoozeInterval?: string | null; + onClick: React.MouseEventHandler; + onClose: () => void; + onRuleChanged: () => void; + snoozeRule: (snoozeEndTime: string | -1, interval: string | null) => Promise; + unsnoozeRule: () => Promise; +} + +const openSnoozePanelAriaLabel = i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.openSnoozePanel', + { defaultMessage: 'Open snooze panel' } +); + +export const RulesListNotifyBadge: React.FunctionComponent = (props) => { + const { + rule, + isOpen, + previousSnoozeInterval: propsPreviousSnoozeInterval, + onClick, + onClose, + onRuleChanged, + snoozeRule, + unsnoozeRule, + } = props; + + const { isSnoozedUntil, muteAll } = rule; + + const [previousSnoozeInterval, setPreviousSnoozeInterval] = usePreviousSnoozeInterval( + propsPreviousSnoozeInterval + ); + + const [isLoading, setIsLoading] = useState(false); + + const isSnoozedIndefinitely = muteAll; + + const isSnoozed = useMemo(() => { + return isRuleSnoozed(rule); + }, [rule]); + + const isScheduled = useMemo(() => { + // TODO: Implement scheduled check + return false; + }, []); + + const formattedSnoozeText = useMemo(() => { + if (!isSnoozedUntil) { + return ''; + } + return moment(isSnoozedUntil).format('MMM D'); + }, [isSnoozedUntil]); + + const snoozeTooltipText = useMemo(() => { + if (isSnoozedIndefinitely) { + return i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.snoozedIndefinitelyTooltip', + { defaultMessage: 'Notifications snoozed indefinitely' } + ); + } + if (isScheduled) { + return ''; + // TODO: Implement scheduled tooltip + } + if (isSnoozed) { + return i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.snoozedTooltip', + { + defaultMessage: 'Notifications snoozed for {snoozeTime}', + values: { + snoozeTime: moment(isSnoozedUntil).fromNow(true), + }, + } + ); + } + return ''; + }, [isSnoozedIndefinitely, isScheduled, isSnoozed, isSnoozedUntil]); + + const snoozedButton = useMemo(() => { + return ( + + {formattedSnoozeText} + + ); + }, [formattedSnoozeText, onClick]); + + const scheduledSnoozeButton = useMemo(() => { + // TODO: Implement scheduled snooze button + return ( + + {formattedSnoozeText} + + ); + }, [formattedSnoozeText, onClick]); + + const unsnoozedButton = useMemo(() => { + return ( + + ); + }, [isOpen, onClick]); + + const indefiniteSnoozeButton = useMemo(() => { + return ( + + ); + }, [onClick]); + + const button = useMemo(() => { + if (isScheduled) { + return scheduledSnoozeButton; + } + if (isSnoozedIndefinitely) { + return indefiniteSnoozeButton; + } + if (isSnoozed) { + return snoozedButton; + } + return unsnoozedButton; + }, [ + isSnoozed, + isScheduled, + isSnoozedIndefinitely, + scheduledSnoozeButton, + snoozedButton, + indefiniteSnoozeButton, + unsnoozedButton, + ]); + + const buttonWithToolTip = useMemo(() => { + if (isOpen) { + return button; + } + return {button}; + }, [isOpen, button, snoozeTooltipText]); + + const snoozeRuleAndStoreInterval = useCallback( + (newSnoozeEndTime: string | -1, interval: string | null) => { + if (interval) { + setPreviousSnoozeInterval(interval); + } + return snoozeRule(newSnoozeEndTime, interval); + }, + [setPreviousSnoozeInterval, snoozeRule] + ); + + const onChangeSnooze = useCallback( + async (value: number, unit?: SnoozeUnit) => { + setIsLoading(true); + try { + if (value === -1) { + await snoozeRuleAndStoreInterval(-1, null); + } else if (value !== 0) { + const newSnoozeEndTime = moment().add(value, unit).toISOString(); + await snoozeRuleAndStoreInterval(newSnoozeEndTime, `${value}${unit}`); + } else await unsnoozeRule(); + onRuleChanged(); + } finally { + onClose(); + setIsLoading(false); + } + }, + [onRuleChanged, onClose, snoozeRuleAndStoreInterval, unsnoozeRule, setIsLoading] + ); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx new file mode 100644 index 0000000000000..53a3b4b69f8c0 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx @@ -0,0 +1,724 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useMemo, useState } from 'react'; +import moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiBasicTable, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, + EuiLink, + EuiButtonEmpty, + EuiHealth, + EuiText, + EuiToolTip, + EuiTableSortingType, + EuiButtonIcon, + EuiSelectableOption, + EuiIcon, + EuiScreenReaderOnly, + RIGHT_ALIGNMENT, + EuiTableFieldDataColumnType, + EuiTableComputedColumnType, + EuiTableActionsColumnType, +} from '@elastic/eui'; +import { + RuleExecutionStatus, + RuleExecutionStatusErrorReasons, + formatDuration, + parseDuration, + MONITORING_HISTORY_LIMIT, +} from '@kbn/alerting-plugin/common'; +import { rulesStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR } from '../translations'; +import { getHealthColor } from './rule_execution_status_filter'; +import { + Rule, + RuleTableItem, + RuleTypeIndex, + Pagination, + Percentiles, + TriggersActionsUiConfig, + RuleTypeRegistryContract, +} from '../../../../types'; +import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils'; +import { PercentileSelectablePopover } from './percentile_selectable_popover'; +import { RuleDurationFormat } from './rule_duration_format'; +import { checkRuleTypeEnabled } from '../../../lib/check_rule_type_enabled'; +import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils'; +import { hasAllPrivilege } from '../../../lib/capabilities'; +import { RuleTagBadge } from './rule_tag_badge'; +import { RuleStatusDropdown } from './rule_status_dropdown'; +import { RulesListNotifyBadge } from './rules_list_notify_badge'; + +interface RuleTypeState { + isLoading: boolean; + isInitialized: boolean; + data: RuleTypeIndex; +} + +export interface RuleState { + isLoading: boolean; + data: Rule[]; + totalItemCount: number; +} + +const percentileOrdinals = { + [Percentiles.P50]: '50th', + [Percentiles.P95]: '95th', + [Percentiles.P99]: '99th', +}; + +export const percentileFields = { + [Percentiles.P50]: 'monitoring.execution.calculated_metrics.p50', + [Percentiles.P95]: 'monitoring.execution.calculated_metrics.p95', + [Percentiles.P99]: 'monitoring.execution.calculated_metrics.p99', +}; + +const EMPTY_OBJECT = {}; +const EMPTY_HANDLER = () => {}; +const EMPTY_RENDER = () => null; + +interface ConvertRulesToTableItemsOpts { + rules: Rule[]; + ruleTypeIndex: RuleTypeIndex; + canExecuteActions: boolean; + config: TriggersActionsUiConfig; +} + +export interface RulesListTableProps { + rulesState: RuleState; + ruleTypesState: RuleTypeState; + ruleTypeRegistry: RuleTypeRegistryContract; + isLoading?: boolean; + sort: EuiTableSortingType['sort']; + page: Pagination; + percentileOptions: EuiSelectableOption[]; + canExecuteActions?: boolean; + itemIdToExpandedRowMap?: Record; + config: TriggersActionsUiConfig; + onSort?: (sort: EuiTableSortingType['sort']) => void; + onPage?: (page: Pagination) => void; + onRuleClick?: (rule: RuleTableItem) => void; + onRuleEditClick?: (rule: RuleTableItem) => void; + onRuleDeleteClick?: (rule: RuleTableItem) => void; + onManageLicenseClick?: (rule: RuleTableItem) => void; + onTagClick?: (rule: RuleTableItem) => void; + onTagClose?: (rule: RuleTableItem) => void; + onSelectionChange?: (updatedSelectedItemsList: RuleTableItem[]) => void; + onPercentileOptionsChange?: (options: EuiSelectableOption[]) => void; + onRuleChanged: () => void; + onEnableRule: (rule: RuleTableItem) => Promise; + onDisableRule: (rule: RuleTableItem) => Promise; + onSnoozeRule: (rule: RuleTableItem, snoozeEndTime: string | -1) => Promise; + onUnsnoozeRule: (rule: RuleTableItem) => Promise; + renderCollapsedItemActions?: (rule: RuleTableItem) => React.ReactNode; + renderRuleError?: (rule: RuleTableItem) => React.ReactNode; +} + +interface ConvertRulesToTableItemsOpts { + rules: Rule[]; + ruleTypeIndex: RuleTypeIndex; + canExecuteActions: boolean; + config: TriggersActionsUiConfig; +} + +export function convertRulesToTableItems(opts: ConvertRulesToTableItemsOpts): RuleTableItem[] { + const { rules, ruleTypeIndex, canExecuteActions, config } = opts; + const minimumDuration = config.minimumScheduleInterval + ? parseDuration(config.minimumScheduleInterval.value) + : 0; + return rules.map((rule, index: number) => { + return { + ...rule, + index, + actionsCount: rule.actions.length, + ruleType: ruleTypeIndex.get(rule.ruleTypeId)?.name ?? rule.ruleTypeId, + isEditable: + hasAllPrivilege(rule, ruleTypeIndex.get(rule.ruleTypeId)) && + (canExecuteActions || (!canExecuteActions && !rule.actions.length)), + enabledInLicense: !!ruleTypeIndex.get(rule.ruleTypeId)?.enabledInLicense, + showIntervalWarning: parseDuration(rule.schedule.interval) < minimumDuration, + }; + }); +} + +export const RulesListTable = (props: RulesListTableProps) => { + const { + rulesState, + ruleTypesState, + ruleTypeRegistry, + isLoading = false, + canExecuteActions = false, + sort, + page, + percentileOptions, + itemIdToExpandedRowMap = EMPTY_OBJECT, + config = EMPTY_OBJECT as TriggersActionsUiConfig, + onSort = EMPTY_HANDLER, + onPage = EMPTY_HANDLER, + onRuleClick = EMPTY_HANDLER, + onRuleEditClick = EMPTY_HANDLER, + onRuleDeleteClick = EMPTY_HANDLER, + onManageLicenseClick = EMPTY_HANDLER, + onSelectionChange = EMPTY_HANDLER, + onPercentileOptionsChange = EMPTY_HANDLER, + onRuleChanged = EMPTY_HANDLER, + onEnableRule = EMPTY_HANDLER, + onDisableRule = EMPTY_HANDLER, + onSnoozeRule = EMPTY_HANDLER, + onUnsnoozeRule = EMPTY_HANDLER, + renderCollapsedItemActions = EMPTY_RENDER, + renderRuleError = EMPTY_RENDER, + } = props; + + const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState(-1); + const [currentlyOpenNotify, setCurrentlyOpenNotify] = useState(); + + const selectedPercentile = useMemo(() => { + const selectedOption = percentileOptions.find((option) => option.checked === 'on'); + if (selectedOption) { + return Percentiles[selectedOption.key as Percentiles]; + } + }, [percentileOptions]); + + const renderPercentileColumnName = () => { + return ( + + + + {selectedPercentile}  + + + + + + ); + }; + + const renderPercentileCellValue = (value: number) => { + return ( + + + + ); + }; + + const renderRuleStatusDropdown = (ruleEnabled: boolean | undefined, rule: RuleTableItem) => { + return ( + await onDisableRule(rule)} + enableRule={async () => await onEnableRule(rule)} + snoozeRule={async (snoozeEndTime: string | -1, interval: string | null) => { + await onSnoozeRule(rule, snoozeEndTime); + }} + unsnoozeRule={async () => await onUnsnoozeRule(rule)} + rule={rule} + onRuleChanged={onRuleChanged} + isEditable={rule.isEditable && isRuleTypeEditableInContext(rule.ruleTypeId)} + /> + ); + }; + + const isRuleTypeEditableInContext = (ruleTypeId: string) => + ruleTypeRegistry.has(ruleTypeId) ? !ruleTypeRegistry.get(ruleTypeId).requiresAppContext : false; + + const renderRuleExecutionStatus = (executionStatus: RuleExecutionStatus, rule: RuleTableItem) => { + const healthColor = getHealthColor(executionStatus.status); + const tooltipMessage = + executionStatus.status === 'error' ? `Error: ${executionStatus?.error?.message}` : null; + const isLicenseError = + executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License; + const statusMessage = isLicenseError + ? ALERT_STATUS_LICENSE_ERROR + : rulesStatusesTranslationsMapping[executionStatus.status]; + + const health = ( + + {statusMessage} + + ); + + const healthWithTooltip = tooltipMessage ? ( + + {health} + + ) : ( + health + ); + + return ( + + {healthWithTooltip} + {isLicenseError && ( + + onManageLicenseClick(rule)} + > + + + + )} + + ); + }; + + const getRulesTableColumns = (): Array< + | EuiTableFieldDataColumnType + | EuiTableComputedColumnType + | EuiTableActionsColumnType + > => { + return [ + { + field: 'name', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.nameTitle', + { defaultMessage: 'Name' } + ), + sortable: true, + truncateText: true, + width: '30%', + 'data-test-subj': 'rulesTableCell-name', + render: (name: string, rule: RuleTableItem) => { + const ruleType = ruleTypesState.data.get(rule.ruleTypeId); + const checkEnabledResult = checkRuleTypeEnabled(ruleType); + const link = ( + <> + + + + + onRuleClick(rule)}> + {name} + + + + {!checkEnabledResult.isEnabled && ( + + )} + + + + + + {rule.ruleType} + + + + + ); + return <>{link}; + }, + }, + { + field: 'tags', + name: '', + sortable: false, + width: '50px', + 'data-test-subj': 'rulesTableCell-tagsPopover', + render: (ruleTags: string[], rule: RuleTableItem) => { + return ruleTags.length > 0 ? ( + setTagPopoverOpenIndex(rule.index)} + onClose={() => setTagPopoverOpenIndex(-1)} + /> + ) : null; + }, + }, + { + field: 'executionStatus.lastExecutionDate', + name: ( + + + Last run{' '} + + + + ), + sortable: true, + width: '15%', + 'data-test-subj': 'rulesTableCell-lastExecutionDate', + render: (date: Date) => { + if (date) { + return ( + <> + + + {moment(date).format('MMM D, YYYY HH:mm:ssa')} + + + + {moment(date).fromNow()} + + + + + ); + } + }, + }, + { + name: 'Notify', + width: '16%', + 'data-test-subj': 'rulesTableCell-rulesListNotify', + render: (rule: RuleTableItem) => { + return ( + setCurrentlyOpenNotify(rule.id)} + onClose={() => setCurrentlyOpenNotify('')} + onRuleChanged={onRuleChanged} + snoozeRule={async (snoozeEndTime: string | -1, interval: string | null) => { + await onSnoozeRule(rule, snoozeEndTime); + }} + unsnoozeRule={async () => await onUnsnoozeRule(rule)} + /> + ); + }, + }, + { + field: 'schedule.interval', + width: '6%', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.scheduleTitle', + { defaultMessage: 'Interval' } + ), + sortable: false, + truncateText: false, + 'data-test-subj': 'rulesTableCell-interval', + render: (interval: string, rule: RuleTableItem) => { + const durationString = formatDuration(interval); + return ( + <> + + {durationString} + + {rule.showIntervalWarning && ( + + onRuleEditClick(rule)} + iconType="flag" + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.intervalIconAriaLabel', + { defaultMessage: 'Below configured minimum interval' } + )} + /> + + )} + + + + ); + }, + }, + { + field: 'executionStatus.lastDuration', + width: '12%', + name: ( + + + Duration{' '} + + + + ), + sortable: true, + truncateText: false, + 'data-test-subj': 'rulesTableCell-duration', + render: (value: number, rule: RuleTableItem) => { + const showDurationWarning = shouldShowDurationWarning( + ruleTypesState.data.get(rule.ruleTypeId), + value + ); + + return ( + <> + {} + {showDurationWarning && ( + + )} + + ); + }, + }, + { + mobileOptions: { header: false }, + field: percentileFields[selectedPercentile!], + width: '16%', + name: renderPercentileColumnName(), + 'data-test-subj': 'rulesTableCell-ruleExecutionPercentile', + sortable: true, + truncateText: false, + render: renderPercentileCellValue, + }, + { + field: 'monitoring.execution.calculated_metrics.success_ratio', + width: '12%', + name: ( + + + Success ratio{' '} + + + + ), + sortable: true, + truncateText: false, + 'data-test-subj': 'rulesTableCell-successRatio', + render: (value: number) => { + return ( + + {value !== undefined ? getFormattedSuccessRatio(value) : 'N/A'} + + ); + }, + }, + { + field: 'executionStatus.status', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastResponseTitle', + { defaultMessage: 'Last response' } + ), + sortable: true, + truncateText: false, + width: '120px', + 'data-test-subj': 'rulesTableCell-lastResponse', + render: (_executionStatus: RuleExecutionStatus, rule: RuleTableItem) => { + return renderRuleExecutionStatus(rule.executionStatus, rule); + }, + }, + { + field: 'enabled', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.stateTitle', + { defaultMessage: 'State' } + ), + sortable: true, + truncateText: false, + width: '10%', + 'data-test-subj': 'rulesTableCell-status', + render: (_enabled: boolean | undefined, rule: RuleTableItem) => { + return renderRuleStatusDropdown(rule.enabled, rule); + }, + }, + { + name: '', + width: '90px', + render(rule: RuleTableItem) { + return ( + + + + {rule.isEditable && isRuleTypeEditableInContext(rule.ruleTypeId) ? ( + + onRuleEditClick(rule)} + iconType={'pencil'} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editAriaLabel', + { defaultMessage: 'Edit' } + )} + /> + + ) : null} + {rule.isEditable ? ( + + onRuleDeleteClick(rule)} + iconType={'trash'} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteAriaLabel', + { defaultMessage: 'Delete' } + )} + /> + + ) : null} + + + {renderCollapsedItemActions(rule)} + + ); + }, + }, + { + align: RIGHT_ALIGNMENT, + width: '40px', + isExpander: true, + name: ( + + Expand rows + + ), + render: renderRuleError, + }, + ]; + }; + + return ( + ({ + 'data-test-subj': 'rule-row', + className: !ruleTypesState.data.get(rule.ruleTypeId)?.enabledInLicense + ? 'actRulesList__tableRowDisabled' + : '', + })} + cellProps={(rule: RuleTableItem) => ({ + 'data-test-subj': 'cell', + className: !ruleTypesState.data.get(rule.ruleTypeId)?.enabledInLicense + ? 'actRulesList__tableCellDisabled' + : '', + })} + data-test-subj="rulesList" + pagination={{ + pageIndex: page.index, + pageSize: page.size, + /* Don't display rule count until we have the rule types initialized */ + totalItemCount: ruleTypesState.isInitialized === false ? 0 : rulesState.totalItemCount, + }} + selection={{ + selectable: (rule: RuleTableItem) => rule.isEditable, + onSelectionChange, + }} + onChange={({ + page: changedPage, + sort: changedSort, + }: { + page?: Pagination; + sort?: EuiTableSortingType['sort']; + }) => { + if (changedPage) { + onPage(changedPage); + } + if (changedSort) { + onSort(changedSort); + } + }} + itemIdToExpandedRowMap={itemIdToExpandedRowMap} + isExpandable={true} + /> + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/type_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/type_filter.tsx index 6ce697f65f898..f8cb70745911c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/type_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/type_filter.tsx @@ -7,13 +7,7 @@ import React, { Fragment, useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiFilterGroup, - EuiPopover, - EuiFilterButton, - EuiFilterSelectItem, - EuiTitle, -} from '@elastic/eui'; +import { EuiPopover, EuiFilterButton, EuiFilterSelectItem, EuiTitle } from '@elastic/eui'; interface TypeFilterProps { options: Array<{ @@ -41,53 +35,51 @@ export const TypeFilter: React.FunctionComponent = ({ }, [selectedValues]); return ( - - setIsPopoverOpen(false)} - button={ - 0} - numActiveFilters={selectedValues.length} - numFilters={selectedValues.length} - onClick={() => setIsPopoverOpen(!isPopoverOpen)} - data-test-subj="ruleTypeFilterButton" - > - - - } - > -
- {options.map((groupItem, groupIndex) => ( - - -

{groupItem.groupName}

-
- {groupItem.subOptions.map((item, index) => ( - { - const isPreviouslyChecked = selectedValues.includes(item.value); - if (isPreviouslyChecked) { - setSelectedValues(selectedValues.filter((val) => val !== item.value)); - } else { - setSelectedValues(selectedValues.concat(item.value)); - } - }} - checked={selectedValues.includes(item.value) ? 'on' : undefined} - data-test-subj={`ruleType${item.value}FilterOption`} - > - {item.name} - - ))} -
- ))} -
-
-
+ setIsPopoverOpen(false)} + button={ + 0} + numActiveFilters={selectedValues.length} + numFilters={selectedValues.length} + onClick={() => setIsPopoverOpen(!isPopoverOpen)} + data-test-subj="ruleTypeFilterButton" + > + + + } + > +
+ {options.map((groupItem, groupIndex) => ( + + +

{groupItem.groupName}

+
+ {groupItem.subOptions.map((item, index) => ( + { + const isPreviouslyChecked = selectedValues.includes(item.value); + if (isPreviouslyChecked) { + setSelectedValues(selectedValues.filter((val) => val !== item.value)); + } else { + setSelectedValues(selectedValues.concat(item.value)); + } + }} + checked={selectedValues.includes(item.value) ? 'on' : undefined} + data-test-subj={`ruleType${item.value}FilterOption`} + > + {item.name} + + ))} +
+ ))} +
+
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rules_list.tsx new file mode 100644 index 0000000000000..b315668c4fab9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rules_list.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { RulesList } from '../application/sections'; + +export const getRulesListLazy = () => { + return ; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index 75ca6d8fd2987..605d83a8eb32e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -30,6 +30,7 @@ import { getRuleTagFilterLazy } from './common/get_rule_tag_filter'; import { getRuleStatusFilterLazy } from './common/get_rule_status_filter'; import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge'; import { getRuleEventLogListLazy } from './common/get_rule_event_log_list'; +import { getRulesListLazy } from './common/get_rules_list'; import { getAlertsTableStateLazy } from './common/get_alerts_table_state'; import { AlertsTableStateProps } from './application/sections/alerts_table/alerts_table_state'; @@ -85,6 +86,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { getRuleEventLogList: (props) => { return getRuleEventLogListLazy(props); }, + getRulesList: () => { + return getRulesListLazy(); + }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index f9df34a5e4abb..f2237ff22f4ae 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -35,6 +35,7 @@ import { getRuleTagFilterLazy } from './common/get_rule_tag_filter'; import { getRuleStatusFilterLazy } from './common/get_rule_status_filter'; import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge'; import { getRuleEventLogListLazy } from './common/get_rule_event_log_list'; +import { getRulesListLazy } from './common/get_rules_list'; import { ExperimentalFeaturesService } from './common/experimental_features_service'; import { ExperimentalFeatures, @@ -91,6 +92,7 @@ export interface TriggersAndActionsUIPublicPluginStart { getRuleStatusFilter: (props: RuleStatusFilterProps) => ReactElement; getRuleTagBadge: (props: RuleTagBadgeProps) => ReactElement; getRuleEventLogList: (props: RuleEventLogListProps) => ReactElement; + getRulesList: () => ReactElement; } interface PluginsSetup { @@ -279,6 +281,9 @@ export class Plugin getRuleEventLogList: (props: RuleEventLogListProps) => { return getRuleEventLogListLazy(props); }, + getRulesList: () => { + return getRulesListLazy(); + }, }; } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index c5ed118c105bb..832cf6c7a9078 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -20,5 +20,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./rule_status_filter')); loadTestFile(require.resolve('./rule_tag_badge')); loadTestFile(require.resolve('./rule_event_log_list')); + loadTestFile(require.resolve('./rules_list')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts index 77d57e2819db5..15ea8fc302622 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); - const find = getService('find'); const PageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const esArchiver = getService('esArchiver'); @@ -31,24 +30,5 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const exists = await testSubjects.exists('ruleTagFilter'); expect(exists).to.be(true); }); - - it('should allow tag filters to be selected', async () => { - let badge = await find.byCssSelector('.euiFilterButton__notification'); - expect(await badge.getVisibleText()).to.be('0'); - - await testSubjects.click('ruleTagFilter'); - await testSubjects.click('ruleTagFilterOption-tag1'); - - badge = await find.byCssSelector('.euiFilterButton__notification'); - expect(await badge.getVisibleText()).to.be('1'); - - await testSubjects.click('ruleTagFilterOption-tag2'); - - badge = await find.byCssSelector('.euiFilterButton__notification'); - expect(await badge.getVisibleText()).to.be('2'); - - await testSubjects.click('ruleTagFilterOption-tag1'); - expect(await badge.getVisibleText()).to.be('1'); - }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list.ts new file mode 100644 index 0000000000000..30baba0caaa08 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); + const esArchiver = getService('esArchiver'); + + describe('Rules list', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + await PageObjects.common.navigateToUrlWithBrowserHistory( + 'triggersActions', + '/__components_sandbox' + ); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + }); + + it('shoud load from shareable lazy loader', async () => { + await testSubjects.find('rulesList'); + const exists = await testSubjects.exists('rulesList'); + expect(exists).to.be(true); + }); + }); +}; From 40df1f3dbffa6fd0b50d95ac9663ba158756ae25 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Mon, 23 May 2022 08:45:50 +0200 Subject: [PATCH 066/120] [Osquery] Add labels, move osquery schema link (#132584) --- .../integration/all/add_integration.spec.ts | 4 ++- .../osquery/cypress/screens/live_query.ts | 4 ++- .../osquery/cypress/tasks/live_query.ts | 2 +- .../osquery/public/agents/agents_table.tsx | 28 ++++++++++--------- .../osquery/public/agents/translations.ts | 2 +- .../public/live_queries/form/index.tsx | 1 - .../form/live_query_query_field.tsx | 2 -- .../osquery/public/saved_queries/constants.ts | 14 ++++++++++ .../saved_queries/saved_queries_dropdown.tsx | 16 ++++------- 9 files changed, 42 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/osquery/cypress/integration/all/add_integration.spec.ts b/x-pack/plugins/osquery/cypress/integration/all/add_integration.spec.ts index d6f8e14381bc2..b1a3d26d850d0 100644 --- a/x-pack/plugins/osquery/cypress/integration/all/add_integration.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/all/add_integration.spec.ts @@ -101,7 +101,9 @@ describe('ALL - Add Integration', () => { findFormFieldByRowsLabelAndType('Name', 'Integration'); findFormFieldByRowsLabelAndType('Scheduled agent policies (optional)', '{downArrow} {enter}'); findAndClickButton('Add query'); - cy.react('EuiComboBox', { props: { placeholder: 'Search for saved queries' } }) + cy.react('EuiComboBox', { + props: { placeholder: 'Search for a query to run, or write a new query below' }, + }) .click() .type('{downArrow} {enter}'); cy.contains(/^Save$/).click(); diff --git a/x-pack/plugins/osquery/cypress/screens/live_query.ts b/x-pack/plugins/osquery/cypress/screens/live_query.ts index ce29edc2c9187..d3be652c24c2c 100644 --- a/x-pack/plugins/osquery/cypress/screens/live_query.ts +++ b/x-pack/plugins/osquery/cypress/screens/live_query.ts @@ -14,4 +14,6 @@ export const RESULTS_TABLE = 'osqueryResultsTable'; export const RESULTS_TABLE_BUTTON = 'dataGridFullScreenButton'; export const RESULTS_TABLE_CELL_WRRAPER = 'EuiDataGridHeaderCellWrapper'; export const getSavedQueriesDropdown = () => - cy.react('EuiComboBox', { props: { placeholder: 'Search for saved queries' } }); + cy.react('EuiComboBox', { + props: { placeholder: 'Search for a query to run, or write a new query below' }, + }); diff --git a/x-pack/plugins/osquery/cypress/tasks/live_query.ts b/x-pack/plugins/osquery/cypress/tasks/live_query.ts index d43516be2bc35..3a1f1b0930edf 100644 --- a/x-pack/plugins/osquery/cypress/tasks/live_query.ts +++ b/x-pack/plugins/osquery/cypress/tasks/live_query.ts @@ -13,7 +13,7 @@ export const BIG_QUERY = 'select * from processes, users limit 200;'; export const selectAllAgents = () => { cy.react('AgentsTable').find('input').should('not.be.disabled'); cy.react('AgentsTable EuiComboBox', { - props: { placeholder: 'Select agents or groups' }, + props: { placeholder: 'Select agents or groups to query' }, }).click(); cy.react('EuiFilterSelectItem').contains('All agents').should('exist'); cy.react('AgentsTable EuiComboBox').type('{downArrow}{enter}{esc}'); diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index 75d073c4d9292..f4baf70cf5593 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -7,7 +7,7 @@ import { find } from 'lodash/fp'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { EuiComboBox, EuiHealth, EuiHighlight, EuiSpacer } from '@elastic/eui'; +import { EuiComboBox, EuiHealth, EuiFormRow, EuiHighlight, EuiSpacer } from '@elastic/eui'; import deepEqual from 'fast-deep-equal'; import useDebounce from 'react-use/lib/useDebounce'; @@ -190,18 +190,20 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh return (
- + + + {numAgentsSelected > 0 ? {generateSelectedAgentsMessage(numAgentsSelected)} : ''}
diff --git a/x-pack/plugins/osquery/public/agents/translations.ts b/x-pack/plugins/osquery/public/agents/translations.ts index 209761b4c8bdf..643284596da1d 100644 --- a/x-pack/plugins/osquery/public/agents/translations.ts +++ b/x-pack/plugins/osquery/public/agents/translations.ts @@ -40,7 +40,7 @@ export const AGENT_SELECTION_LABEL = i18n.translate('xpack.osquery.agents.select }); export const SELECT_AGENT_LABEL = i18n.translate('xpack.osquery.agents.selectAgentLabel', { - defaultMessage: `Select agents or groups`, + defaultMessage: `Select agents or groups to query`, }); export const ERROR_ALL_AGENTS = i18n.translate('xpack.osquery.agents.errorSearchDescription', { diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index bba443be9569a..505550508874f 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -254,7 +254,6 @@ const LiveQueryFormComponent: React.FC = ({ disabled={isSavedQueryDisabled} onChange={handleSavedQueryChange} /> - )} = ({ isInvalid={typeof error === 'string'} error={error} fullWidth - labelAppend={} isDisabled={!permissions.writeLiveQueries || disabled} > {!permissions.writeLiveQueries || disabled ? ( diff --git a/x-pack/plugins/osquery/public/saved_queries/constants.ts b/x-pack/plugins/osquery/public/saved_queries/constants.ts index 8edcfd00d1788..5dc23354322cd 100644 --- a/x-pack/plugins/osquery/public/saved_queries/constants.ts +++ b/x-pack/plugins/osquery/public/saved_queries/constants.ts @@ -4,6 +4,20 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { i18n } from '@kbn/i18n'; export const SAVED_QUERIES_ID = 'savedQueryList'; export const SAVED_QUERY_ID = 'savedQuery'; + +export const QUERIES_DROPDOWN_LABEL = i18n.translate( + 'xpack.osquery.savedQueries.dropdown.searchFieldPlaceholder', + { + defaultMessage: `Search for a query to run, or write a new query below`, + } +); +export const QUERIES_DROPDOWN_SEARCH_FIELD_LABEL = i18n.translate( + 'xpack.osquery.savedQueries.dropdown.searchFieldLabel', + { + defaultMessage: `Query`, + } +); diff --git a/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx b/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx index 784a2375ad1a6..6722ade12ad16 100644 --- a/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx @@ -9,9 +9,9 @@ import { find } from 'lodash/fp'; import { EuiCodeBlock, EuiFormRow, EuiComboBox, EuiTextColor } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { SimpleSavedObject } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import styled from 'styled-components'; +import { QUERIES_DROPDOWN_LABEL, QUERIES_DROPDOWN_SEARCH_FIELD_LABEL } from './constants'; +import { OsquerySchemaLink } from '../components/osquery_schema_link'; import { useSavedQueries } from './use_saved_queries'; import { useFormData } from '../shared_imports'; @@ -133,20 +133,14 @@ const SavedQueriesDropdownComponent: React.FC = ({ return ( - } + label={QUERIES_DROPDOWN_SEARCH_FIELD_LABEL} + labelAppend={} fullWidth > Date: Mon, 23 May 2022 10:12:54 +0200 Subject: [PATCH 067/120] [DOCS] Updates alerting authorization docs with info on retaining API keys (#132402) Co-authored-by: Lisa Cawley --- docs/user/alerting/alerting-setup.asciidoc | 64 +++++++++++++++++----- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc index 6643f8d0ec870..9e3fb54e39444 100644 --- a/docs/user/alerting/alerting-setup.asciidoc +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -5,35 +5,47 @@ Set up ++++ -Alerting is automatically enabled in {kib}, but might require some additional configuration. +Alerting is automatically enabled in {kib}, but might require some additional +configuration. [float] [[alerting-prerequisites]] === Prerequisites If you are using an *on-premises* Elastic Stack deployment: -* In the kibana.yml configuration file, add the <> setting. -* For emails to have a footer with a link back to {kib}, set the <> configuration setting. +* In the kibana.yml configuration file, add the +<> +setting. +* For emails to have a footer with a link back to {kib}, set the +<> configuration setting. -If you are using an *on-premises* Elastic Stack deployment with <>: +If you are using an *on-premises* Elastic Stack deployment with +<>: -* If you are unable to access {kib} Alerting, ensure that you have not {ref}/security-settings.html#api-key-service-settings[explicitly disabled API keys]. +* If you are unable to access {kib} Alerting, ensure that you have not +{ref}/security-settings.html#api-key-service-settings[explicitly disabled API keys]. -The alerting framework uses queries that require the `search.allow_expensive_queries` setting to be `true`. See the scripts {ref}/query-dsl-script-query.html#_allow_expensive_queries_4[documentation]. +The alerting framework uses queries that require the +`search.allow_expensive_queries` setting to be `true`. See the scripts +{ref}/query-dsl-script-query.html#_allow_expensive_queries_4[documentation]. [float] [[alerting-setup-production]] === Production considerations and scaling guidance -When relying on alerting and actions as mission critical services, make sure you follow the <>. +When relying on alerting and actions as mission critical services, make sure you +follow the +<>. -See <> for more information on the scalability of Alerting. +See <> for more information on the scalability of +Alerting. [float] [[alerting-security]] === Security -To access alerting in a space, a user must have access to one of the following features: +To access alerting in a space, a user must have access to one of the following +features: * Alerting * <> @@ -43,31 +55,53 @@ To access alerting in a space, a user must have access to one of the following f * <> * <> -See <> for more information on configuring roles that provide access to these features. -Also note that a user will need +read+ privileges for the *Actions and Connectors* feature to attach actions to a rule or to edit a rule that has an action attached to it. +See <> for more information on +configuring roles that provide access to these features. +Also note that a user will need +read+ privileges for the +*Actions and Connectors* feature to attach actions to a rule or to edit a rule +that has an action attached to it. [float] [[alerting-restricting-actions]] ==== Restrict actions -For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and allowlist the hostnames that {kib} can connect with. +For security reasons you may wish to limit the extent to which {kib} can connect +to external services. <> allows you to disable certain +<> and allowlist the hostnames that {kib} can connect with. [float] [[alerting-spaces]] === Space isolation -Rules and connectors are isolated to the {kib} space in which they were created. A rule or connector created in one space will not be visible in another. +Rules and connectors are isolated to the {kib} space in which they were created. +A rule or connector created in one space will not be visible in another. [float] [[alerting-authorization]] === Authorization -Rules are authorized using an <> associated with the last user to edit the rule. This API key captures a snapshot of the user's privileges at the time of edit and is subsequently used to run all background tasks associated with the rule, including condition checks like {es} queries and triggered actions. The following rule actions will re-generate the API key: +Rules are authorized using an <> associated with the last user +to edit the rule. This API key captures a snapshot of the user's privileges at +the time of the edit. They are subsequently used to run all background tasks +associated with the rule, including condition checks like {es} queries and +triggered actions. The following rule actions will re-generate the API key: * Creating a rule * Updating a rule +When you disable a rule, it retains the associated API key which is re-used when +the rule is enabled. If the API key is missing when you enable the rule (for +example, in the case of imported rules), it generates a new key that has your +security privileges. + +You can update an API key manually in +**{stack-manage-app} > {rules-ui}** or in the rule details page by selecting +**Update API key** in the actions menu. + [IMPORTANT] ============================================== -If a rule requires certain privileges, such as index privileges, to run, and a user without those privileges updates the rule, the rule will no longer function. Conversely, if a user with greater or administrator privileges modifies the rule, it will begin running with increased privileges. +If a rule requires certain privileges, such as index privileges, to run, and a +user without those privileges updates the rule, the rule will no longer +function. Conversely, if a user with greater or administrator privileges +modifies the rule, it will begin running with increased privileges. ============================================== From a3646eb2b82e5d790c548882d976e1f16245d118 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Mon, 23 May 2022 10:17:12 +0200 Subject: [PATCH 068/120] [Security Solutions] Refactor breadcrumbs to support new menu structure (#131624) * Refactor breadcrumbs to support new structure * Fix code style * Fix more code style * Fix unit test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/common/components/link_to/index.ts | 3 +- .../breadcrumbs/get_breadcrumbs_for_page.ts | 42 + .../navigation/breadcrumbs/index.test.ts | 1166 +++++++++++------ .../navigation/breadcrumbs/index.ts | 242 ++-- .../common/components/navigation/helpers.ts | 4 +- .../components/navigation/index.test.tsx | 62 +- .../common/components/navigation/index.tsx | 8 - .../index.tsx | 8 - .../detection_engine/rules/utils.test.ts | 29 - .../pages/detection_engine/rules/utils.ts | 40 +- .../public/hosts/pages/details/utils.ts | 24 +- .../public/management/common/breadcrumbs.ts | 2 +- .../public/network/pages/details/index.tsx | 2 +- .../public/network/pages/details/utils.ts | 29 +- .../public/timelines/pages/index.tsx | 25 +- .../public/users/pages/details/utils.ts | 25 +- 16 files changed, 976 insertions(+), 735 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/get_breadcrumbs_for_page.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts index 0db0699628cc0..ba86842106e23 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts @@ -48,7 +48,7 @@ export const useFormatUrl = (page: SecurityPageName) => { return { formatUrl, search }; }; -type GetSecuritySolutionUrl = (param: { +export type GetSecuritySolutionUrl = (param: { deepLinkId: SecurityPageName; path?: string; absolute?: boolean; @@ -63,6 +63,7 @@ export const useGetSecuritySolutionUrl = () => { ({ deepLinkId, path = '', absolute = false, skipSearch = false }) => { const search = needsUrlState(deepLinkId) ? getUrlStateQueryString() : ''; const formattedPath = formatPath(path, search, skipSearch); + return getAppUrl({ deepLinkId, path: formattedPath, absolute }); }, [getAppUrl, getUrlStateQueryString] diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/get_breadcrumbs_for_page.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/get_breadcrumbs_for_page.ts new file mode 100644 index 0000000000000..c70d7d24fcb94 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/get_breadcrumbs_for_page.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ChromeBreadcrumb } from '@kbn/core/public'; +import { SecurityPageName } from '../../../../app/types'; +import { APP_NAME } from '../../../../../common/constants'; +import { getAppLandingUrl } from '../../link_to/redirect_to_landing'; + +import { GetSecuritySolutionUrl } from '../../link_to'; +import { getAncestorLinksInfo } from '../../../links'; +import { GenericNavRecord } from '../types'; + +export const getLeadingBreadcrumbsForSecurityPage = ( + pageName: SecurityPageName, + getSecuritySolutionUrl: GetSecuritySolutionUrl, + navTabs: GenericNavRecord, + isGroupedNavigationEnabled: boolean +): [ChromeBreadcrumb, ...ChromeBreadcrumb[]] => { + const landingPath = getSecuritySolutionUrl({ deepLinkId: SecurityPageName.landing }); + + const siemRootBreadcrumb: ChromeBreadcrumb = { + text: APP_NAME, + href: getAppLandingUrl(landingPath), + }; + + const breadcrumbs: ChromeBreadcrumb[] = getAncestorLinksInfo(pageName).map(({ title, id }) => { + const newTitle = title; + // Get title from navTabs because pages title on the new structure might be different. + const oldTitle = navTabs[id] ? navTabs[id].name : title; + + return { + text: isGroupedNavigationEnabled ? newTitle : oldTitle, + href: getSecuritySolutionUrl({ deepLinkId: id }), + }; + }); + + return [siemRootBreadcrumb, ...breadcrumbs]; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index 7d2bfaa405cb2..05dd7145ba785 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -7,15 +7,35 @@ import '../../../mock/match_media'; import { encodeIpv6 } from '../../../lib/helpers'; -import { getBreadcrumbsForRoute, useSetBreadcrumbs } from '.'; +import { getBreadcrumbsForRoute, ObjectWithNavTabs, useSetBreadcrumbs } from '.'; import { HostsTableType } from '../../../../hosts/store/model'; import { RouteSpyState, SiemRouteType } from '../../../utils/route/types'; -import { TabNavigationProps } from '../tab_navigation/types'; import { NetworkRouteType } from '../../../../network/pages/navigation/types'; import { TimelineTabs } from '../../../../../common/types/timeline'; import { AdministrationSubTab } from '../../../../management/types'; import { renderHook } from '@testing-library/react-hooks'; import { TestProviders } from '../../../mock'; +import { GetSecuritySolutionUrl } from '../../link_to'; +import { APP_UI_ID } from '../../../../../common/constants'; +import { useDeepEqualSelector } from '../../../hooks/use_selector'; +import { useIsGroupedNavigationEnabled } from '../helpers'; +import { navTabs } from '../../../../app/home/home_navigations'; +import { getAppLinks } from '../../../links/app_links'; +import { allowedExperimentalValues } from '../../../../../common/experimental_features'; +import { StartPlugins } from '../../../../types'; +import { coreMock } from '@kbn/core/public/mocks'; +import { updateAppLinks } from '../../../links'; + +jest.mock('../../../hooks/use_selector'); + +const mockUseIsGroupedNavigationEnabled = useIsGroupedNavigationEnabled as jest.Mock; +jest.mock('../helpers', () => { + const original = jest.requireActual('../helpers'); + return { + ...original, + useIsGroupedNavigationEnabled: jest.fn(), + }; +}); const setBreadcrumbsMock = jest.fn(); const chromeMock = { @@ -40,412 +60,824 @@ const getMockObject = ( pageName: string, pathName: string, detailName: string | undefined -): RouteSpyState & TabNavigationProps => ({ +): RouteSpyState & ObjectWithNavTabs => ({ detailName, - navTabs: { - cases: { - disabled: false, - href: '/app/security/cases', - id: 'cases', - name: 'Cases', - urlKey: 'cases', - }, - hosts: { - disabled: false, - href: '/app/security/hosts', - id: 'hosts', - name: 'Hosts', - urlKey: 'host', - }, - network: { - disabled: false, - href: '/app/security/network', - id: 'network', - name: 'Network', - urlKey: 'network', - }, - overview: { - disabled: false, - href: '/app/security/overview', - id: 'overview', - name: 'Overview', - urlKey: 'overview', - }, - timelines: { - disabled: false, - href: '/app/security/timelines', - id: 'timelines', - name: 'Timelines', - urlKey: 'timeline', - }, - alerts: { - disabled: false, - href: '/app/security/alerts', - id: 'alerts', - name: 'Alerts', - urlKey: 'alerts', - }, - exceptions: { - disabled: false, - href: '/app/security/exceptions', - id: 'exceptions', - name: 'Exceptions', - urlKey: 'exceptions', - }, - rules: { - disabled: false, - href: '/app/security/rules', - id: 'rules', - name: 'Rules', - urlKey: 'rules', - }, - }, + navTabs, pageName, pathName, search: '', tabName: mockDefaultTab(pageName) as HostsTableType, - query: { query: '', language: 'kuery' }, - filters: [], - timeline: { - activeTab: TimelineTabs.query, - id: '', - isOpen: false, - graphEventId: '', - }, - timerange: { - global: { - linkTo: ['timeline'], - timerange: { - from: '2019-05-16T23:10:43.696Z', - fromStr: 'now-24h', - kind: 'relative', - to: '2019-05-17T23:10:43.697Z', - toStr: 'now', +}); + +(useDeepEqualSelector as jest.Mock).mockImplementation(() => { + return { + urlState: { + query: { query: '', language: 'kuery' }, + filters: [], + timeline: { + activeTab: TimelineTabs.query, + id: '', + isOpen: false, + graphEventId: '', }, - }, - timeline: { - linkTo: ['global'], timerange: { - from: '2019-05-16T23:10:43.696Z', - fromStr: 'now-24h', - kind: 'relative', - to: '2019-05-17T23:10:43.697Z', - toStr: 'now', + global: { + linkTo: ['timeline'], + timerange: { + from: '2019-05-16T23:10:43.696Z', + fromStr: 'now-24h', + kind: 'relative', + to: '2019-05-17T23:10:43.697Z', + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: '2019-05-16T23:10:43.696Z', + fromStr: 'now-24h', + kind: 'relative', + to: '2019-05-17T23:10:43.697Z', + toStr: 'now', + }, + }, }, + sourcerer: {}, }, - }, - sourcerer: {}, + }; }); -// The string returned is different from what getUrlForApp returns, but does not matter for the purposes of this test. -const getUrlForAppMock = ( - appId: string, - options?: { deepLinkId?: string; path?: string; absolute?: boolean } -) => `${appId}${options?.deepLinkId ? `/${options.deepLinkId}` : ''}${options?.path ?? ''}`; +// The string returned is different from what getSecuritySolutionUrl returns, but does not matter for the purposes of this test. +const getSecuritySolutionUrl: GetSecuritySolutionUrl = ({ + deepLinkId, + path, +}: { + deepLinkId?: string; + path?: string; + absolute?: boolean; +}) => `${APP_UI_ID}${deepLinkId ? `/${deepLinkId}` : ''}${path ?? ''}`; + +jest.mock('../../../lib/kibana/kibana_react', () => { + return { + useKibana: () => ({ + services: { + chrome: undefined, + application: { + navigateToApp: jest.fn(), + getUrlForApp: (appId: string, options?: { path?: string; deepLinkId?: boolean }) => + `${appId}/${options?.deepLinkId ?? ''}${options?.path ?? ''}`, + }, + }, + }), + }; +}); describe('Navigation Breadcrumbs', () => { + beforeAll(async () => { + const appLinks = await getAppLinks(coreMock.createStart(), {} as StartPlugins); + updateAppLinks(appLinks, { + experimentalFeatures: allowedExperimentalValues, + capabilities: { + navLinks: {}, + management: {}, + catalogue: {}, + actions: { show: true, crud: true }, + siem: { + show: true, + crud: true, + }, + }, + }); + }); + const hostName = 'siem-kibana'; const ipv4 = '192.0.2.255'; const ipv6 = '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff'; const ipv6Encoded = encodeIpv6(ipv6); - describe('getBreadcrumbsForRoute', () => { - test('should return Host breadcrumbs when supplied host pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('hosts', '/', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { - href: 'securitySolutionUI/get_started', - text: 'Security', - }, - { - href: "securitySolutionUI/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - text: 'Hosts', - }, - { - href: '', - text: 'Authentications', - }, - ]); + describe('Old Architecture', () => { + beforeAll(() => { + mockUseIsGroupedNavigationEnabled.mockReturnValue(false); }); - test('should return Network breadcrumbs when supplied network pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('network', '/', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Network', - href: "securitySolutionUI/network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: 'Flows', - href: '', - }, - ]); - }); + describe('getBreadcrumbsForRoute', () => { + test('should return Overview breadcrumbs when supplied overview pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('overview', '/', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { + href: 'securitySolutionUI/get_started', + text: 'Security', + }, + { + href: '', + text: 'Overview', + }, + ]); + }); - test('should return Timelines breadcrumbs when supplied timelines pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('timelines', '/', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Timelines', - href: "securitySolutionUI/timelines?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - ]); - }); + test('should return Host breadcrumbs when supplied hosts pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('hosts', '/', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { + href: 'securitySolutionUI/get_started', + text: 'Security', + }, + { + href: 'securitySolutionUI/hosts', + text: 'Hosts', + }, + { + href: '', + text: 'Authentications', + }, + ]); + }); - test('should return Host Details breadcrumbs when supplied a pathname with hostName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('hosts', '/', hostName), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Hosts', - href: "securitySolutionUI/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: 'siem-kibana', - href: "securitySolutionUI/hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { text: 'Authentications', href: '' }, - ]); - }); + test('should return Network breadcrumbs when supplied network pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Network', + href: 'securitySolutionUI/network', + }, + { + text: 'Flows', + href: '', + }, + ]); + }); - test('should return IP Details breadcrumbs when supplied pathname with ipv4', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('network', '/', ipv4), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Network', - href: "securitySolutionUI/network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: ipv4, - href: `securitySolutionUI/network/ip/${ipv4}/source?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, - }, - { text: 'Flows', href: '' }, - ]); - }); + test('should return Timelines breadcrumbs when supplied timelines pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('timelines', '/', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Timelines', + href: '', + }, + ]); + }); - test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('network', '/', ipv6Encoded), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Network', - href: "securitySolutionUI/network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: ipv6, - href: `securitySolutionUI/network/ip/${ipv6Encoded}/source?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, - }, - { text: 'Flows', href: '' }, - ]); - }); + test('should return Host Details breadcrumbs when supplied a pathname with hostName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('hosts', '/', hostName), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Hosts', + href: 'securitySolutionUI/hosts', + }, + { + text: 'siem-kibana', + href: 'securitySolutionUI/hosts/siem-kibana', + }, + { text: 'Authentications', href: '' }, + ]); + }); - test('should return Alerts breadcrumbs when supplied alerts pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('alerts', '/alerts', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Alerts', - href: '', - }, - ]); - }); + test('should return IP Details breadcrumbs when supplied pathname with ipv4', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', ipv4), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Network', + href: 'securitySolutionUI/network', + }, + { + text: ipv4, + href: `securitySolutionUI/network/ip/${ipv4}/source`, + }, + { text: 'Flows', href: '' }, + ]); + }); - test('should return Exceptions breadcrumbs when supplied exceptions pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('exceptions', '/exceptions', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Exceptions', - href: '', - }, - ]); - }); + test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', ipv6Encoded), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Network', + href: 'securitySolutionUI/network', + }, + { + text: ipv6, + href: `securitySolutionUI/network/ip/${ipv6Encoded}/source`, + }, + { text: 'Flows', href: '' }, + ]); + }); - test('should return Rules breadcrumbs when supplied rules pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('rules', '/rules', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Rules', - href: "securitySolutionUI/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - ]); - }); + test('should return Alerts breadcrumbs when supplied alerts pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('alerts', '/alerts', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Alerts', + href: '', + }, + ]); + }); - test('should return Rules breadcrumbs when supplied rules Creation pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('rules', '/rules/create', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Rules', - href: "securitySolutionUI/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: 'Create', - href: '', - }, - ]); - }); + test('should return Exceptions breadcrumbs when supplied exceptions pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('exceptions', '/exceptions', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Exception lists', + href: '', + }, + ]); + }); - test('should return Rules breadcrumbs when supplied rules Details pathname', () => { - const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; - const mockRuleName = 'ALERT_RULE_NAME'; - const breadcrumbs = getBreadcrumbsForRoute( - { - ...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined), - detailName: mockDetailName, - state: { - ruleName: mockRuleName, + test('should return Rules breadcrumbs when supplied rules pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('rules', '/rules', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Rules', + href: '', }, - }, - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Rules', - href: "securitySolutionUI/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: mockRuleName, - href: `securitySolutionUI/rules/id/${mockDetailName}?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, - }, - ]); - }); + ]); + }); - test('should return Rules breadcrumbs when supplied rules Edit pathname', () => { - const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; - const mockRuleName = 'ALERT_RULE_NAME'; - const breadcrumbs = getBreadcrumbsForRoute( - { - ...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined), - detailName: mockDetailName, - state: { - ruleName: mockRuleName, + test('should return Rules breadcrumbs when supplied rules Creation pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('rules', '/rules/create', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Rules', + href: 'securitySolutionUI/rules', }, - }, - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Rules', - href: "securitySolutionUI/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: 'ALERT_RULE_NAME', - href: `securitySolutionUI/rules/id/${mockDetailName}?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, - }, - { - text: 'Edit', - href: '', - }, - ]); + { + text: 'Create', + href: '', + }, + ]); + }); + + test('should return Rules breadcrumbs when supplied rules Details pageName', () => { + const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; + const mockRuleName = 'ALERT_RULE_NAME'; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined), + detailName: mockDetailName, + state: { + ruleName: mockRuleName, + }, + }, + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Rules', + href: 'securitySolutionUI/rules', + }, + { + text: mockRuleName, + href: ``, + }, + ]); + }); + + test('should return Rules breadcrumbs when supplied rules Edit pageName', () => { + const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; + const mockRuleName = 'ALERT_RULE_NAME'; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined), + detailName: mockDetailName, + state: { + ruleName: mockRuleName, + }, + }, + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Rules', + href: 'securitySolutionUI/rules', + }, + { + text: 'ALERT_RULE_NAME', + href: `securitySolutionUI/rules/id/${mockDetailName}`, + }, + { + text: 'Edit', + href: '', + }, + ]); + }); + + test('should return null breadcrumbs when supplied Cases pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('cases', '/', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual(null); + }); + + test('should return null breadcrumbs when supplied Cases details pageName', () => { + const sampleCase = { + id: 'my-case-id', + name: 'Case name', + }; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('cases', `/${sampleCase.id}`, sampleCase.id), + state: { caseTitle: sampleCase.name }, + }, + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual(null); + }); + + test('should return Admin breadcrumbs when supplied endpoints pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('administration', '/endpoints', undefined), + getSecuritySolutionUrl, + false + ); + + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Endpoints', + href: '', + }, + ]); + }); }); - test('should return null breadcrumbs when supplied Cases pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('cases', '/', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual(null); + describe('setBreadcrumbs()', () => { + test('should call chrome breadcrumb service with correct breadcrumbs', () => { + const navigateToUrlMock = jest.fn(); + const { result } = renderHook(() => useSetBreadcrumbs(), { wrapper: TestProviders }); + result.current(getMockObject('hosts', '/', hostName), chromeMock, navigateToUrlMock); + + expect(setBreadcrumbsMock).toBeCalledWith([ + expect.objectContaining({ + text: 'Security', + href: "securitySolutionUI/get_started?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + onClick: expect.any(Function), + }), + expect.objectContaining({ + text: 'Hosts', + href: "securitySolutionUI/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + onClick: expect.any(Function), + }), + expect.objectContaining({ + text: 'siem-kibana', + href: "securitySolutionUI/hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + onClick: expect.any(Function), + }), + { + text: 'Authentications', + href: '', + }, + ]); + }); }); + }); - test('should return null breadcrumbs when supplied Cases details pathname', () => { - const sampleCase = { - id: 'my-case-id', - name: 'Case name', - }; - const breadcrumbs = getBreadcrumbsForRoute( - { - ...getMockObject('cases', `/${sampleCase.id}`, sampleCase.id), - state: { caseTitle: sampleCase.name }, - }, - getUrlForAppMock - ); - expect(breadcrumbs).toEqual(null); + describe('New Architecture', () => { + beforeAll(() => { + mockUseIsGroupedNavigationEnabled.mockReturnValue(true); }); - test('should return Admin breadcrumbs when supplied endpoints pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('administration', '/endpoints', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Endpoints', - href: '', - }, - ]); + describe('getBreadcrumbsForRoute', () => { + test('should return Overview breadcrumbs when supplied overview pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('overview', '/', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { + href: 'securitySolutionUI/get_started', + text: 'Security', + }, + { + href: 'securitySolutionUI/dashboards', + text: 'Dashboards', + }, + { + href: '', + text: 'Overview', + }, + ]); + }); + + test('should return Host breadcrumbs when supplied hosts pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('hosts', '/', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { + href: 'securitySolutionUI/get_started', + text: 'Security', + }, + { + href: 'securitySolutionUI/threat_hunting', + text: 'Threat Hunting', + }, + { + href: 'securitySolutionUI/hosts', + text: 'Hosts', + }, + { + href: '', + text: 'Authentications', + }, + ]); + }); + + test('should return Network breadcrumbs when supplied network pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + href: 'securitySolutionUI/threat_hunting', + text: 'Threat Hunting', + }, + { + text: 'Network', + href: 'securitySolutionUI/network', + }, + { + text: 'Flows', + href: '', + }, + ]); + }); + + test('should return Timelines breadcrumbs when supplied timelines pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('timelines', '/', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Timelines', + href: '', + }, + ]); + }); + + test('should return Host Details breadcrumbs when supplied a pathname with hostName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('hosts', '/', hostName), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + href: 'securitySolutionUI/threat_hunting', + text: 'Threat Hunting', + }, + { + text: 'Hosts', + href: 'securitySolutionUI/hosts', + }, + { + text: 'siem-kibana', + href: 'securitySolutionUI/hosts/siem-kibana', + }, + { text: 'Authentications', href: '' }, + ]); + }); + + test('should return IP Details breadcrumbs when supplied pathname with ipv4', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', ipv4), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + href: 'securitySolutionUI/threat_hunting', + text: 'Threat Hunting', + }, + { + text: 'Network', + href: 'securitySolutionUI/network', + }, + { + text: ipv4, + href: `securitySolutionUI/network/ip/${ipv4}/source`, + }, + { text: 'Flows', href: '' }, + ]); + }); + + test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', ipv6Encoded), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + href: 'securitySolutionUI/threat_hunting', + text: 'Threat Hunting', + }, + { + text: 'Network', + href: 'securitySolutionUI/network', + }, + { + text: ipv6, + href: `securitySolutionUI/network/ip/${ipv6Encoded}/source`, + }, + { text: 'Flows', href: '' }, + ]); + }); + + test('should return Alerts breadcrumbs when supplied alerts pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('alerts', '/alerts', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Alerts', + href: '', + }, + ]); + }); + + test('should return Exceptions breadcrumbs when supplied exceptions pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('exceptions', '/exceptions', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Manage', + href: 'securitySolutionUI/administration', + }, + { + text: 'Exception lists', + href: '', + }, + ]); + }); + + test('should return Rules breadcrumbs when supplied rules pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('rules', '/rules', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Manage', + href: 'securitySolutionUI/administration', + }, + { + text: 'Rules', + href: '', + }, + ]); + }); + + test('should return Rules breadcrumbs when supplied rules Creation pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('rules', '/rules/create', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Manage', + href: 'securitySolutionUI/administration', + }, + { + text: 'Rules', + href: 'securitySolutionUI/rules', + }, + { + text: 'Create', + href: '', + }, + ]); + }); + + test('should return Rules breadcrumbs when supplied rules Details pageName', () => { + const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; + const mockRuleName = 'ALERT_RULE_NAME'; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined), + detailName: mockDetailName, + state: { + ruleName: mockRuleName, + }, + }, + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Manage', + href: 'securitySolutionUI/administration', + }, + { + text: 'Rules', + href: 'securitySolutionUI/rules', + }, + { + text: mockRuleName, + href: ``, + }, + ]); + }); + + test('should return Rules breadcrumbs when supplied rules Edit pageName', () => { + const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; + const mockRuleName = 'ALERT_RULE_NAME'; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined), + detailName: mockDetailName, + state: { + ruleName: mockRuleName, + }, + }, + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Manage', + href: 'securitySolutionUI/administration', + }, + { + text: 'Rules', + href: 'securitySolutionUI/rules', + }, + { + text: 'ALERT_RULE_NAME', + href: `securitySolutionUI/rules/id/${mockDetailName}`, + }, + { + text: 'Edit', + href: '', + }, + ]); + }); + + test('should return null breadcrumbs when supplied Cases pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('cases', '/', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual(null); + }); + + test('should return null breadcrumbs when supplied Cases details pageName', () => { + const sampleCase = { + id: 'my-case-id', + name: 'Case name', + }; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('cases', `/${sampleCase.id}`, sampleCase.id), + state: { caseTitle: sampleCase.name }, + }, + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual(null); + }); + + test('should return Admin breadcrumbs when supplied endpoints pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('administration', '/endpoints', undefined), + getSecuritySolutionUrl, + true + ); + + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Manage', + href: 'securitySolutionUI/administration', + }, + { + text: 'Endpoints', + href: '', + }, + ]); + }); }); - }); - describe('setBreadcrumbs()', () => { - test('should call chrome breadcrumb service with correct breadcrumbs', () => { - const navigateToUrlMock = jest.fn(); - const { result } = renderHook(() => useSetBreadcrumbs(), { wrapper: TestProviders }); - result.current( - getMockObject('hosts', '/', hostName), - chromeMock, - getUrlForAppMock, - navigateToUrlMock - ); - expect(setBreadcrumbsMock).toBeCalledWith([ - expect.objectContaining({ - text: 'Security', - href: 'securitySolutionUI/get_started', - onClick: expect.any(Function), - }), - expect.objectContaining({ - text: 'Hosts', - href: "securitySolutionUI/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - onClick: expect.any(Function), - }), - expect.objectContaining({ - text: 'siem-kibana', - href: "securitySolutionUI/hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - onClick: expect.any(Function), - }), - { - text: 'Authentications', - href: '', - }, - ]); + describe('setBreadcrumbs()', () => { + test('should call chrome breadcrumb service with correct breadcrumbs', () => { + const navigateToUrlMock = jest.fn(); + const { result } = renderHook(() => useSetBreadcrumbs(), { wrapper: TestProviders }); + result.current(getMockObject('hosts', '/', hostName), chromeMock, navigateToUrlMock); + const searchString = + "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"; + + expect(setBreadcrumbsMock).toBeCalledWith([ + expect.objectContaining({ + text: 'Security', + href: `securitySolutionUI/get_started${searchString}`, + onClick: expect.any(Function), + }), + expect.objectContaining({ + text: 'Threat Hunting', + href: `securitySolutionUI/threat_hunting`, + onClick: expect.any(Function), + }), + expect.objectContaining({ + text: 'Hosts', + href: `securitySolutionUI/hosts${searchString}`, + onClick: expect.any(Function), + }), + expect.objectContaining({ + text: 'siem-kibana', + href: `securitySolutionUI/hosts/siem-kibana${searchString}`, + onClick: expect.any(Function), + }), + { + text: 'Authentications', + href: '', + }, + ]); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 3c2e103c0dfd3..ba4835bf776c9 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -5,43 +5,50 @@ * 2.0. */ -import { getOr, omit } from 'lodash/fp'; +import { last, omit } from 'lodash/fp'; import { useDispatch } from 'react-redux'; import { ChromeBreadcrumb } from '@kbn/core/public'; -import { APP_NAME, APP_UI_ID } from '../../../../../common/constants'; import { StartServices } from '../../../../types'; -import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../hosts/pages/details/utils'; -import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/details'; -import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; -import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../../timelines/pages'; -import { getBreadcrumbs as getUsersBreadcrumbs } from '../../../../users/pages/details/utils'; -import { getBreadcrumbs as getAdminBreadcrumbs } from '../../../../management/common/breadcrumbs'; +import { getTrailingBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../hosts/pages/details/utils'; +import { getTrailingBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/details'; +import { getTrailingBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; +import { getTrailingBreadcrumbs as getUsersBreadcrumbs } from '../../../../users/pages/details/utils'; +import { getTrailingBreadcrumbs as getAdminBreadcrumbs } from '../../../../management/common/breadcrumbs'; import { SecurityPageName } from '../../../../app/types'; import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState, - TimelineRouteSpyState, AdministrationRouteSpyState, UsersRouteSpyState, } from '../../../utils/route/types'; -import { getAppLandingUrl } from '../../link_to/redirect_to_landing'; import { timelineActions } from '../../../../timelines/store/timeline'; import { TimelineId } from '../../../../../common/types/timeline'; -import { TabNavigationProps } from '../tab_navigation/types'; -import { getSearch } from '../helpers'; -import { GetUrlForApp, NavigateToUrl, SearchNavTab } from '../types'; +import { GenericNavRecord, NavigateToUrl } from '../types'; +import { getLeadingBreadcrumbsForSecurityPage } from './get_breadcrumbs_for_page'; +import { GetSecuritySolutionUrl, useGetSecuritySolutionUrl } from '../../link_to'; +import { useIsGroupedNavigationEnabled } from '../helpers'; + +export interface ObjectWithNavTabs { + navTabs: GenericNavRecord; +} export const useSetBreadcrumbs = () => { const dispatch = useDispatch(); + const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); + const isGroupedNavigationEnabled = useIsGroupedNavigationEnabled(); + return ( - spyState: RouteSpyState & TabNavigationProps, + spyState: RouteSpyState & ObjectWithNavTabs, chrome: StartServices['chrome'], - getUrlForApp: GetUrlForApp, navigateToUrl: NavigateToUrl ) => { - const breadcrumbs = getBreadcrumbsForRoute(spyState, getUrlForApp); + const breadcrumbs = getBreadcrumbsForRoute( + spyState, + getSecuritySolutionUrl, + isGroupedNavigationEnabled + ); if (breadcrumbs) { chrome.setBreadcrumbs( breadcrumbs.map((breadcrumb) => ({ @@ -64,158 +71,103 @@ export const useSetBreadcrumbs = () => { }; }; -const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.network; - -const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.hosts; - -const isUsersRoutes = (spyState: RouteSpyState): spyState is UsersRouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.users; - -const isTimelinesRoutes = (spyState: RouteSpyState): spyState is TimelineRouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.timelines; - -const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.case; - -const isAdminRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.administration; - -const isRulesRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState => - spyState != null && - (spyState.pageName === SecurityPageName.rules || - spyState.pageName === SecurityPageName.rulesCreate); - -// eslint-disable-next-line complexity export const getBreadcrumbsForRoute = ( - object: RouteSpyState & TabNavigationProps, - getUrlForApp: GetUrlForApp + object: RouteSpyState & ObjectWithNavTabs, + getSecuritySolutionUrl: GetSecuritySolutionUrl, + isGroupedNavigationEnabled: boolean ): ChromeBreadcrumb[] | null => { const spyState: RouteSpyState = omit('navTabs', object); - const landingPath = getUrlForApp(APP_UI_ID, { deepLinkId: SecurityPageName.landing }); - - const siemRootBreadcrumb: ChromeBreadcrumb = { - text: APP_NAME, - href: getAppLandingUrl(landingPath), - }; - if (isHostsRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'host', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - return [ - siemRootBreadcrumb, - ...getHostDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ), - getUrlForApp - ), - ]; + if (!spyState || !object.navTabs || !spyState.pageName || isCaseRoutes(spyState)) { + return null; } - if (isNetworkRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + + const newMenuLeadingBreadcrumbs = getLeadingBreadcrumbsForSecurityPage( + spyState.pageName as SecurityPageName, + getSecuritySolutionUrl, + object.navTabs, + isGroupedNavigationEnabled + ); + + // last newMenuLeadingBreadcrumbs is the current page + const pageBreadcrumb = newMenuLeadingBreadcrumbs[newMenuLeadingBreadcrumbs.length - 1]; + const siemRootBreadcrumb = newMenuLeadingBreadcrumbs[0]; + + const leadingBreadcrumbs = isGroupedNavigationEnabled + ? newMenuLeadingBreadcrumbs + : [siemRootBreadcrumb, pageBreadcrumb]; + + // Admin URL works differently. All admin pages are under '/administration' + if (isAdminRoutes(spyState)) { + if (isGroupedNavigationEnabled) { + return emptyLastBreadcrumbUrl([...leadingBreadcrumbs, ...getAdminBreadcrumbs(spyState)]); + } else { + return [ + ...(siemRootBreadcrumb ? [siemRootBreadcrumb] : []), + ...getAdminBreadcrumbs(spyState), + ]; } - return [ - siemRootBreadcrumb, - ...getIPDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ), - getUrlForApp - ), - ]; } - if (isUsersRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'users', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } + return emptyLastBreadcrumbUrl([ + ...leadingBreadcrumbs, + ...getTrailingBreadcrumbsForRoutes(spyState, getSecuritySolutionUrl), + ]); +}; - return [ - siemRootBreadcrumb, - ...getUsersBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ), - getUrlForApp - ), - ]; +const getTrailingBreadcrumbsForRoutes = ( + spyState: RouteSpyState, + getSecuritySolutionUrl: GetSecuritySolutionUrl +): ChromeBreadcrumb[] => { + if (isHostsRoutes(spyState)) { + return getHostDetailsBreadcrumbs(spyState, getSecuritySolutionUrl); + } + if (isNetworkRoutes(spyState)) { + return getIPDetailsBreadcrumbs(spyState, getSecuritySolutionUrl); } - if (isRulesRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: SecurityPageName.rules, isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - - return [ - siemRootBreadcrumb, - ...getDetectionRulesBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ), - getUrlForApp - ), - ]; + if (isUsersRoutes(spyState)) { + return getUsersBreadcrumbs(spyState, getSecuritySolutionUrl); } - if (isCaseRoutes(spyState) && object.navTabs) { - return null; // controlled by Cases routes + if (isRulesRoutes(spyState)) { + return getDetectionRulesBreadcrumbs(spyState, getSecuritySolutionUrl); } - if (isTimelinesRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'timeline', isDetailPage: false }; - const urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + return []; +}; - return [ - siemRootBreadcrumb, - ...getTimelinesBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ), - getUrlForApp - ), - ]; - } +const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => + spyState.pageName === SecurityPageName.network; - if (isAdminRoutes(spyState) && object.navTabs) { - return [siemRootBreadcrumb, ...getAdminBreadcrumbs(spyState)]; - } +const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => + spyState.pageName === SecurityPageName.hosts; + +const isUsersRoutes = (spyState: RouteSpyState): spyState is UsersRouteSpyState => + spyState.pageName === SecurityPageName.users; + +const isCaseRoutes = (spyState: RouteSpyState) => spyState.pageName === SecurityPageName.case; + +const isAdminRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState => + spyState.pageName === SecurityPageName.administration; + +const isRulesRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState => + spyState.pageName === SecurityPageName.rules || + spyState.pageName === SecurityPageName.rulesCreate; + +const emptyLastBreadcrumbUrl = (breadcrumbs: ChromeBreadcrumb[]) => { + const leadingBreadCrumbs = breadcrumbs.slice(0, -1); + const lastBreadcrumb = last(breadcrumbs); - if ( - spyState != null && - object.navTabs && - spyState.pageName && - object.navTabs[spyState.pageName] - ) { + if (lastBreadcrumb) { return [ - siemRootBreadcrumb, + ...leadingBreadCrumbs, { - text: object.navTabs[spyState.pageName].name, + ...lastBreadcrumb, href: '', }, ]; } - return null; + return breadcrumbs; }; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts b/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts index 5569d8c85afa8..b2d91492b3ae1 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts @@ -9,8 +9,6 @@ import { isEmpty } from 'lodash/fp'; import { Location } from 'history'; import type { Filter, Query } from '@kbn/es-query'; -import { useUiSetting$ } from '../../lib/kibana'; -import { ENABLE_GROUPED_NAVIGATION } from '../../../../common/constants'; import { UrlInputsModel } from '../../store/inputs/model'; import { TimelineUrl } from '../../../timelines/store/timeline/model'; import { CONSTANTS } from '../url_state/constants'; @@ -24,6 +22,8 @@ import { import { SearchNavTab } from './types'; import { SourcererUrlState } from '../../store/sourcerer/model'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; +import { useUiSetting$ } from '../../lib/kibana'; +import { ENABLE_GROUPED_NAVIGATION } from '../../../../common/constants'; export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { if (tab && tab.urlKey != null && !isAdministration(tab.urlKey)) { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index d14c8a51a66ee..f70b77b15dc8c 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -111,44 +111,12 @@ describe('SIEM Navigation', () => { pageName: 'hosts', pathName: '/', search: '', - sourcerer: {}, state: undefined, tabName: 'authentications', - query: { query: '', language: 'kuery' }, - filters: [], flowTarget: undefined, savedQuery: undefined, - timeline: { - activeTab: TimelineTabs.query, - id: '', - isOpen: false, - graphEventId: '', - }, - timerange: { - global: { - linkTo: ['timeline'], - timerange: { - from: '2019-05-16T23:10:43.696Z', - fromStr: 'now-24h', - kind: 'relative', - to: '2019-05-17T23:10:43.697Z', - toStr: 'now', - }, - }, - timeline: { - linkTo: ['global'], - timerange: { - from: '2019-05-16T23:10:43.696Z', - fromStr: 'now-24h', - kind: 'relative', - to: '2019-05-17T23:10:43.697Z', - toStr: 'now', - }, - }, - }, }, undefined, - mockGetUrlForApp, mockNavigateToUrl ); }); @@ -163,43 +131,15 @@ describe('SIEM Navigation', () => { 2, { detailName: undefined, - filters: [], flowTarget: undefined, navTabs, + search: '', pageName: 'network', pathName: '/', - query: { language: 'kuery', query: '' }, - savedQuery: undefined, - search: '', - sourcerer: {}, state: undefined, tabName: 'authentications', - timeline: { id: '', isOpen: false, activeTab: TimelineTabs.query, graphEventId: '' }, - timerange: { - global: { - linkTo: ['timeline'], - timerange: { - from: '2019-05-16T23:10:43.696Z', - fromStr: 'now-24h', - kind: 'relative', - to: '2019-05-17T23:10:43.697Z', - toStr: 'now', - }, - }, - timeline: { - linkTo: ['global'], - timerange: { - from: '2019-05-16T23:10:43.696Z', - fromStr: 'now-24h', - kind: 'relative', - to: '2019-05-17T23:10:43.697Z', - toStr: 'now', - }, - }, - }, }, undefined, - mockGetUrlForApp, mockNavigateToUrl ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx index f8b9251f4ff91..8491171e65bca 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx @@ -49,22 +49,15 @@ export const TabNavigationComponent: React.FC< setBreadcrumbs( { detailName, - filters: urlState.filters, flowTarget, navTabs, pageName, pathName, - query: urlState.query, - savedQuery: urlState.savedQuery, search, - sourcerer: urlState.sourcerer, state, tabName, - timeline: urlState.timeline, - timerange: urlState.timerange, }, chrome, - getUrlForApp, navigateToUrl ); } @@ -74,7 +67,6 @@ export const TabNavigationComponent: React.FC< pathName, search, navTabs, - urlState, state, detailName, flowTarget, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx index 870ab15906f71..c20cf6414ae5d 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx @@ -45,22 +45,15 @@ export const useSecuritySolutionNavigation = () => { setBreadcrumbs( { detailName, - filters: urlState.filters, flowTarget, navTabs: enabledNavTabs, pageName, pathName, - query: urlState.query, - savedQuery: urlState.savedQuery, search, - sourcerer: urlState.sourcerer, state, tabName, - timeline: urlState.timeline, - timerange: urlState.timerange, }, chrome, - getUrlForApp, navigateToUrl ); } @@ -69,7 +62,6 @@ export const useSecuritySolutionNavigation = () => { pageName, pathName, search, - urlState, state, detailName, flowTarget, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts deleted file mode 100644 index d405837a4f7f2..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getBreadcrumbs } from './utils'; - -const getUrlForAppMock = (appId: string, options?: { path?: string; absolute?: boolean }) => - `${appId}${options?.path ?? ''}`; - -describe('getBreadcrumbs', () => { - it('Does not render for incorrect params', () => { - expect( - getBreadcrumbs( - { - pageName: 'pageName', - detailName: 'detailName', - tabName: undefined, - search: '', - pathName: 'pathName', - }, - [], - getUrlForAppMock - ) - ).toEqual([]); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts index b4778bb8c24ea..21737d307f3fd 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts @@ -5,19 +5,14 @@ * 2.0. */ -import { isEmpty } from 'lodash/fp'; - import { ChromeBreadcrumb } from '@kbn/core/public'; -import { - getRulesUrl, - getRuleDetailsUrl, -} from '../../../../common/components/link_to/redirect_to_detection_engine'; +import { getRuleDetailsUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; import * as i18nRules from './translations'; import { RouteSpyState } from '../../../../common/utils/route/types'; -import { GetUrlForApp } from '../../../../common/components/navigation/types'; import { SecurityPageName } from '../../../../app/types'; -import { APP_UI_ID, RULES_PATH } from '../../../../../common/constants'; +import { RULES_PATH } from '../../../../../common/constants'; import { RuleStep, RuleStepsOrder } from './types'; +import { GetSecuritySolutionUrl } from '../../../../common/components/link_to'; export const ruleStepsOrder: RuleStepsOrder = [ RuleStep.defineRule, @@ -26,47 +21,26 @@ export const ruleStepsOrder: RuleStepsOrder = [ RuleStep.ruleActions, ]; -const getRulesBreadcrumb = (pathname: string, search: string[], getUrlForApp: GetUrlForApp) => { - const tabPath = pathname.split('/')[1]; - - if (tabPath === 'rules') { - return { - text: i18nRules.PAGE_TITLE, - href: getUrlForApp(APP_UI_ID, { - deepLinkId: SecurityPageName.rules, - path: getRulesUrl(!isEmpty(search[0]) ? search[0] : ''), - }), - }; - } -}; - const isRuleCreatePage = (pathname: string) => pathname.includes(RULES_PATH) && pathname.includes('/create'); const isRuleEditPage = (pathname: string) => pathname.includes(RULES_PATH) && pathname.includes('/edit'); -export const getBreadcrumbs = ( +export const getTrailingBreadcrumbs = ( params: RouteSpyState, - search: string[], - getUrlForApp: GetUrlForApp + getSecuritySolutionUrl: GetSecuritySolutionUrl ): ChromeBreadcrumb[] => { let breadcrumb: ChromeBreadcrumb[] = []; - const rulesBreadcrumb = getRulesBreadcrumb(params.pathName, search, getUrlForApp); - - if (rulesBreadcrumb) { - breadcrumb = [...breadcrumb, rulesBreadcrumb]; - } - if (params.detailName && params.state?.ruleName) { breadcrumb = [ ...breadcrumb, { text: params.state.ruleName, - href: getUrlForApp(APP_UI_ID, { + href: getSecuritySolutionUrl({ deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), + path: getRuleDetailsUrl(params.detailName, ''), }), }, ]; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts index 859790b4f342e..061dba0c37358 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { get, isEmpty } from 'lodash/fp'; +import { get } from 'lodash/fp'; import { ChromeBreadcrumb } from '@kbn/core/public'; import { hostsModel } from '../../store'; @@ -14,9 +14,8 @@ import { getHostDetailsUrl } from '../../../common/components/link_to/redirect_t import * as i18n from '../translations'; import { HostRouteSpyState } from '../../../common/utils/route/types'; -import { GetUrlForApp } from '../../../common/components/navigation/types'; -import { APP_UI_ID } from '../../../../common/constants'; import { SecurityPageName } from '../../../app/types'; +import { GetSecuritySolutionUrl } from '../../../common/components/link_to'; export const type = hostsModel.HostsType.details; @@ -31,28 +30,19 @@ const TabNameMappedToI18nKey: Record = { [HostsTableType.sessions]: i18n.NAVIGATION_SESSIONS_TITLE, }; -export const getBreadcrumbs = ( +export const getTrailingBreadcrumbs = ( params: HostRouteSpyState, - search: string[], - getUrlForApp: GetUrlForApp + getSecuritySolutionUrl: GetSecuritySolutionUrl ): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: getUrlForApp(APP_UI_ID, { - path: !isEmpty(search[0]) ? search[0] : '', - deepLinkId: SecurityPageName.hosts, - }), - }, - ]; + let breadcrumb: ChromeBreadcrumb[] = []; if (params.detailName != null) { breadcrumb = [ ...breadcrumb, { text: params.detailName, - href: getUrlForApp(APP_UI_ID, { - path: getHostDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), + href: getSecuritySolutionUrl({ + path: getHostDetailsUrl(params.detailName, ''), deepLinkId: SecurityPageName.hosts, }), }, diff --git a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts index 49b4214d60bd6..2fec83e423917 100644 --- a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts +++ b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts @@ -20,7 +20,7 @@ const TabNameMappedToI18nKey: Record = { [AdministrationSubTab.blocklist]: BLOCKLIST, }; -export function getBreadcrumbs(params: AdministrationRouteSpyState): ChromeBreadcrumb[] { +export function getTrailingBreadcrumbs(params: AdministrationRouteSpyState): ChromeBreadcrumb[] { return [ ...(params?.tabName ? [params?.tabName] : []).map((tabName) => ({ text: TabNameMappedToI18nKey[tabName], diff --git a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx index e01ab13722bf2..f28798af68dc2 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx @@ -50,7 +50,7 @@ import { SecurityPageName } from '../../../app/types'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; import { LandingPageComponent } from '../../../common/components/landing_page'; -export { getBreadcrumbs } from './utils'; +export { getTrailingBreadcrumbs } from './utils'; const NetworkDetailsManage = manageQuery(IpOverview); diff --git a/x-pack/plugins/security_solution/public/network/pages/details/utils.ts b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts index 044c1d22a6348..d0d885fc47a79 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { get, isEmpty } from 'lodash/fp'; +import { get } from 'lodash/fp'; import { ChromeBreadcrumb } from '@kbn/core/public'; import { decodeIpv6 } from '../../../common/lib/helpers'; @@ -14,9 +14,8 @@ import { networkModel } from '../../store'; import * as i18n from '../translations'; import { NetworkRouteType } from '../navigation/types'; import { NetworkRouteSpyState } from '../../../common/utils/route/types'; -import { GetUrlForApp } from '../../../common/components/navigation/types'; -import { APP_UI_ID } from '../../../../common/constants'; import { SecurityPageName } from '../../../app/types'; +import { GetSecuritySolutionUrl } from '../../../common/components/link_to'; export const type = networkModel.NetworkType.details; const TabNameMappedToI18nKey: Record = { @@ -28,33 +27,19 @@ const TabNameMappedToI18nKey: Record = { [NetworkRouteType.tls]: i18n.NAVIGATION_TLS_TITLE, }; -export const getBreadcrumbs = ( +export const getTrailingBreadcrumbs = ( params: NetworkRouteSpyState, - search: string[], - getUrlForApp: GetUrlForApp + getSecuritySolutionUrl: GetSecuritySolutionUrl ): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: getUrlForApp(APP_UI_ID, { - deepLinkId: SecurityPageName.network, - path: !isEmpty(search[0]) ? search[0] : '', - }), - }, - ]; + let breadcrumb: ChromeBreadcrumb[] = []; if (params.detailName != null) { breadcrumb = [ - ...breadcrumb, { text: decodeIpv6(params.detailName), - href: getUrlForApp(APP_UI_ID, { + href: getSecuritySolutionUrl({ deepLinkId: SecurityPageName.network, - path: getNetworkDetailsUrl( - params.detailName, - params.flowTarget, - !isEmpty(search[0]) ? search[0] : '' - ), + path: getNetworkDetailsUrl(params.detailName, params.flowTarget, ''), }), }, ]; diff --git a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx index b2c813087f8db..5ad969adba5cd 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx @@ -5,39 +5,20 @@ * 2.0. */ -import { isEmpty } from 'lodash/fp'; import React from 'react'; import { Switch, Route, Redirect } from 'react-router-dom'; -import { ChromeBreadcrumb } from '@kbn/core/public'; - import { TimelineType } from '../../../common/types/timeline'; -import { TimelineRouteSpyState } from '../../common/utils/route/types'; import { TimelinesPage } from './timelines_page'; -import { PAGE_TITLE } from './translations'; + import { appendSearch } from '../../common/components/link_to/helpers'; -import { GetUrlForApp } from '../../common/components/navigation/types'; -import { APP_UI_ID, TIMELINES_PATH } from '../../../common/constants'; -import { SecurityPageName } from '../../app/types'; + +import { TIMELINES_PATH } from '../../../common/constants'; const timelinesPagePath = `${TIMELINES_PATH}/:tabName(${TimelineType.default}|${TimelineType.template})`; const timelinesDefaultPath = `${TIMELINES_PATH}/${TimelineType.default}`; -export const getBreadcrumbs = ( - params: TimelineRouteSpyState, - search: string[], - getUrlForApp: GetUrlForApp -): ChromeBreadcrumb[] => [ - { - text: PAGE_TITLE, - href: getUrlForApp(APP_UI_ID, { - deepLinkId: SecurityPageName.timelines, - path: !isEmpty(search[0]) ? search[0] : '', - }), - }, -]; - export const Timelines = React.memo(() => ( diff --git a/x-pack/plugins/security_solution/public/users/pages/details/utils.ts b/x-pack/plugins/security_solution/public/users/pages/details/utils.ts index 26ed75997a85d..a9b3cb30ef84a 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/users/pages/details/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { get, isEmpty } from 'lodash/fp'; +import { get } from 'lodash/fp'; import { ChromeBreadcrumb } from '@kbn/core/public'; import { usersModel } from '../../store'; @@ -14,9 +14,8 @@ import { getUsersDetailsUrl } from '../../../common/components/link_to/redirect_ import * as i18n from '../translations'; import { UsersRouteSpyState } from '../../../common/utils/route/types'; -import { GetUrlForApp } from '../../../common/components/navigation/types'; -import { APP_UI_ID } from '../../../../common/constants'; import { SecurityPageName } from '../../../app/types'; +import { GetSecuritySolutionUrl } from '../../../common/components/link_to'; export const type = usersModel.UsersType.details; @@ -30,28 +29,18 @@ const TabNameMappedToI18nKey: Record = { [UsersTableType.risk]: i18n.NAVIGATION_RISK_TITLE, }; -export const getBreadcrumbs = ( +export const getTrailingBreadcrumbs = ( params: UsersRouteSpyState, - search: string[], - getUrlForApp: GetUrlForApp + getSecuritySolutionUrl: GetSecuritySolutionUrl ): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: getUrlForApp(APP_UI_ID, { - path: !isEmpty(search[0]) ? search[0] : '', - deepLinkId: SecurityPageName.users, - }), - }, - ]; + let breadcrumb: ChromeBreadcrumb[] = []; if (params.detailName != null) { breadcrumb = [ - ...breadcrumb, { text: params.detailName, - href: getUrlForApp(APP_UI_ID, { - path: getUsersDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), + href: getSecuritySolutionUrl({ + path: getUsersDetailsUrl(params.detailName, ''), deepLinkId: SecurityPageName.users, }), }, From e0944d17ece72cdddbdf07b41f5209e9ffda048c Mon Sep 17 00:00:00 2001 From: Nodir Latipov Date: Mon, 23 May 2022 13:27:24 +0500 Subject: [PATCH 069/120] [Unified search] Use the DataViews service (#130008) * feat: cleanup deprecated service and type * fix: rollback test * refact: replace deprecated type * refact: changed deprecation type * feat: added comments to deprecated imports that can't be cleaned up in this PR * refact: rollback query_string_input.test file --- .../public/actions/apply_filter_action.ts | 4 +++- .../apply_filters/apply_filter_popover_content.tsx | 2 +- .../public/apply_filters/apply_filters_popover.tsx | 2 +- .../providers/kql_query_suggestion/conjunction.test.ts | 2 +- .../providers/kql_query_suggestion/field.test.ts | 3 ++- .../providers/kql_query_suggestion/field.tsx | 1 + .../providers/kql_query_suggestion/operator.test.ts | 2 +- .../providers/kql_query_suggestion/value.test.ts | 2 +- .../providers/kql_query_suggestion/value.ts | 4 +++- .../providers/query_suggestion_provider.ts | 5 +++-- .../providers/value_suggestion_provider.ts | 10 ++++------ .../public/filter_bar/filter_editor/index.tsx | 8 ++++---- .../filter_editor/lib/filter_editor_utils.test.ts | 2 +- .../filter_editor/lib/filter_editor_utils.ts | 10 +++++----- .../filter_bar/filter_editor/lib/filter_label.tsx | 2 +- .../filter_bar/filter_editor/lib/filter_operators.ts | 8 ++++---- .../filter_bar/filter_editor/phrase_suggestor.tsx | 6 +++--- .../filter_bar/filter_editor/range_value_input.tsx | 4 ++-- .../filter_bar/filter_editor/value_input_type.tsx | 4 ++-- .../create_index_pattern_select.tsx | 4 ++-- .../index_pattern_select/index_pattern_select.tsx | 4 ++-- .../public/search_bar/create_search_bar.tsx | 3 ++- .../public/search_bar/lib/use_filter_manager.ts | 3 ++- .../public/test_helpers/get_stub_filter.ts | 3 ++- .../unified_search/public/utils/helpers.test.ts | 4 ++-- src/plugins/unified_search/public/utils/helpers.ts | 4 ++-- .../server/autocomplete/terms_agg.test.ts | 5 ++++- .../unified_search/server/autocomplete/terms_agg.ts | 9 +++++---- .../server/autocomplete/terms_enum.test.ts | 9 ++++++++- .../unified_search/server/autocomplete/terms_enum.ts | 4 ++-- 30 files changed, 76 insertions(+), 57 deletions(-) diff --git a/src/plugins/unified_search/public/actions/apply_filter_action.ts b/src/plugins/unified_search/public/actions/apply_filter_action.ts index 36524cf3ff826..465d6d33890de 100644 --- a/src/plugins/unified_search/public/actions/apply_filter_action.ts +++ b/src/plugins/unified_search/public/actions/apply_filter_action.ts @@ -10,7 +10,9 @@ import { i18n } from '@kbn/i18n'; import { ThemeServiceSetup } from '@kbn/core/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { Action, createAction, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; -import { Filter, FilterManager, TimefilterContract, esFilters } from '@kbn/data-plugin/public'; +// for cleanup esFilters need to fix the issue https://github.com/elastic/kibana/issues/131292 +import { FilterManager, TimefilterContract, esFilters } from '@kbn/data-plugin/public'; +import type { Filter } from '@kbn/es-query'; import { getOverlays, getIndexPatterns } from '../services'; import { applyFiltersPopover } from '../apply_filters'; diff --git a/src/plugins/unified_search/public/apply_filters/apply_filter_popover_content.tsx b/src/plugins/unified_search/public/apply_filters/apply_filter_popover_content.tsx index 9017fbf40ee2f..8119127e87e2c 100644 --- a/src/plugins/unified_search/public/apply_filters/apply_filter_popover_content.tsx +++ b/src/plugins/unified_search/public/apply_filters/apply_filter_popover_content.tsx @@ -24,7 +24,7 @@ import { mapAndFlattenFilters, getFieldDisplayValueFromFilter, } from '@kbn/data-plugin/public'; -import { Filter } from '@kbn/data-plugin/common'; +import type { Filter } from '@kbn/es-query'; import { DataView } from '@kbn/data-views-plugin/public'; import { FilterLabel } from '../filter_bar'; diff --git a/src/plugins/unified_search/public/apply_filters/apply_filters_popover.tsx b/src/plugins/unified_search/public/apply_filters/apply_filters_popover.tsx index 4cefbd1a202a0..8c515ae4e6d78 100644 --- a/src/plugins/unified_search/public/apply_filters/apply_filters_popover.tsx +++ b/src/plugins/unified_search/public/apply_filters/apply_filters_popover.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { Filter } from '@kbn/data-plugin/common'; +import type { Filter } from '@kbn/es-query'; import { DataView } from '@kbn/data-views-plugin/common'; type CancelFnType = () => void; diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts index 24a27bcb99fbe..d553538329874 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts @@ -7,7 +7,7 @@ */ import { coreMock } from '@kbn/core/public/mocks'; -import { KueryNode } from '@kbn/data-plugin/public'; +import type { KueryNode } from '@kbn/es-query'; import { setupGetConjunctionSuggestions } from './conjunction'; import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts index 4446fcf685bde..085ba3dc0979f 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts @@ -8,7 +8,8 @@ import indexPatternResponse from './__fixtures__/index_pattern_response.json'; -import { indexPatterns as indexPatternsUtils, KueryNode } from '@kbn/data-plugin/public'; +import { indexPatterns as indexPatternsUtils } from '@kbn/data-plugin/public'; +import type { KueryNode } from '@kbn/es-query'; import { setupGetFieldSuggestions } from './field'; import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; import { coreMock } from '@kbn/core/public/mocks'; diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx index 723b7e6896229..37f9c4658b81a 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +// for replace IFieldType => DataViewField need to fix the issue https://github.com/elastic/kibana/issues/131292 import { IFieldType, indexPatterns as indexPatternsUtils } from '@kbn/data-plugin/public'; import { flatten } from 'lodash'; import { sortPrefixFirst } from './sort_prefix_first'; diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts index a40678ad4ac16..7e2340fdb043a 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts @@ -9,7 +9,7 @@ import indexPatternResponse from './__fixtures__/index_pattern_response.json'; import { setupGetOperatorSuggestions } from './operator'; -import { KueryNode } from '@kbn/data-plugin/public'; +import type { KueryNode } from '@kbn/es-query'; import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; import { coreMock } from '@kbn/core/public/mocks'; diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts index 3405d26824a26..e852e8e11f347 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts @@ -10,7 +10,7 @@ import { setupGetValueSuggestions } from './value'; import indexPatternResponse from './__fixtures__/index_pattern_response.json'; import { coreMock } from '@kbn/core/public/mocks'; -import { KueryNode } from '@kbn/data-plugin/public'; +import type { KueryNode } from '@kbn/es-query'; import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; const mockKueryNode = (kueryNode: Partial) => kueryNode as unknown as KueryNode; diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts index 06b0fc9639a3c..0bbf416d99a2e 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts @@ -8,7 +8,9 @@ import { flatten } from 'lodash'; import { CoreSetup } from '@kbn/core/public'; -import { IFieldType, IIndexPattern } from '@kbn/data-plugin/public'; +// for replace IIndexPattern => DataView and IFieldType => DataViewField +// need to fix the issue https://github.com/elastic/kibana/issues/131292 +import type { IIndexPattern, IFieldType } from '@kbn/data-views-plugin/common'; import { escapeQuotes } from './lib/escape_kuery'; import { KqlQuerySuggestionProvider } from './types'; import type { UnifiedSearchPublicPluginStart } from '../../../types'; diff --git a/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts b/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts index 056fcb716054a..2e0e5c793f82f 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts @@ -7,7 +7,8 @@ */ import { ValueSuggestionsMethod } from '@kbn/data-plugin/common'; -import { IFieldType, IIndexPattern } from '@kbn/data-plugin/common'; +// for replace IIndexPattern => DataView need to fix the issue https://github.com/elastic/kibana/issues/131292 +import type { DataViewField, IIndexPattern } from '@kbn/data-views-plugin/common'; export enum QuerySuggestionTypes { Field = 'field', @@ -47,7 +48,7 @@ export interface QuerySuggestionBasic { /** @public **/ export interface QuerySuggestionField extends QuerySuggestionBasic { type: QuerySuggestionTypes.Field; - field: IFieldType; + field: DataViewField; } /** @public **/ diff --git a/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts index 2c25fe0230501..8d08a9de2577d 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts @@ -9,12 +9,10 @@ import { CoreSetup } from '@kbn/core/public'; import dateMath from '@kbn/datemath'; import { memoize } from 'lodash'; -import { - IIndexPattern, - IFieldType, - UI_SETTINGS, - ValueSuggestionsMethod, -} from '@kbn/data-plugin/common'; +import { UI_SETTINGS, ValueSuggestionsMethod } from '@kbn/data-plugin/common'; +// for replace IIndexPattern => DataView and IFieldType => DataViewField +// need to fix the issue https://github.com/elastic/kibana/issues/131292 +import type { IIndexPattern, IFieldType } from '@kbn/data-views-plugin/common'; import type { TimefilterSetup } from '@kbn/data-plugin/public'; import { AutocompleteUsageCollector } from '../collectors'; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx index 490d6480b28c9..6a3d7192ab905 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx @@ -33,7 +33,7 @@ import { import { get } from 'lodash'; import React, { Component } from 'react'; import { XJsonLang } from '@kbn/monaco'; -import { DataView, IFieldType } from '@kbn/data-views-plugin/common'; +import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { getIndexPatternFromFilter } from '@kbn/data-plugin/public'; import { CodeEditor } from '@kbn/kibana-react-plugin/public'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; @@ -61,7 +61,7 @@ export interface Props { interface State { selectedIndexPattern?: DataView; - selectedField?: IFieldType; + selectedField?: DataViewField; selectedOperator?: Operator; params: any; useCustomLabel: boolean; @@ -447,7 +447,7 @@ class FilterEditorUI extends Component { this.setState({ selectedIndexPattern, selectedField, selectedOperator, params }); }; - private onFieldChange = ([selectedField]: IFieldType[]) => { + private onFieldChange = ([selectedField]: DataViewField[]) => { const selectedOperator = undefined; const params = undefined; this.setState({ selectedField, selectedOperator, params }); @@ -529,7 +529,7 @@ function IndexPatternComboBox(props: GenericComboBoxProps) { return GenericComboBox(props); } -function FieldComboBox(props: GenericComboBoxProps) { +function FieldComboBox(props: GenericComboBoxProps) { return GenericComboBox(props); } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts index d6c44228eb72f..07ce05d039582 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts @@ -14,7 +14,7 @@ import { stubIndexPattern, stubFields, } from '@kbn/data-plugin/common/stubs'; -import { toggleFilterNegated } from '@kbn/data-plugin/common'; +import { toggleFilterNegated } from '@kbn/es-query'; import { getFieldFromFilter, getFilterableFields, diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts index f85b9a9e788d8..0863d10fe0c10 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -10,8 +10,8 @@ import dateMath from '@kbn/datemath'; import { Filter, FieldFilter } from '@kbn/es-query'; import { ES_FIELD_TYPES } from '@kbn/field-types'; import isSemverValid from 'semver/functions/valid'; -import { isFilterable, IFieldType, IpAddress } from '@kbn/data-plugin/common'; -import { DataView } from '@kbn/data-views-plugin/common'; +import { isFilterable, IpAddress } from '@kbn/data-plugin/common'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { FILTER_OPERATORS, Operator } from './filter_operators'; export function getFieldFromFilter(filter: FieldFilter, indexPattern: DataView) { @@ -28,7 +28,7 @@ export function getFilterableFields(indexPattern: DataView) { return indexPattern.fields.filter(isFilterable); } -export function getOperatorOptions(field: IFieldType) { +export function getOperatorOptions(field: DataViewField) { return FILTER_OPERATORS.filter((operator) => { if (operator.field) return operator.field(field); if (operator.fieldTypes) return operator.fieldTypes.includes(field.type); @@ -36,7 +36,7 @@ export function getOperatorOptions(field: IFieldType) { }); } -export function validateParams(params: any, field: IFieldType) { +export function validateParams(params: any, field: DataViewField) { switch (field.type) { case 'date': const moment = typeof params === 'string' ? dateMath.parse(params) : null; @@ -59,7 +59,7 @@ export function validateParams(params: any, field: IFieldType) { export function isFilterValid( indexPattern?: DataView, - field?: IFieldType, + field?: DataViewField, operator?: Operator, params?: any ) { diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx index 601cf68141c49..35c05316465f8 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx @@ -9,7 +9,7 @@ import React, { Fragment } from 'react'; import { EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Filter, FILTERS } from '@kbn/data-plugin/common'; +import { Filter, FILTERS } from '@kbn/es-query'; import { existsOperator, isOneOfOperator } from './filter_operators'; import type { FilterLabelStatus } from '../../filter_item/filter_item'; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_operators.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_operators.ts index c1e4d5361e3f8..6143158d69d5c 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_operators.ts +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_operators.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FILTERS } from '@kbn/es-query'; import { ES_FIELD_TYPES } from '@kbn/field-types'; -import { IFieldType } from '@kbn/data-views-plugin/common'; +import { DataViewField } from '@kbn/data-views-plugin/common'; export interface Operator { message: string; @@ -25,7 +25,7 @@ export interface Operator { * A filter predicate for a field, * takes precedence over {@link fieldTypes} */ - field?: (field: IFieldType) => boolean; + field?: (field: DataViewField) => boolean; } export const isOperator = { @@ -68,7 +68,7 @@ export const isBetweenOperator = { }), type: FILTERS.RANGE, negate: false, - field: (field: IFieldType) => { + field: (field: DataViewField) => { if (['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'].includes(field.type)) return true; @@ -84,7 +84,7 @@ export const isNotBetweenOperator = { }), type: FILTERS.RANGE, negate: true, - field: (field: IFieldType) => { + field: (field: DataViewField) => { if (['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'].includes(field.type)) return true; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx index 50acadea2a990..dc987421e2661 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { withKibana, KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; -import { IFieldType, UI_SETTINGS } from '@kbn/data-plugin/common'; -import { DataView } from '@kbn/data-views-plugin/common'; +import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { IDataPluginServices } from '@kbn/data-plugin/public'; import { debounce } from 'lodash'; @@ -18,7 +18,7 @@ import { getAutocomplete } from '../../services'; export interface PhraseSuggestorProps { kibana: KibanaReactContextValue; indexPattern: DataView; - field: IFieldType; + field: DataViewField; timeRangeForSuggestionsOverride?: boolean; } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx index 3c1046d928981..26a25886ac866 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx @@ -12,7 +12,7 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import { get } from 'lodash'; import React from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { IFieldType } from '@kbn/data-plugin/common'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { ValueInputType } from './value_input_type'; interface RangeParams { @@ -23,7 +23,7 @@ interface RangeParams { type RangeParamsPartial = Partial; interface Props { - field: IFieldType; + field: DataViewField; value?: RangeParams; onChange: (params: RangeParamsPartial) => void; intl: InjectedIntl; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/value_input_type.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/value_input_type.tsx index 1e50e92cec7bb..a87888ed85c93 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/value_input_type.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/value_input_type.tsx @@ -10,12 +10,12 @@ import { EuiFieldNumber, EuiFieldText, EuiSelect } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import { isEmpty } from 'lodash'; import React, { Component } from 'react'; -import { IFieldType } from '@kbn/data-views-plugin/common'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { validateParams } from './lib/filter_editor_utils'; interface Props { value?: string | number; - field: IFieldType; + field: DataViewField; onChange: (value: string | number | boolean) => void; onBlur?: (value: string | number | boolean) => void; placeholder: string; diff --git a/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx b/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx index 04b15aac84778..4dc7dc0f3b57b 100644 --- a/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx +++ b/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx @@ -8,11 +8,11 @@ import React from 'react'; -import { IndexPatternsContract } from '@kbn/data-plugin/public'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import { IndexPatternSelect, IndexPatternSelectProps } from '.'; // Takes in stateful runtime dependencies and pre-wires them to the component -export function createIndexPatternSelect(indexPatternService: IndexPatternsContract) { +export function createIndexPatternSelect(indexPatternService: DataViewsContract) { return (props: IndexPatternSelectProps) => ( ); diff --git a/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx b/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx index 335787d2ee38a..81534575d10b1 100644 --- a/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx @@ -11,7 +11,7 @@ import React, { Component } from 'react'; import { Required } from '@kbn/utility-types'; import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; -import { IndexPatternsContract } from '@kbn/data-plugin/public'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; export type IndexPatternSelectProps = Required< Omit< @@ -26,7 +26,7 @@ export type IndexPatternSelectProps = Required< }; export type IndexPatternSelectInternalProps = IndexPatternSelectProps & { - indexPatternService: IndexPatternsContract; + indexPatternService: DataViewsContract; }; interface IndexPatternSelectState { diff --git a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx index c73aa258863ed..a90098ebcf156 100644 --- a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx @@ -12,7 +12,8 @@ import { CoreStart } from '@kbn/core/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { QueryStart, SavedQuery, DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { Filter, Query, TimeRange } from '@kbn/data-plugin/common'; +import { Query, TimeRange } from '@kbn/data-plugin/common'; +import type { Filter } from '@kbn/es-query'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { SearchBar } from '.'; import type { SearchBarOwnProps } from '.'; diff --git a/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts b/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts index 511e05e043b26..a6d0487cb90c7 100644 --- a/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts +++ b/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts @@ -8,7 +8,8 @@ import { useState, useEffect } from 'react'; import { Subscription } from 'rxjs'; -import { DataPublicPluginStart, Filter } from '@kbn/data-plugin/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { Filter } from '@kbn/es-query'; interface UseFilterManagerProps { filters?: Filter[]; diff --git a/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts b/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts index 2954526d7ede8..10444d1d19055 100644 --- a/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts +++ b/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { Filter, FilterStateStore } from '@kbn/data-plugin/public'; +import { FilterStateStore } from '@kbn/data-plugin/public'; +import type { Filter } from '@kbn/es-query'; export function getFilter( store: FilterStateStore, diff --git a/src/plugins/unified_search/public/utils/helpers.test.ts b/src/plugins/unified_search/public/utils/helpers.test.ts index 4659e35602228..803d6c53bb007 100644 --- a/src/plugins/unified_search/public/utils/helpers.test.ts +++ b/src/plugins/unified_search/public/utils/helpers.test.ts @@ -7,11 +7,11 @@ */ import { getFieldValidityAndErrorMessage } from './helpers'; -import { IFieldType } from '@kbn/data-views-plugin/common'; +import { DataViewField } from '@kbn/data-views-plugin/common'; const mockField = { type: 'date', -} as IFieldType; +} as DataViewField; describe('Check field validity and error message', () => { it('should return a message that the entered date is not incorrect', () => { diff --git a/src/plugins/unified_search/public/utils/helpers.ts b/src/plugins/unified_search/public/utils/helpers.ts index 1c056636c67b8..6f0a605fa0e14 100644 --- a/src/plugins/unified_search/public/utils/helpers.ts +++ b/src/plugins/unified_search/public/utils/helpers.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -import type { IFieldType } from '@kbn/data-views-plugin/common'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { i18n } from '@kbn/i18n'; import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; import { isEmpty } from 'lodash'; import { validateParams } from '../filter_bar/filter_editor/lib/filter_editor_utils'; export const getFieldValidityAndErrorMessage = ( - field: IFieldType, + field: DataViewField, value?: string | undefined ): { isInvalid: boolean; errorMessage?: string } => { const type = field.type; diff --git a/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts b/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts index f27fa9d594f97..03ceffc73b34f 100644 --- a/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts @@ -10,6 +10,7 @@ import { coreMock } from '@kbn/core/server/mocks'; import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { ConfigSchema } from '../../config'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { termsAggSuggestions } from './terms_agg'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { duration } from 'moment'; @@ -22,6 +23,8 @@ const configMock = { }, } as unknown as ConfigSchema; +const dataViewFieldMock = { name: 'field_name', type: 'string' } as DataViewField; + // @ts-expect-error not full interface const mockResponse = { aggregations: { @@ -50,7 +53,7 @@ describe('terms agg suggestions', () => { 'fieldName', 'query', [], - { name: 'field_name', type: 'string' } + dataViewFieldMock ); const [[args]] = esClientMock.search.mock.calls; diff --git a/src/plugins/unified_search/server/autocomplete/terms_agg.ts b/src/plugins/unified_search/server/autocomplete/terms_agg.ts index ffdaca8caad4b..c7d303e526ca8 100644 --- a/src/plugins/unified_search/server/autocomplete/terms_agg.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_agg.ts @@ -9,7 +9,8 @@ import { get, map } from 'lodash'; import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { IFieldType, getFieldSubtypeNested } from '@kbn/data-plugin/common'; +import { getFieldSubtypeNested } from '@kbn/data-plugin/common'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; import { ConfigSchema } from '../../config'; import { findIndexPatternById, getFieldByName } from '../data_views'; @@ -21,7 +22,7 @@ export async function termsAggSuggestions( fieldName: string, query: string, filters?: estypes.QueryDslQueryContainer[], - field?: IFieldType, + field?: FieldSpec, abortSignal?: AbortSignal ) { const autocompleteSearchOptions = { @@ -54,11 +55,11 @@ export async function termsAggSuggestions( async function getBody( // eslint-disable-next-line @typescript-eslint/naming-convention { timeout, terminate_after }: Record, - field: IFieldType | string, + field: FieldSpec | string, query: string, filters: estypes.QueryDslQueryContainer[] = [] ) { - const isFieldObject = (f: any): f is IFieldType => Boolean(f && f.name); + const isFieldObject = (f: any): f is FieldSpec => Boolean(f && f.name); // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators const getEscapedQuery = (q: string = '') => diff --git a/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts b/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts index bc2a4e010a765..f0209e66ee58d 100644 --- a/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts @@ -12,12 +12,19 @@ import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/serve import { ConfigSchema } from '../../config'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { TermsEnumResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; let savedObjectsClientMock: jest.Mocked; let esClientMock: DeeplyMockedKeys; const configMock = { autocomplete: { valueSuggestions: { tiers: ['data_hot', 'data_warm', 'data_content'] } }, } as ConfigSchema; +const dataViewFieldMock = { + name: 'field_name', + type: 'string', + searchable: true, + aggregatable: true, +} as DataViewField; const mockResponse = { terms: ['whoa', 'amazing'] }; jest.mock('../data_views'); @@ -39,7 +46,7 @@ describe('_terms_enum suggestions', () => { 'fieldName', 'query', [], - { name: 'field_name', type: 'string', searchable: true, aggregatable: true } + dataViewFieldMock ); const [[args]] = esClientMock.termsEnum.mock.calls; diff --git a/src/plugins/unified_search/server/autocomplete/terms_enum.ts b/src/plugins/unified_search/server/autocomplete/terms_enum.ts index 924b5b3a1671e..3e8207eb644e5 100644 --- a/src/plugins/unified_search/server/autocomplete/terms_enum.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_enum.ts @@ -8,7 +8,7 @@ import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { IFieldType } from '@kbn/data-plugin/common'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; import { findIndexPatternById, getFieldByName } from '../data_views'; import { ConfigSchema } from '../../config'; @@ -20,7 +20,7 @@ export async function termsEnumSuggestions( fieldName: string, query: string, filters?: estypes.QueryDslQueryContainer[], - field?: IFieldType, + field?: FieldSpec, abortSignal?: AbortSignal ) { const { tiers } = config.autocomplete.valueSuggestions; From ae8b6c8beb3ad0b3f30de3f520fdf9dcb4e23e01 Mon Sep 17 00:00:00 2001 From: "Lucas F. da Costa" Date: Mon, 23 May 2022 09:29:11 +0100 Subject: [PATCH 070/120] [Uptime] Fix bug causing all monitors to be saved to all locations [solves #132314] (#132325) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../formatters/format_configs.test.ts | 2 + .../formatters/format_configs.ts | 1 - .../synthetics_service.test.ts | 72 ++++++++++++++++++- .../synthetics_service/synthetics_service.ts | 4 +- 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts index c30d9af766b48..48d052d35a1f8 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts @@ -53,6 +53,7 @@ describe('formatMonitorConfig', () => { expect(yamlConfig).toEqual({ 'check.request.method': 'GET', enabled: true, + locations: [], max_redirects: '0', name: 'Test', password: '3z9SBOQWW5F0UrdqLVFqlF6z', @@ -110,6 +111,7 @@ describe('formatMonitorConfig', () => { 'filter_journeys.tags': ['dev'], ignore_https_errors: false, name: 'Test', + locations: [], schedule: '@every 3m', screenshots: 'on', 'source.inline.script': diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts index e2a1bf1b869ed..ea298992d2246 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts @@ -15,7 +15,6 @@ const UI_KEYS_TO_SKIP = [ ConfigKey.DOWNLOAD_SPEED, ConfigKey.LATENCY, ConfigKey.IS_THROTTLING_ENABLED, - ConfigKey.LOCATIONS, ConfigKey.REVISION, 'secrets', ]; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts index 305f1d15a4823..952e18ce9c884 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts @@ -5,10 +5,13 @@ * 2.0. */ -import { SyntheticsService } from './synthetics_service'; +jest.mock('axios', () => jest.fn()); + +import { SyntheticsService, SyntheticsConfig } from './synthetics_service'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { loggerMock } from '@kbn/core/server/logging/logger.mock'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; +import axios, { AxiosResponse } from 'axios'; describe('SyntheticsService', () => { const mockEsClient = { @@ -67,4 +70,71 @@ describe('SyntheticsService', () => { }, ]); }); + + describe('addConfig', () => { + afterEach(() => jest.restoreAllMocks()); + + it('saves configs only to the selected locations', async () => { + serverMock.config = { service: { devUrl: 'http://localhost' } }; + const service = new SyntheticsService(logger, serverMock, { + username: 'dev', + password: '12345', + }); + + service.apiClient.locations = [ + { + id: 'selected', + label: 'Selected Location', + url: 'example.com/1', + geo: { + lat: 0, + lon: 0, + }, + isServiceManaged: true, + }, + { + id: 'not selected', + label: 'Not Selected Location', + url: 'example.com/2', + geo: { + lat: 0, + lon: 0, + }, + isServiceManaged: true, + }, + ]; + + jest.spyOn(service, 'getApiKey').mockResolvedValue({ name: 'example', id: 'i', apiKey: 'k' }); + jest.spyOn(service, 'getOutput').mockResolvedValue({ hosts: ['es'], api_key: 'i:k' }); + + const payload = { + type: 'http', + enabled: true, + schedule: { + number: '3', + unit: 'm', + }, + name: 'my mon', + locations: [{ id: 'selected', isServiceManaged: true }], + urls: 'http://google.com', + max_redirects: '0', + password: '', + proxy_url: '', + id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d', + fields: { config_id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d' }, + fields_under_root: true, + }; + + (axios as jest.MockedFunction).mockResolvedValue({} as AxiosResponse); + + await service.addConfig(payload as SyntheticsConfig); + + expect(axios).toHaveBeenCalledTimes(1); + expect(axios).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'example.com/1/monitors', + }) + ); + }); + }); }); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts index f655dd6d4cc8c..b1af1717e1a1c 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -48,7 +48,7 @@ const SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_TYPE = const SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_ID = 'UPTIME:SyntheticsService:sync-task'; const SYNTHETICS_SERVICE_SYNC_INTERVAL_DEFAULT = '5m'; -type SyntheticsConfig = SyntheticsMonitorWithId & { +export type SyntheticsConfig = SyntheticsMonitorWithId & { fields_under_root?: boolean; fields?: { config_id: string; run_once?: boolean; test_run_id?: string }; }; @@ -56,7 +56,7 @@ type SyntheticsConfig = SyntheticsMonitorWithId & { export class SyntheticsService { private logger: Logger; private readonly server: UptimeServerSetup; - private apiClient: ServiceAPIClient; + public apiClient: ServiceAPIClient; private readonly config: ServiceConfig; private readonly esHosts: string[]; From 37d40d7343510a6eb5457ad123b700882f46f627 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 23 May 2022 04:56:34 -0400 Subject: [PATCH 071/120] [Synthetics] fix browser type as default in monitor management (#132572) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../synthetics/e2e/journeys/monitor_details.journey.ts | 4 ++-- .../synthetics/e2e/journeys/monitor_name.journey.ts | 8 ++++---- .../synthetics/e2e/page_objects/monitor_management.tsx | 1 + .../fleet_package/contexts/policy_config_context.tsx | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_details.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/monitor_details.journey.ts index 192fcf06c3095..3ddf0cebd0cf3 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_details.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/monitor_details.journey.ts @@ -40,12 +40,12 @@ journey('MonitorDetails', async ({ page, params }: { page: Page; params: any }) step('create basic monitor', async () => { await uptime.enableMonitorManagement(); await uptime.clickAddMonitor(); - await uptime.createBasicMonitorDetails({ + await uptime.createBasicHTTPMonitorDetails({ name, locations: ['US Central'], apmServiceName: 'synthetics', + url: 'https://www.google.com', }); - await uptime.fillByTestSubj('syntheticsUrlField', 'https://www.google.com'); await uptime.confirmAndSave(); }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts index a21627548aeb1..a9dd2c4633402 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts @@ -21,12 +21,12 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl }); const createBasicMonitor = async () => { - await uptime.createBasicMonitorDetails({ + await uptime.createBasicHTTPMonitorDetails({ name, locations: ['US Central'], apmServiceName: 'synthetics', + url: 'https://www.google.com', }); - await uptime.fillByTestSubj('syntheticsUrlField', 'https://www.google.com'); }; before(async () => { @@ -52,12 +52,12 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => step(`shows error if name already exists`, async () => { await uptime.navigateToAddMonitor(); - await uptime.createBasicMonitorDetails({ + await uptime.createBasicHTTPMonitorDetails({ name, locations: ['US Central'], apmServiceName: 'synthetics', + url: 'https://www.google.com', }); - await uptime.fillByTestSubj('syntheticsUrlField', 'https://www.google.com'); await uptime.assertText({ text: 'Monitor name already exists.' }); diff --git a/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx index eb13c3678f47e..91d8151c29701 100644 --- a/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx @@ -189,6 +189,7 @@ export function monitorManagementPageProvider({ apmServiceName: string; locations: string[]; }) { + await this.selectMonitorType('http'); await this.createBasicMonitorDetails({ name, apmServiceName, locations }); await this.fillByTestSubj('syntheticsUrlField', url); }, diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/contexts/policy_config_context.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/contexts/policy_config_context.tsx index a9cdc2c78d86d..99419e2ca9145 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/contexts/policy_config_context.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/contexts/policy_config_context.tsx @@ -121,10 +121,10 @@ export function PolicyConfigContextProvider({ const isAddMonitorRoute = useRouteMatch(MONITOR_ADD_ROUTE); useEffect(() => { - if (isAddMonitorRoute) { + if (isAddMonitorRoute?.isExact) { setMonitorType(DataStream.BROWSER); } - }, [isAddMonitorRoute]); + }, [isAddMonitorRoute?.isExact]); const value = useMemo(() => { return { From 7591fb61556bc56aaae725d6db51bd80e958d91b Mon Sep 17 00:00:00 2001 From: Giorgos Bamparopoulos Date: Mon, 23 May 2022 10:37:03 +0100 Subject: [PATCH 072/120] Fix agent config indicator when applied through fleet integration (#131820) * Fix agent config indicator when applied through fleet integration * Add synthrace scenario Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/elastic-apm-synthtrace/src/index.ts | 1 + .../src/lib/agent_config/agent_config.ts | 24 +++++ .../lib/agent_config/agent_config_fields.ts | 25 ++++++ .../src/lib/agent_config/index.ts | 9 ++ .../src/lib/agent_config/observer.ts | 21 +++++ .../src/lib/apm/instance.ts | 2 +- .../src/lib/apm/metricset.ts | 6 +- .../src/lib/stream_processor.ts | 4 +- .../src/scripts/examples/04_agent_config.ts | 36 ++++++++ .../configuration_types.d.ts | 2 +- .../convert_settings_to_string.ts | 32 ++++--- .../find_exact_configuration.ts | 24 +++-- ...t_config_applied_to_agent_through_fleet.ts | 60 +++++++++++++ .../list_configurations.ts | 23 +++-- .../settings/agent_configuration/route.ts | 10 ++- .../add_agent_config_metrics.ts | 31 +++++++ .../agent_configuration.spec.ts | 89 ++++++++++++++++++- 17 files changed, 363 insertions(+), 36 deletions(-) create mode 100644 packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config.ts create mode 100644 packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config_fields.ts create mode 100644 packages/elastic-apm-synthtrace/src/lib/agent_config/index.ts create mode 100644 packages/elastic-apm-synthtrace/src/lib/agent_config/observer.ts create mode 100644 packages/elastic-apm-synthtrace/src/scripts/examples/04_agent_config.ts create mode 100644 x-pack/plugins/apm/server/routes/settings/agent_configuration/get_config_applied_to_agent_through_fleet.ts create mode 100644 x-pack/test/apm_api_integration/tests/settings/agent_configuration/add_agent_config_metrics.ts rename x-pack/test/apm_api_integration/tests/settings/{ => agent_configuration}/agent_configuration.spec.ts (85%) diff --git a/packages/elastic-apm-synthtrace/src/index.ts b/packages/elastic-apm-synthtrace/src/index.ts index ab6a3e3731be7..3e7a2f1d59190 100644 --- a/packages/elastic-apm-synthtrace/src/index.ts +++ b/packages/elastic-apm-synthtrace/src/index.ts @@ -9,6 +9,7 @@ export { timerange } from './lib/timerange'; export { apm } from './lib/apm'; export { stackMonitoring } from './lib/stack_monitoring'; +export { observer } from './lib/agent_config'; export { cleanWriteTargets } from './lib/utils/clean_write_targets'; export { createLogger, LogLevel } from './lib/utils/create_logger'; diff --git a/packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config.ts b/packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config.ts new file mode 100644 index 0000000000000..5ec90035141da --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AgentConfigFields } from './agent_config_fields'; +import { Metricset } from '../apm/metricset'; + +export class AgentConfig extends Metricset { + constructor() { + super({ + 'metricset.name': 'agent_config', + agent_config_applied: 1, + }); + } + + etag(etag: string) { + this.fields['labels.etag'] = etag; + return this; + } +} diff --git a/packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config_fields.ts b/packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config_fields.ts new file mode 100644 index 0000000000000..82b0963cee6e6 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config_fields.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ApmFields } from '../apm/apm_fields'; + +export type AgentConfigFields = Pick< + ApmFields, + | '@timestamp' + | 'processor.event' + | 'processor.name' + | 'metricset.name' + | 'observer' + | 'ecs.version' + | 'event.ingested' +> & + Partial<{ + 'labels.etag': string; + agent_config_applied: number; + 'event.agent_id_status': string; + }>; diff --git a/packages/elastic-apm-synthtrace/src/lib/agent_config/index.ts b/packages/elastic-apm-synthtrace/src/lib/agent_config/index.ts new file mode 100644 index 0000000000000..204a12386b275 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/agent_config/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { observer } from './observer'; diff --git a/packages/elastic-apm-synthtrace/src/lib/agent_config/observer.ts b/packages/elastic-apm-synthtrace/src/lib/agent_config/observer.ts new file mode 100644 index 0000000000000..189f3f62abb39 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/agent_config/observer.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AgentConfigFields } from './agent_config_fields'; +import { AgentConfig } from './agent_config'; +import { Entity } from '../entity'; + +export class Observer extends Entity { + agentConfig() { + return new AgentConfig(); + } +} + +export function observer() { + return new Observer({}); +} diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/instance.ts b/packages/elastic-apm-synthtrace/src/lib/apm/instance.ts index 4051d7e8241da..9a7664e9518ce 100644 --- a/packages/elastic-apm-synthtrace/src/lib/apm/instance.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/instance.ts @@ -45,7 +45,7 @@ export class Instance extends Entity { } appMetrics(metrics: ApmApplicationMetricFields) { - return new Metricset({ + return new Metricset({ ...this.fields, 'metricset.name': 'app', ...metrics, diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/metricset.ts b/packages/elastic-apm-synthtrace/src/lib/apm/metricset.ts index 88177e816a852..515af829c6a5a 100644 --- a/packages/elastic-apm-synthtrace/src/lib/apm/metricset.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/metricset.ts @@ -7,10 +7,10 @@ */ import { Serializable } from '../serializable'; -import { ApmFields } from './apm_fields'; +import { Fields } from '../entity'; -export class Metricset extends Serializable { - constructor(fields: ApmFields) { +export class Metricset extends Serializable { + constructor(fields: TFields) { super({ 'processor.event': 'metric', 'processor.name': 'metric', diff --git a/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts b/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts index e1cb332996e23..a6f8f923b3714 100644 --- a/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts +++ b/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts @@ -211,7 +211,9 @@ export class StreamProcessor { const eventType = d.processor.event as keyof ApmElasticsearchOutputWriteTargets; let dataStream = writeTargets[eventType]; if (eventType === 'metric') { - if (!d.service?.name) { + if (d.metricset?.name === 'agent_config') { + dataStream = 'metrics-apm.internal-default'; + } else if (!d.service?.name) { dataStream = 'metrics-apm.app-default'; } else { if (!d.transaction && !d.span) { diff --git a/packages/elastic-apm-synthtrace/src/scripts/examples/04_agent_config.ts b/packages/elastic-apm-synthtrace/src/scripts/examples/04_agent_config.ts new file mode 100644 index 0000000000000..ec6d57eba4b61 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/scripts/examples/04_agent_config.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { observer, timerange } from '../..'; +import { Scenario } from '../scenario'; +import { getLogger } from '../utils/get_common_services'; +import { RunOptions } from '../utils/parse_run_cli_flags'; +import { AgentConfigFields } from '../../lib/agent_config/agent_config_fields'; + +const scenario: Scenario = async (runOptions: RunOptions) => { + const logger = getLogger(runOptions); + + return { + generate: ({ from, to }) => { + const agentConfig = observer().agentConfig(); + + const range = timerange(from, to); + return range + .interval('30s') + .rate(1) + .generator((timestamp) => { + const events = logger.perf('generating_agent_config_events', () => { + return agentConfig.etag('test-etag').timestamp(timestamp); + }); + return events; + }); + }, + }; +}; + +export default scenario; diff --git a/x-pack/plugins/apm/common/agent_configuration/configuration_types.d.ts b/x-pack/plugins/apm/common/agent_configuration/configuration_types.d.ts index 0f315c1583f1a..88302dea91200 100644 --- a/x-pack/plugins/apm/common/agent_configuration/configuration_types.d.ts +++ b/x-pack/plugins/apm/common/agent_configuration/configuration_types.d.ts @@ -15,6 +15,6 @@ export type AgentConfigurationIntake = t.TypeOf< export type AgentConfiguration = { '@timestamp': number; applied_by_agent?: boolean; - etag?: string; + etag: string; agent_name?: string; } & AgentConfigurationIntake; diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/convert_settings_to_string.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/convert_settings_to_string.ts index d52b048bc6b46..a0b3fa2e45c54 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/convert_settings_to_string.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/convert_settings_to_string.ts @@ -13,17 +13,27 @@ import { AgentConfiguration } from '../../../../common/agent_configuration/confi export function convertConfigSettingsToString( hit: SearchHit ) { - const config = hit._source; + const { settings } = hit._source; - if (config.settings?.transaction_sample_rate) { - config.settings.transaction_sample_rate = - config.settings.transaction_sample_rate.toString(); - } + const convertedConfigSettings = { + ...settings, + ...(settings?.transaction_sample_rate + ? { + transaction_sample_rate: settings.transaction_sample_rate.toString(), + } + : {}), + ...(settings?.transaction_max_spans + ? { + transaction_max_spans: settings.transaction_max_spans.toString(), + } + : {}), + }; - if (config.settings?.transaction_max_spans) { - config.settings.transaction_max_spans = - config.settings.transaction_max_spans.toString(); - } - - return hit; + return { + ...hit, + _source: { + ...hit._source, + settings: convertedConfigSettings, + }, + }; } diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/find_exact_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/find_exact_configuration.ts index 18e2fe0f34a6d..f32e53a1ad1dd 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/find_exact_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/find_exact_configuration.ts @@ -13,6 +13,7 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../lib/helpers/setup_request'; import { convertConfigSettingsToString } from './convert_settings_to_string'; +import { getConfigsAppliedToAgentsThroughFleet } from './get_config_applied_to_agent_through_fleet'; export async function findExactConfiguration({ service, @@ -40,16 +41,27 @@ export async function findExactConfiguration({ }, }; - const resp = await internalClient.search( - 'find_exact_agent_configuration', - params - ); + const [agentConfig, configsAppliedToAgentsThroughFleet] = await Promise.all([ + internalClient.search( + 'find_exact_agent_configuration', + params + ), + getConfigsAppliedToAgentsThroughFleet({ setup }), + ]); - const hit = resp.hits.hits[0] as SearchHit | undefined; + const hit = agentConfig.hits.hits[0] as + | SearchHit + | undefined; if (!hit) { return; } - return convertConfigSettingsToString(hit); + return { + id: hit._id, + ...convertConfigSettingsToString(hit)._source, + applied_by_agent: + hit._source.applied_by_agent || + configsAppliedToAgentsThroughFleet.hasOwnProperty(hit._source.etag), + }; } diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/get_config_applied_to_agent_through_fleet.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/get_config_applied_to_agent_through_fleet.ts new file mode 100644 index 0000000000000..351c21b43c1e9 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/get_config_applied_to_agent_through_fleet.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { termQuery, rangeQuery } from '@kbn/observability-plugin/server'; +import datemath from '@kbn/datemath'; +import { METRICSET_NAME } from '../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../../lib/helpers/setup_request'; + +export async function getConfigsAppliedToAgentsThroughFleet({ + setup, +}: { + setup: Setup; +}) { + const { internalClient, indices } = setup; + + const params = { + index: indices.metric, + size: 0, + body: { + query: { + bool: { + filter: [ + ...termQuery(METRICSET_NAME, 'agent_config'), + ...rangeQuery( + datemath.parse('now-15m')!.valueOf(), + datemath.parse('now')!.valueOf() + ), + ], + }, + }, + aggs: { + config_by_etag: { + terms: { + field: 'labels.etag', + size: 200, + }, + }, + }, + }, + }; + + const response = await internalClient.search( + 'get_config_applied_to_agent_through_fleet', + params + ); + + return ( + response.aggregations?.config_by_etag.buckets.reduce( + (configsAppliedToAgentsThroughFleet, bucket) => { + configsAppliedToAgentsThroughFleet[bucket.key as string] = true; + return configsAppliedToAgentsThroughFleet; + }, + {} as Record + ) ?? {} + ); +} diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/list_configurations.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/list_configurations.ts index bc105106cb5e4..416cb50c0a801 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/list_configurations.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/list_configurations.ts @@ -8,6 +8,7 @@ import { Setup } from '../../../lib/helpers/setup_request'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; import { convertConfigSettingsToString } from './convert_settings_to_string'; +import { getConfigsAppliedToAgentsThroughFleet } from './get_config_applied_to_agent_through_fleet'; export async function listConfigurations({ setup }: { setup: Setup }) { const { internalClient, indices } = setup; @@ -17,12 +18,22 @@ export async function listConfigurations({ setup }: { setup: Setup }) { size: 200, }; - const resp = await internalClient.search( - 'list_agent_configuration', - params - ); + const [agentConfigs, configsAppliedToAgentsThroughFleet] = await Promise.all([ + internalClient.search( + 'list_agent_configuration', + params + ), + getConfigsAppliedToAgentsThroughFleet({ setup }), + ]); - return resp.hits.hits + return agentConfigs.hits.hits .map(convertConfigSettingsToString) - .map((hit) => hit._source); + .map((hit) => { + return { + ...hit._source, + applied_by_agent: + hit._source.applied_by_agent || + configsAppliedToAgentsThroughFleet.hasOwnProperty(hit._source.etag), + }; + }); } diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts index 72869ef165fa2..3d9abebeeef2b 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts @@ -38,7 +38,9 @@ const agentConfigurationRoute = createApmServerRoute({ >; }> => { const setup = await setupRequest(resources); + const configurations = await listConfigurations({ setup }); + return { configurations }; }, }); @@ -71,7 +73,7 @@ const getSingleAgentConfigurationRoute = createApmServerRoute({ throw Boom.notFound(); } - return config._source; + return config; }, }); @@ -102,11 +104,11 @@ const deleteAgentConfigurationRoute = createApmServerRoute({ } logger.info( - `Deleting config ${service.name}/${service.environment} (${config._id})` + `Deleting config ${service.name}/${service.environment} (${config.id})` ); const deleteConfigurationResult = await deleteConfiguration({ - configurationId: config._id, + configurationId: config.id, setup, }); @@ -162,7 +164,7 @@ const createOrUpdateAgentConfigurationRoute = createApmServerRoute({ ); await createOrUpdateConfiguration({ - configurationId: config?._id, + configurationId: config?.id, configurationIntake: body, setup, }); diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_configuration/add_agent_config_metrics.ts b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/add_agent_config_metrics.ts new file mode 100644 index 0000000000000..f0329a220c71a --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/add_agent_config_metrics.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { timerange, observer } from '@elastic/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace'; + +export async function addAgentConfigMetrics({ + synthtraceEsClient, + start, + end, + etag, +}: { + synthtraceEsClient: ApmSynthtraceEsClient; + start: number; + end: number; + etag?: string; +}) { + const agentConfig = observer().agentConfig(); + + const agentConfigEvents = [ + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => agentConfig.etag(etag ?? 'test-etag').timestamp(timestamp)), + ]; + + await synthtraceEsClient.index(agentConfigEvents); +} diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_configuration.spec.ts b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts similarity index 85% rename from x-pack/test/apm_api_integration/tests/settings/agent_configuration.spec.ts rename to x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts index ecf5b87e82d70..e4960791eee5a 100644 --- a/x-pack/test/apm_api_integration/tests/settings/agent_configuration.spec.ts +++ b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts @@ -11,14 +11,17 @@ import expect from '@kbn/expect'; import { omit, orderBy } from 'lodash'; import { AgentConfigurationIntake } from '@kbn/apm-plugin/common/agent_configuration/configuration_types'; import { AgentConfigSearchParams } from '@kbn/apm-plugin/server/routes/settings/agent_configuration/route'; - -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import moment from 'moment'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { addAgentConfigMetrics } from './add_agent_config_metrics'; export default function agentConfigurationTests({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); const log = getService('log'); + const synthtraceEsClient = getService('synthtraceEsClient'); const archiveName = 'apm_8.0.0'; @@ -77,6 +80,18 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte }); } + function findExactConfiguration(name: string, environment: string) { + return apmApiClient.readUser({ + endpoint: 'GET /api/apm/settings/agent-configuration/view', + params: { + query: { + name, + environment, + }, + }, + }); + } + registry.when( 'agent configuration when no data is loaded', { config: 'basic', archives: [] }, @@ -297,7 +312,7 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte service: { name: 'myservice', environment: 'production' }, settings: { transaction_sample_rate: '0.9' }, }; - let etag: string | undefined; + let etag: string; before(async () => { log.debug('creating agent configuration'); @@ -371,6 +386,74 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte } ); + registry.when( + 'Agent configurations through fleet', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + const name = 'myservice'; + const environment = 'development'; + const testConfig = { + service: { name, environment }, + settings: { transaction_sample_rate: '0.9' }, + }; + + let agentConfiguration: + | APIReturnType<'GET /api/apm/settings/agent-configuration/view'> + | undefined; + + before(async () => { + log.debug('creating agent configuration'); + await createConfiguration(testConfig); + const { body } = await findExactConfiguration(name, environment); + agentConfiguration = body; + }); + + after(async () => { + await deleteConfiguration(testConfig); + }); + + it(`should have 'applied_by_agent=false' when there are no agent config metrics for this etag`, async () => { + expect(agentConfiguration?.applied_by_agent).to.be(false); + }); + + describe('when there are agent config metrics for this etag', () => { + before(async () => { + const start = new Date().getTime(); + const end = moment(start).add(15, 'minutes').valueOf(); + + await addAgentConfigMetrics({ + synthtraceEsClient, + start, + end, + etag: agentConfiguration?.etag, + }); + }); + + after(() => synthtraceEsClient.clean()); + + it(`should have 'applied_by_agent=true' when getting a config from all configurations`, async () => { + const { + body: { configurations }, + } = await getAllConfigurations(); + + const updatedConfig = configurations.find( + (x) => x.service.name === name && x.service.environment === environment + ); + + expect(updatedConfig?.applied_by_agent).to.be(true); + }); + + it(`should have 'applied_by_agent=true' when getting a single config`, async () => { + const { + body: { applied_by_agent: appliedByAgent }, + } = await findExactConfiguration(name, environment); + + expect(appliedByAgent).to.be(true); + }); + }); + } + ); + registry.when( 'agent configuration when data is loaded', { config: 'basic', archives: [archiveName] }, From 2cddced8c3e0da5831fd160700ea80ead8540a07 Mon Sep 17 00:00:00 2001 From: Jordan <51442161+JordanSh@users.noreply.github.com> Date: Mon, 23 May 2022 12:50:55 +0300 Subject: [PATCH 073/120] [Cloud Posture] Trendline query changes (#132680) --- .../cloud_posture_score_chart.tsx | 7 ++++++- .../routes/compliance_dashboard/get_trends.ts | 14 +++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx index 540402b986e5b..9fd7806d27665 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx @@ -129,7 +129,12 @@ const ComplianceTrendChart = ({ trend }: { trend: PostureTrend[] }) => { xAccessor={'timestamp'} yAccessors={['postureScore']} /> - + ({ index: BENCHMARK_SCORE_INDEX_DEFAULT_NS, - size: 5, + size: 99, sort: '@timestamp:desc', + query: { + bool: { + must: { + range: { + '@timestamp': { + gte: 'now-1d', + lte: 'now', + }, + }, + }, + }, + }, }); export type Trends = Array<{ From 693b3e85a49c62c58effe22bbf4aecd90a0bb246 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Mon, 23 May 2022 11:54:29 +0200 Subject: [PATCH 074/120] [Osquery] Add Osquery to Alert context menu (#131790) --- .../cypress/integration/all/alerts.spec.ts | 11 ++++-- .../common/ecs/agent/index.ts | 1 + .../timeline_actions/alert_context_menu.tsx | 38 ++++++++++++++++--- .../osquery/osquery_action_item.tsx | 20 +++++----- .../use_osquery_context_action_item.tsx | 27 +++++++++++++ .../timeline/eql/helpers.test.ts | 12 ++++++ .../timeline/factory/helpers/constants.ts | 1 + 7 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/osquery/use_osquery_context_action_item.tsx diff --git a/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts b/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts index 21d3584b9fc46..4ef3e263df01c 100644 --- a/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts @@ -34,10 +34,9 @@ describe('Alert Event Details', () => { runKbnArchiverScript(ArchiverMethod.UNLOAD, 'rule'); }); - it('should be able to run live query', () => { + it('should prepare packs and alert rules', () => { const PACK_NAME = 'testpack'; const RULE_NAME = 'Test-rule'; - const TIMELINE_NAME = 'Untitled timeline'; navigateTo('/app/osquery/packs'); preparePack(PACK_NAME); findAndClickButton('Edit'); @@ -57,8 +56,14 @@ describe('Alert Event Details', () => { cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'false'); cy.getBySel('ruleSwitch').click(); cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true'); + }); + + it('should be able to run live query and add to timeline (-depending on the previous test)', () => { + const TIMELINE_NAME = 'Untitled timeline'; cy.visit('/app/security/alerts'); - cy.wait(500); + cy.getBySel('header-page-title').contains('Alerts').should('exist'); + cy.getBySel('timeline-context-menu-button').first().click({ force: true }); + cy.getBySel('osquery-action-item').should('exist').contains('Run Osquery'); cy.getBySel('expand-event').first().click(); cy.getBySel('take-action-dropdown-btn').click(); cy.getBySel('osquery-action-item').click(); diff --git a/x-pack/plugins/security_solution/common/ecs/agent/index.ts b/x-pack/plugins/security_solution/common/ecs/agent/index.ts index 2332b60f1a3ca..7084214a9b876 100644 --- a/x-pack/plugins/security_solution/common/ecs/agent/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/agent/index.ts @@ -7,4 +7,5 @@ export interface AgentEcs { type?: string[]; + id?: string[]; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 05a91f094ed38..efc4666b7bd61 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -13,6 +13,8 @@ import { connect, ConnectedProps } from 'react-redux'; import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { get } from 'lodash/fp'; import { DEFAULT_ACTION_BUTTON_WIDTH } from '@kbn/timelines-plugin/public'; +import { useOsqueryContextActionItem } from '../../osquery/use_osquery_context_action_item'; +import { OsqueryFlyout } from '../../osquery/osquery_flyout'; import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; @@ -63,6 +65,7 @@ const AlertContextMenuComponent: React.FC { const [isPopoverOpen, setPopover] = useState(false); + const [isOsqueryFlyoutOpen, setOsqueryFlyoutOpen] = useState(false); const [routeProps] = useRouteSpy(); const onMenuItemClick = useCallback(() => { @@ -186,18 +189,38 @@ const AlertContextMenuComponent: React.FC get(0, ecsRowData?.agent?.id), [ecsRowData]); + + const handleOnOsqueryClick = useCallback(() => { + setOsqueryFlyoutOpen((prevValue) => !prevValue); + setPopover(false); + }, []); + + const { osqueryActionItems } = useOsqueryContextActionItem({ handleClick: handleOnOsqueryClick }); + const items: React.ReactElement[] = useMemo( () => !isEvent && ruleId - ? [...addToCaseActionItems, ...statusActionItems, ...exceptionActionItems] - : [...addToCaseActionItems, ...eventFilterActionItems], + ? [ + ...addToCaseActionItems, + ...statusActionItems, + ...exceptionActionItems, + ...(agentId ? osqueryActionItems : []), + ] + : [ + ...addToCaseActionItems, + ...eventFilterActionItems, + ...(agentId ? osqueryActionItems : []), + ], [ - statusActionItems, - addToCaseActionItems, - eventFilterActionItems, - exceptionActionItems, isEvent, ruleId, + addToCaseActionItems, + statusActionItems, + exceptionActionItems, + agentId, + osqueryActionItems, + eventFilterActionItems, ] ); @@ -239,6 +262,9 @@ const AlertContextMenuComponent: React.FC )} + {isOsqueryFlyoutOpen && agentId && ecsRowData != null && ( + + )} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx index ca61e2f3ebf6d..e27a13ef217e3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx @@ -13,14 +13,12 @@ interface IProps { handleClick: () => void; } -export const OsqueryActionItem = ({ handleClick }: IProps) => { - return ( - - {ACTION_OSQUERY} - - ); -}; +export const OsqueryActionItem = ({ handleClick }: IProps) => ( + + {ACTION_OSQUERY} + +); diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/use_osquery_context_action_item.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/use_osquery_context_action_item.tsx new file mode 100644 index 0000000000000..41a78eb32619f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/use_osquery_context_action_item.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { OsqueryActionItem } from './osquery_action_item'; +import { useKibana } from '../../../common/lib/kibana'; + +interface IProps { + handleClick: () => void; +} + +export const useOsqueryContextActionItem = ({ handleClick }: IProps) => { + const osqueryActionItem = useMemo( + () => , + [handleClick] + ); + const permissions = useKibana().services.application.capabilities.osquery; + + return { + osqueryActionItems: + permissions?.writeLiveQueries || permissions?.runSavedQueries ? [osqueryActionItem] : [], + }; +}; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.test.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.test.ts index 5c6a0ac0bd416..10a4fae0a036d 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.test.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.test.ts @@ -208,6 +208,9 @@ describe('Search Strategy EQL helper', () => { "_id": "qhymg3cBX5UUcOOYP3Ec", "_index": ".ds-logs-endpoint.events.security-default-2021.02.05-000005", "agent": Object { + "id": Array [ + "1d15cf9e-3dc7-5b97-f586-743f7c2518b2", + ], "type": Array [ "endpoint", ], @@ -335,6 +338,9 @@ describe('Search Strategy EQL helper', () => { "_id": "qxymg3cBX5UUcOOYP3Ec", "_index": ".ds-logs-endpoint.events.security-default-2021.02.05-000005", "agent": Object { + "id": Array [ + "1d15cf9e-3dc7-5b97-f586-743f7c2518b2", + ], "type": Array [ "endpoint", ], @@ -476,6 +482,9 @@ describe('Search Strategy EQL helper', () => { "_id": "rBymg3cBX5UUcOOYP3Ec", "_index": ".ds-logs-endpoint.events.security-default-2021.02.05-000005", "agent": Object { + "id": Array [ + "1d15cf9e-3dc7-5b97-f586-743f7c2518b2", + ], "type": Array [ "endpoint", ], @@ -592,6 +601,9 @@ describe('Search Strategy EQL helper', () => { "_id": "pxymg3cBX5UUcOOYP3Ec", "_index": ".ds-logs-endpoint.events.process-default-2021.02.02-000005", "agent": Object { + "id": Array [ + "1d15cf9e-3dc7-5b97-f586-743f7c2518b2", + ], "type": Array [ "endpoint", ], diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts index 211edec96b8ac..068b52b8cd821 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts @@ -94,6 +94,7 @@ export const TIMELINE_EVENTS_FIELDS = [ 'event.timezone', 'event.type', 'agent.type', + 'agent.id', 'auditd.result', 'auditd.session', 'auditd.data.acct', From b59fb972c65c485c3f2811ae415d43a2bb5951f7 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Mon, 23 May 2022 12:02:43 +0200 Subject: [PATCH 075/120] [Security Solution] Update use_url_state to work with new side nav (#132518) * Fix landing pages browser tab title * Fix new navigation url state * Fix unit tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/url_state/index.test.tsx | 69 +++++++++++ .../url_state/index_mocked.test.tsx | 111 +++++++++++++++++- .../components/url_state/use_url_state.tsx | 23 +++- 3 files changed, 197 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index faf3fe10da079..cb49215ee8c9c 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -25,6 +25,11 @@ import { UrlStateContainerPropTypes } from './types'; import { useUrlStateHooks } from './use_url_state'; import { waitFor } from '@testing-library/react'; import { useLocation } from 'react-router-dom'; +import { updateAppLinks } from '../../links'; +import { getAppLinks } from '../../links/app_links'; +import { StartPlugins } from '../../../types'; +import { allowedExperimentalValues } from '../../../../common/experimental_features'; +import { coreMock } from '@kbn/core/public/mocks'; let mockProps: UrlStateContainerPropTypes; @@ -78,10 +83,36 @@ jest.mock('react-router-dom', () => { }; }); +const mockedUseIsGroupedNavigationEnabled = jest.fn(); +jest.mock('../navigation/helpers', () => ({ + useIsGroupedNavigationEnabled: () => mockedUseIsGroupedNavigationEnabled(), +})); + describe('UrlStateContainer', () => { + beforeAll(async () => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); + + const appLinks = await getAppLinks(coreMock.createStart(), {} as StartPlugins); + updateAppLinks(appLinks, { + experimentalFeatures: allowedExperimentalValues, + capabilities: { + navLinks: {}, + management: {}, + catalogue: {}, + actions: { show: true, crud: true }, + siem: { + show: true, + crud: true, + }, + }, + }); + }); + afterEach(() => { jest.clearAllMocks(); + mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); }); + describe('handleInitialize', () => { describe('URL state updates redux', () => { describe('relative timerange actions are called with correct data on component mount', () => { @@ -226,6 +257,44 @@ describe('UrlStateContainer', () => { expect(mockHistory.replace).not.toHaveBeenCalled(); }); + it("it doesn't update URL state when on admin page and grouped nav disabled", () => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); + mockProps = getMockPropsObj({ + page: CONSTANTS.unknown, + examplePath: '/administration', + namespaceLower: 'administration', + pageName: SecurityPageName.administration, + detailName: undefined, + }).noSearch.undefinedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + + mount( useUrlStateHooks(args)} />); + + expect(mockHistory.replace.mock.calls[0][0].search).toBe('?'); + }); + + it("it doesn't update URL state when on admin page and grouped nav enabled", () => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(true); + mockProps = getMockPropsObj({ + page: CONSTANTS.unknown, + examplePath: '/dashboards', + namespaceLower: 'dashboards', + pageName: SecurityPageName.dashboardsLanding, + detailName: undefined, + }).noSearch.undefinedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + + mount( useUrlStateHooks(args)} />); + + expect(mockHistory.replace.mock.calls[0][0].search).toBe('?'); + }); + it('it removes empty AppQuery state from URL', () => { mockProps = { ...getMockProps( diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx index 4063ecdb73935..011621b95a0c4 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx @@ -16,7 +16,12 @@ import { getFilterQuery, getMockPropsObj, mockHistory, testCases } from './test_ import { UrlStateContainerPropTypes } from './types'; import { useUrlStateHooks } from './use_url_state'; import { useLocation } from 'react-router-dom'; -import { MANAGEMENT_PATH } from '../../../../common/constants'; +import { DASHBOARDS_PATH, MANAGEMENT_PATH } from '../../../../common/constants'; +import { getAppLinks } from '../../links/app_links'; +import { StartPlugins } from '../../../types'; +import { updateAppLinks } from '../../links'; +import { allowedExperimentalValues } from '../../../../common/experimental_features'; +import { coreMock } from '@kbn/core/public/mocks'; let mockProps: UrlStateContainerPropTypes; @@ -45,7 +50,31 @@ jest.mock('react-redux', () => { }; }); +const mockedUseIsGroupedNavigationEnabled = jest.fn(); +jest.mock('../navigation/helpers', () => ({ + useIsGroupedNavigationEnabled: () => mockedUseIsGroupedNavigationEnabled(), +})); + describe('UrlStateContainer - lodash.throttle mocked to test update url', () => { + beforeAll(async () => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); + + const appLinks = await getAppLinks(coreMock.createStart(), {} as StartPlugins); + updateAppLinks(appLinks, { + experimentalFeatures: allowedExperimentalValues, + capabilities: { + navLinks: {}, + management: {}, + catalogue: {}, + actions: { show: true, crud: true }, + siem: { + show: true, + crud: true, + }, + }, + }); + }); + afterEach(() => { jest.clearAllMocks(); jest.resetAllMocks(); @@ -210,7 +239,8 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => }); }); - test("administration page doesn't has query string", () => { + test("administration page doesn't has query string when grouped nav disabled", () => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); mockProps = getMockPropsObj({ page: CONSTANTS.networkPage, examplePath: '/network', @@ -285,6 +315,83 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => state: '', }); }); + + test("dashboards page doesn't has query string when grouped nav enabled", () => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(true); + mockProps = getMockPropsObj({ + page: CONSTANTS.networkPage, + examplePath: '/network', + namespaceLower: 'network', + pageName: SecurityPageName.network, + detailName: undefined, + }).noSearch.definedQuery; + + const urlState = { + ...mockProps.urlState, + [CONSTANTS.appQuery]: getFilterQuery(), + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: { + from: '2020-07-07T08:20:18.966Z', + fromStr: 'now-24h', + kind: 'relative', + to: '2020-07-08T08:20:18.966Z', + toStr: 'now', + }, + linkTo: ['timeline'], + }, + timeline: { + [CONSTANTS.timerange]: { + from: '2020-07-07T08:20:18.966Z', + fromStr: 'now-24h', + kind: 'relative', + to: '2020-07-08T08:20:18.966Z', + toStr: 'now', + }, + linkTo: ['global'], + }, + }, + }; + + const updatedMockProps = { + ...getMockPropsObj({ + ...mockProps, + page: CONSTANTS.unknown, + examplePath: DASHBOARDS_PATH, + namespaceLower: 'dashboards', + pageName: SecurityPageName.dashboardsLanding, + detailName: undefined, + }).noSearch.definedQuery, + urlState, + }; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + + const wrapper = mount( + useUrlStateHooks(args)} + /> + ); + + (useLocation as jest.Mock).mockReturnValue({ + pathname: updatedMockProps.pathName, + }); + + wrapper.setProps({ + hookProps: updatedMockProps, + }); + + wrapper.update(); + expect(mockHistory.replace.mock.calls[1][0]).toStrictEqual({ + hash: '', + pathname: DASHBOARDS_PATH, + search: '?', + state: '', + }); + }); }); describe('handleInitialize', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx index 3245d647227ad..e787b3a750e91 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx @@ -40,6 +40,9 @@ import { import { TimelineUrl } from '../../../timelines/store/timeline/model'; import { UrlInputsModel } from '../../store/inputs/model'; import { queryTimelineByIdOnUrlChange } from './query_timeline_by_id_on_url_change'; +import { getLinkInfo } from '../../links'; +import { SecurityPageName } from '../../../app/types'; +import { useIsGroupedNavigationEnabled } from '../navigation/helpers'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef(value); @@ -62,7 +65,9 @@ export const useUrlStateHooks = ({ const { filterManager, savedQueries } = useKibana().services.data.query; const { pathname: browserPathName } = useLocation(); const prevProps = usePrevious({ pathName, pageName, urlState, search }); + const isGroupedNavEnabled = useIsGroupedNavigationEnabled(); + const linkInfo = pageName ? getLinkInfo(pageName as SecurityPageName) : undefined; const { setInitialStateFromUrl, updateTimeline, updateTimelineIsLoading } = useSetInitialStateFromUrl(); @@ -70,9 +75,10 @@ export const useUrlStateHooks = ({ (type: UrlStateType) => { const urlStateUpdatesToStore: UrlStateToRedux[] = []; const urlStateUpdatesToLocation: ReplaceStateInLocation[] = []; + const skipUrlState = isGroupedNavEnabled ? linkInfo?.skipUrlState : isAdministration(type); // Delete all query strings from URL when the page is security/administration (Manage menu group) - if (isAdministration(type)) { + if (skipUrlState) { ALL_URL_STATE_KEYS.forEach((urlKey: KeyUrlState) => { urlStateUpdatesToLocation.push({ urlStateToReplace: '', @@ -146,6 +152,8 @@ export const useUrlStateHooks = ({ setInitialStateFromUrl, urlState, isFirstPageLoad, + isGroupedNavEnabled, + linkInfo?.skipUrlState, ] ); @@ -159,8 +167,9 @@ export const useUrlStateHooks = ({ if (browserPathName !== pathName) return; const type: UrlStateType = getUrlType(pageName); + const skipUrlState = isGroupedNavEnabled ? linkInfo?.skipUrlState : isAdministration(type); - if (!deepEqual(urlState, prevProps.urlState) && !isFirstPageLoad && !isAdministration(type)) { + if (!deepEqual(urlState, prevProps.urlState) && !isFirstPageLoad && !skipUrlState) { const urlStateUpdatesToLocation: ReplaceStateInLocation[] = ALL_URL_STATE_KEYS.map( (urlKey: KeyUrlState) => ({ urlStateToReplace: getUrlStateKeyValue(urlState, urlKey), @@ -186,11 +195,17 @@ export const useUrlStateHooks = ({ browserPathName, handleInitialize, search, + isGroupedNavEnabled, + linkInfo?.skipUrlState, ]); useEffect(() => { - document.title = `${getTitle(pageName, navTabs)} - Kibana`; - }, [pageName, navTabs]); + if (!isGroupedNavEnabled) { + document.title = `${getTitle(pageName, navTabs)} - Kibana`; + } else { + document.title = `${linkInfo?.title ?? ''} - Kibana`; + } + }, [pageName, navTabs, isGroupedNavEnabled, linkInfo]); useEffect(() => { queryTimelineByIdOnUrlChange({ From c993ff2a4fa10898d5a6e15aeb1d0848534ae48e Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Mon, 23 May 2022 06:25:17 -0400 Subject: [PATCH 076/120] [Workplace Search] Add categories to source data for internal connectors (#132671) --- .../workplace_search/constants.ts | 101 +++++++++++++++++- .../views/content_sources/source_data.tsx | 98 ++++++++++++++++- 2 files changed, 192 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 1ffb6c74d25fa..9e39b86242a90 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -366,21 +366,90 @@ export const SOURCE_OBJ_TYPES = { }; export const SOURCE_CATEGORIES = { + ACCOUNT_MANAGEMENT: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.accountManagement', + { + defaultMessage: 'Account management', + } + ), + ATLASSIAN: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.atlassian', { + defaultMessage: 'Atlassian', + }), + BUG_TRACKING: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.bugTracking', + { + defaultMessage: 'Bug tracking', + } + ), + CHAT: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.chat', { + defaultMessage: 'Chat', + }), CLOUD: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.cloud', { defaultMessage: 'Cloud', }), - COMMUNICATIONS: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.categories.communications', + CODE_REPOSITORY: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.codeRepository', + { + defaultMessage: 'Code repository', + } + ), + COLLABORATION: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.collaboration', + { + defaultMessage: 'Collaboration', + } + ), + COMMUNICATION: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.communication', + { + defaultMessage: 'Communication', + } + ), + CRM: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.crm', { + defaultMessage: 'CRM', + }), + CUSTOMER_RELATIONSHIP_MANAGEMENT: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.customerRelationshipManagement', + { + defaultMessage: 'Customer relationship management', + } + ), + CUSTOMER_SERVICE: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.customerService', { - defaultMessage: 'Communications', + defaultMessage: 'Customer service', } ), + EMAIL: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.email', { + defaultMessage: 'Email', + }), FILE_SHARING: i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.categories.fileSharing', { - defaultMessage: 'File Sharing', + defaultMessage: 'File sharing', + } + ), + GOOGLE: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.google', { + defaultMessage: 'Google', + }), + GSUITE: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.gsuite', { + defaultMessage: 'GSuite', + }), + HELP: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.help', { + defaultMessage: 'Help', + }), + HELPDESK: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.helpdesk', { + defaultMessage: 'Helpdesk', + }), + INSTANT_MESSAGING: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.instantMessaging', + { + defaultMessage: 'Instant messaging', } ), + INTRANET: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.intranet', { + defaultMessage: 'Intranet', + }), MICROSOFT: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.microsoft', { defaultMessage: 'Microsoft', }), @@ -393,9 +462,33 @@ export const SOURCE_CATEGORIES = { defaultMessage: 'Productivity', } ), + PROJECT_MANAGEMENT: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.projectManagement', + { + defaultMessage: 'Project management', + } + ), + SOFTWARE: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.software', { + defaultMessage: 'Software', + }), STORAGE: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.storage', { defaultMessage: 'Storage', }), + TICKETING: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.ticketing', { + defaultMessage: 'Ticketing', + }), + VERSION_CONTROL: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.versionControl', + { + defaultMessage: 'Version control', + } + ), + WIKI: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.wiki', { + defaultMessage: 'Wiki', + }), + WORKFLOW: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.workflow', { + defaultMessage: 'Workflow', + }), }; export const API_KEYS_TITLE = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index 181cd8b7c9a73..6188c37b20057 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -42,6 +42,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.BOX, serviceType: 'box', + categories: [ + SOURCE_CATEGORIES.FILE_SHARING, + SOURCE_CATEGORIES.STORAGE, + SOURCE_CATEGORIES.CLOUD, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -69,6 +74,7 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.CONFLUENCE, serviceType: 'confluence_cloud', + categories: [SOURCE_CATEGORIES.WIKI, SOURCE_CATEGORIES.ATLASSIAN, SOURCE_CATEGORIES.INTRANET], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -103,6 +109,7 @@ export const staticSourceData: SourceDataItem[] = [ name: SOURCE_NAMES.CONFLUENCE_CONNECTOR_PACKAGE, serviceType: 'external', baseServiceType: 'confluence_cloud', + categories: [SOURCE_CATEGORIES.WIKI, SOURCE_CATEGORIES.ATLASSIAN, SOURCE_CATEGORIES.INTRANET], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -136,6 +143,7 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.CONFLUENCE_SERVER, serviceType: 'confluence_server', + categories: [SOURCE_CATEGORIES.WIKI, SOURCE_CATEGORIES.ATLASSIAN, SOURCE_CATEGORIES.INTRANET], configuration: { isPublicKey: true, hasOauthRedirect: true, @@ -166,6 +174,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.DROPBOX, serviceType: 'dropbox', + categories: [ + SOURCE_CATEGORIES.FILE_SHARING, + SOURCE_CATEGORIES.STORAGE, + SOURCE_CATEGORIES.CLOUD, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -193,6 +206,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.GITHUB, serviceType: 'github', + categories: [ + SOURCE_CATEGORIES.SOFTWARE, + SOURCE_CATEGORIES.VERSION_CONTROL, + SOURCE_CATEGORIES.CODE_REPOSITORY, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -227,6 +245,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.GITHUB_ENTERPRISE, serviceType: 'github_enterprise_server', + categories: [ + SOURCE_CATEGORIES.SOFTWARE, + SOURCE_CATEGORIES.VERSION_CONTROL, + SOURCE_CATEGORIES.CODE_REPOSITORY, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -267,6 +290,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.GMAIL, serviceType: 'gmail', + categories: [ + SOURCE_CATEGORIES.COMMUNICATION, + SOURCE_CATEGORIES.EMAIL, + SOURCE_CATEGORIES.GOOGLE, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -283,6 +311,13 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.GOOGLE_DRIVE, serviceType: 'google_drive', + categories: [ + SOURCE_CATEGORIES.FILE_SHARING, + SOURCE_CATEGORIES.STORAGE, + SOURCE_CATEGORIES.CLOUD, + SOURCE_CATEGORIES.PRODUCTIVITY, + SOURCE_CATEGORIES.GSUITE, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -314,6 +349,12 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.JIRA, serviceType: 'jira_cloud', + categories: [ + SOURCE_CATEGORIES.SOFTWARE, + SOURCE_CATEGORIES.BUG_TRACKING, + SOURCE_CATEGORIES.ATLASSIAN, + SOURCE_CATEGORIES.PROJECT_MANAGEMENT, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -348,6 +389,12 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.JIRA_SERVER, serviceType: 'jira_server', + categories: [ + SOURCE_CATEGORIES.SOFTWARE, + SOURCE_CATEGORIES.BUG_TRACKING, + SOURCE_CATEGORIES.ATLASSIAN, + SOURCE_CATEGORIES.PROJECT_MANAGEMENT, + ], configuration: { isPublicKey: true, hasOauthRedirect: true, @@ -396,6 +443,13 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.ONEDRIVE, serviceType: 'one_drive', + categories: [ + SOURCE_CATEGORIES.FILE_SHARING, + SOURCE_CATEGORIES.CLOUD, + SOURCE_CATEGORIES.STORAGE, + SOURCE_CATEGORIES.MICROSOFT, + SOURCE_CATEGORIES.OFFICE_365, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -423,7 +477,7 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.OUTLOOK, categories: [ - SOURCE_CATEGORIES.COMMUNICATIONS, + SOURCE_CATEGORIES.COMMUNICATION, SOURCE_CATEGORIES.PRODUCTIVITY, SOURCE_CATEGORIES.MICROSOFT, ], @@ -442,6 +496,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.SALESFORCE, serviceType: 'salesforce', + categories: [ + SOURCE_CATEGORIES.CRM, + SOURCE_CATEGORIES.CUSTOMER_RELATIONSHIP_MANAGEMENT, + SOURCE_CATEGORIES.ACCOUNT_MANAGEMENT, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -476,6 +535,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.SALESFORCE_SANDBOX, serviceType: 'salesforce_sandbox', + categories: [ + SOURCE_CATEGORIES.CRM, + SOURCE_CATEGORIES.CUSTOMER_RELATIONSHIP_MANAGEMENT, + SOURCE_CATEGORIES.ACCOUNT_MANAGEMENT, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -510,6 +574,7 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.SERVICENOW, serviceType: 'service_now', + categories: [SOURCE_CATEGORIES.WORKFLOW], configuration: { isPublicKey: false, hasOauthRedirect: false, @@ -542,6 +607,13 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.SHAREPOINT, serviceType: 'share_point', + categories: [ + SOURCE_CATEGORIES.FILE_SHARING, + SOURCE_CATEGORIES.STORAGE, + SOURCE_CATEGORIES.CLOUD, + SOURCE_CATEGORIES.MICROSOFT, + SOURCE_CATEGORIES.OFFICE_365, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -570,6 +642,13 @@ export const staticSourceData: SourceDataItem[] = [ name: SOURCE_NAMES.SHAREPOINT_CONNECTOR_PACKAGE, serviceType: 'external', baseServiceType: 'share_point', + categories: [ + SOURCE_CATEGORIES.FILE_SHARING, + SOURCE_CATEGORIES.STORAGE, + SOURCE_CATEGORIES.CLOUD, + SOURCE_CATEGORIES.MICROSOFT, + SOURCE_CATEGORIES.OFFICE_365, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -619,6 +698,12 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.SLACK, serviceType: 'slack', + categories: [ + SOURCE_CATEGORIES.COLLABORATION, + SOURCE_CATEGORIES.COMMUNICATION, + SOURCE_CATEGORIES.INSTANT_MESSAGING, + SOURCE_CATEGORIES.CHAT, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -639,7 +724,7 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.TEAMS, categories: [ - SOURCE_CATEGORIES.COMMUNICATIONS, + SOURCE_CATEGORIES.COMMUNICATION, SOURCE_CATEGORIES.PRODUCTIVITY, SOURCE_CATEGORIES.MICROSOFT, ], @@ -658,6 +743,13 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.ZENDESK, serviceType: 'zendesk', + categories: [ + SOURCE_CATEGORIES.HELP, + SOURCE_CATEGORIES.CUSTOMER_SERVICE, + SOURCE_CATEGORIES.CUSTOMER_RELATIONSHIP_MANAGEMENT, + SOURCE_CATEGORIES.TICKETING, + SOURCE_CATEGORIES.HELPDESK, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -684,7 +776,7 @@ export const staticSourceData: SourceDataItem[] = [ }, { name: SOURCE_NAMES.ZOOM, - categories: [SOURCE_CATEGORIES.COMMUNICATIONS, SOURCE_CATEGORIES.PRODUCTIVITY], + categories: [SOURCE_CATEGORIES.COMMUNICATION, SOURCE_CATEGORIES.PRODUCTIVITY], serviceType: 'custom', baseServiceType: 'zoom', configuration: { From 6b846af084b60d0eba3bfcda3ee754b8584b4cfa Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Mon, 23 May 2022 14:11:04 +0300 Subject: [PATCH 077/120] [Actionable Observability] Update the Rule details design and clean up (#132616) * Add rule status in the rule summary * Match design * Remove unused imports * code review --- .../rule_details/components/page_title.tsx | 13 +++- .../public/pages/rule_details/index.tsx | 68 ++++++++----------- 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx index 478fbf69a226c..d75be330df548 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx @@ -6,11 +6,12 @@ */ import React, { useState } from 'react'; import moment from 'moment'; -import { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiText, EuiFlexGroup, EuiFlexItem, EuiBadge, EuiSpacer } from '@elastic/eui'; import { ExperimentalBadge } from '../../../components/shared/experimental_badge'; import { PageHeaderProps } from '../types'; import { useKibana } from '../../../utils/kibana_react'; import { LAST_UPDATED_MESSAGE, CREATED_WORD, BY_WORD, ON_WORD } from '../translations'; +import { getHealthColor } from '../../rules/config'; export function PageTitle({ rule }: PageHeaderProps) { const { triggersActionsUi } = useKibana().services; @@ -23,6 +24,16 @@ export function PageTitle({ rule }: PageHeaderProps) { return ( <> {rule.name} + + + + + {rule.executionStatus.status.charAt(0).toUpperCase() + + rule.executionStatus.status.slice(1)} + + + + diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx index 5cc12452e57e1..9ca155ab7ef25 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/index.tsx @@ -18,7 +18,6 @@ import { EuiButtonIcon, EuiPanel, EuiTitle, - EuiHealth, EuiPopover, EuiHorizontalRule, EuiTabbedContent, @@ -42,13 +41,8 @@ import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; import { DeleteModalConfirmation } from '../rules/components/delete_modal_confirmation'; import { CenterJustifiedSpinner } from '../rules/components/center_justified_spinner'; -import { getHealthColor, OBSERVABILITY_SOLUTIONS } from '../rules/config'; -import { - RuleDetailsPathParams, - EVENT_ERROR_LOG_TAB, - EVENT_LOG_LIST_TAB, - ALERT_LIST_TAB, -} from './types'; +import { OBSERVABILITY_SOLUTIONS } from '../rules/config'; +import { RuleDetailsPathParams, EVENT_LOG_LIST_TAB, ALERT_LIST_TAB } from './types'; import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useFetchRule } from '../../hooks/use_fetch_rule'; @@ -188,14 +182,6 @@ export function RuleDetailsPage() { 'data-test-subj': 'ruleAlertListTab', content: Alerts, }, - { - id: EVENT_ERROR_LOG_TAB, - name: i18n.translate('xpack.observability.ruleDetails.rule.errorLogTabText', { - defaultMessage: 'Error log', - }), - 'data-test-subj': 'errorLogTab', - content: Error log, - }, ]; if (isPageLoading || isRuleLoading) return ; @@ -222,6 +208,20 @@ export function RuleDetailsPage() { /> ); + + const getRuleStatusComponent = () => + getRuleStatusDropdown({ + rule, + enableRule: async () => await enableRule({ http, id: rule.id }), + disableRule: async () => await disableRule({ http, id: rule.id }), + onRuleChanged: () => reloadRule(), + isEditable: hasEditButton, + snoozeRule: async (snoozeEndTime: string | -1) => { + await snoozeRule({ http, id: rule.id, snoozeEndTime }); + }, + unsnoozeRule: async () => await unsnoozeRule({ http, id: rule.id }), + }); + const getNotifyText = () => NOTIFY_WHEN_OPTIONS.find((option) => option.value === rule?.notifyWhen)?.inputDisplay || rule.notifyWhen; @@ -284,17 +284,7 @@ export function RuleDetailsPage() { - {getRuleStatusDropdown({ - rule, - enableRule: async () => await enableRule({ http, id: rule.id }), - disableRule: async () => await disableRule({ http, id: rule.id }), - onRuleChanged: () => reloadRule(), - isEditable: hasEditButton, - snoozeRule: async (snoozeEndTime: string | -1) => { - await snoozeRule({ http, id: rule.id, snoozeEndTime }); - }, - unsnoozeRule: async () => await unsnoozeRule({ http, id: rule.id }), - })} + {getRuleStatusComponent()} , ] @@ -304,21 +294,8 @@ export function RuleDetailsPage() { {/* Left side of Rule Summary */} - + - - - - {rule.executionStatus.status.charAt(0).toUpperCase() + - rule.executionStatus.status.slice(1)} - - - - {i18n.translate('xpack.observability.ruleDetails.lastRun', { @@ -330,6 +307,15 @@ export function RuleDetailsPage() { itemValue={moment(rule.executionStatus.lastExecutionDate).fromNow()} /> + + + + {i18n.translate('xpack.observability.ruleDetails.ruleIs', { + defaultMessage: 'Rule is', + })} + + {getRuleStatusComponent()} + From ba84602455671f0f6175bbc0fd2e8f302c60bbe6 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Mon, 23 May 2022 13:33:20 +0200 Subject: [PATCH 078/120] [Osquery] Change prebuilt saved queries to include prebuilt flag (#132651) --- .../routes/saved_queries/edit/index.tsx | 2 +- .../saved_query/delete_saved_query_route.ts | 9 +++- .../saved_query/find_saved_query_route.ts | 9 +++- .../server/routes/saved_query/index.ts | 6 +-- .../saved_query/read_saved_query_route.ts | 7 ++- .../saved_query/update_saved_query_route.ts | 7 +++ .../server/routes/saved_query/utils.ts | 54 +++++++++++++++++++ 7 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/osquery/server/routes/saved_query/utils.ts diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx index 94b1f092e1ede..cb7a95b4271e7 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx @@ -44,7 +44,7 @@ const EditSavedQueryPageComponent = () => { useBreadcrumbs('saved_query_edit', { savedQueryName: savedQueryDetails?.attributes?.id ?? '' }); const elasticPrebuiltQuery = useMemo( - () => savedQueryDetails?.attributes?.version, + () => savedQueryDetails?.attributes?.prebuilt, [savedQueryDetails] ); const viewMode = useMemo( diff --git a/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts index c2a2ad7fa8619..a27c4a0953098 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts @@ -9,8 +9,10 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; import { PLUGIN_ID } from '../../../common'; import { savedQuerySavedObjectType } from '../../../common/types'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; +import { isSavedQueryPrebuilt } from './utils'; -export const deleteSavedQueryRoute = (router: IRouter) => { +export const deleteSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.delete( { path: '/internal/osquery/saved_query/{id}', @@ -25,6 +27,11 @@ export const deleteSavedQueryRoute = (router: IRouter) => { const coreContext = await context.core; const savedObjectsClient = coreContext.savedObjects.client; + const isPrebuilt = await isSavedQueryPrebuilt(osqueryContext, request.params.id); + if (isPrebuilt) { + return response.conflict({ body: `Elastic prebuilt Saved query cannot be deleted.` }); + } + await savedObjectsClient.delete(savedQuerySavedObjectType, request.params.id, { refresh: 'wait_for', }); diff --git a/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts index a2b85dbf539d9..abf62ca782daa 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts @@ -7,11 +7,14 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; + +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; import { PLUGIN_ID } from '../../../common'; import { savedQuerySavedObjectType } from '../../../common/types'; import { convertECSMappingToObject } from '../utils'; +import { getInstalledSavedQueriesMap } from './utils'; -export const findSavedQueryRoute = (router: IRouter) => { +export const findSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.get( { path: '/internal/osquery/saved_query', @@ -34,6 +37,7 @@ export const findSavedQueryRoute = (router: IRouter) => { const savedQueries = await savedObjectsClient.find<{ ecs_mapping: Array<{ field: string; value: string }>; + prebuilt: boolean; }>({ type: savedQuerySavedObjectType, page: parseInt(request.query.pageIndex ?? '0', 10) + 1, @@ -43,10 +47,13 @@ export const findSavedQueryRoute = (router: IRouter) => { sortOrder: request.query.sortDirection ?? 'desc', }); + const prebuiltSavedQueriesMap = await getInstalledSavedQueriesMap(osqueryContext); const savedObjects = savedQueries.saved_objects.map((savedObject) => { // eslint-disable-next-line @typescript-eslint/naming-convention const ecs_mapping = savedObject.attributes.ecs_mapping; + savedObject.attributes.prebuilt = !!prebuiltSavedQueriesMap[savedObject.id]; + if (ecs_mapping) { // @ts-expect-error update types savedObject.attributes.ecs_mapping = convertECSMappingToObject(ecs_mapping); diff --git a/x-pack/plugins/osquery/server/routes/saved_query/index.ts b/x-pack/plugins/osquery/server/routes/saved_query/index.ts index e0bf4f622c42c..025199dcba6b6 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/index.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/index.ts @@ -16,8 +16,8 @@ import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; export const initSavedQueryRoutes = (router: IRouter, context: OsqueryAppContext) => { createSavedQueryRoute(router, context); - deleteSavedQueryRoute(router); - findSavedQueryRoute(router); - readSavedQueryRoute(router); + deleteSavedQueryRoute(router, context); + findSavedQueryRoute(router, context); + readSavedQueryRoute(router, context); updateSavedQueryRoute(router, context); }; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts index 1c206464d1f65..d1627d220682a 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts @@ -7,11 +7,13 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; +import { isSavedQueryPrebuilt } from './utils'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; import { PLUGIN_ID } from '../../../common'; import { savedQuerySavedObjectType } from '../../../common/types'; import { convertECSMappingToObject } from '../utils'; -export const readSavedQueryRoute = (router: IRouter) => { +export const readSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.get( { path: '/internal/osquery/saved_query/{id}', @@ -28,6 +30,7 @@ export const readSavedQueryRoute = (router: IRouter) => { const savedQuery = await savedObjectsClient.get<{ ecs_mapping: Array<{ key: string; value: Record }>; + prebuilt: boolean; }>(savedQuerySavedObjectType, request.params.id); if (savedQuery.attributes.ecs_mapping) { @@ -37,6 +40,8 @@ export const readSavedQueryRoute = (router: IRouter) => { ); } + savedQuery.attributes.prebuilt = await isSavedQueryPrebuilt(osqueryContext, savedQuery.id); + return response.ok({ body: savedQuery, }); diff --git a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts index 1d2bf153afd7f..e2686868b7eff 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts @@ -9,6 +9,7 @@ import { filter } from 'lodash'; import { schema } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; +import { isSavedQueryPrebuilt } from './utils'; import { PLUGIN_ID } from '../../../common'; import { savedQuerySavedObjectType } from '../../../common/types'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; @@ -63,6 +64,12 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp ecs_mapping, } = request.body; + const isPrebuilt = await isSavedQueryPrebuilt(osqueryContext, request.params.id); + + if (isPrebuilt) { + return response.conflict({ body: `Elastic prebuilt Saved query cannot be updated.` }); + } + const conflictingEntries = await savedObjectsClient.find<{ id: string }>({ type: savedQuerySavedObjectType, filter: `${savedQuerySavedObjectType}.attributes.id: "${id}"`, diff --git a/x-pack/plugins/osquery/server/routes/saved_query/utils.ts b/x-pack/plugins/osquery/server/routes/saved_query/utils.ts new file mode 100644 index 0000000000000..d99d5b70f0dab --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/saved_query/utils.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { find, reduce } from 'lodash'; +import { KibanaAssetReference } from '@kbn/fleet-plugin/common'; + +import { OSQUERY_INTEGRATION_NAME } from '../../../common'; +import { savedQuerySavedObjectType } from '../../../common/types'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +const getInstallation = async (osqueryContext: OsqueryAppContext) => + await osqueryContext.service + .getPackageService() + ?.asInternalUser?.getInstallation(OSQUERY_INTEGRATION_NAME); + +export const getInstalledSavedQueriesMap = async (osqueryContext: OsqueryAppContext) => { + const installation = await getInstallation(osqueryContext); + if (installation) { + return reduce( + installation.installed_kibana, + // @ts-expect-error not sure why it shouts, but still it's properly typed + (acc: Record, item: KibanaAssetReference) => { + if (item.type === savedQuerySavedObjectType) { + return { ...acc, [item.id]: item }; + } + }, + {} + ); + } + + return {}; +}; + +export const isSavedQueryPrebuilt = async ( + osqueryContext: OsqueryAppContext, + savedQueryId: string +) => { + const installation = await getInstallation(osqueryContext); + + if (installation) { + const installationSavedQueries = find( + installation.installed_kibana, + (item) => item.type === savedQuerySavedObjectType && item.id === savedQueryId + ); + + return !!installationSavedQueries; + } + + return false; +}; From a807c90310d2adf5c43694a8e4ae1f982ade0e32 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 23 May 2022 13:36:00 +0200 Subject: [PATCH 079/120] [Cases] Add a key to userActionMarkdown to prevent stale state (#132681) --- .../components/user_actions/comment/user.tsx | 1 + .../components/user_actions/description.tsx | 4 +- .../user_actions/markdown_form.test.tsx | 112 +++++++++++++++++- 3 files changed, 115 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/user.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/user.tsx index a4e6fe6cf2887..6c4c96a95bc46 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/user.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/user.tsx @@ -65,6 +65,7 @@ export const createUserAttachmentUserActionBuilder = ({ }), children: ( (commentRefs.current[comment.id] = element)} id={comment.id} content={comment.comment} diff --git a/x-pack/plugins/cases/public/components/user_actions/description.tsx b/x-pack/plugins/cases/public/components/user_actions/description.tsx index 01b0e105ecd96..eae2bd3d1258e 100644 --- a/x-pack/plugins/cases/public/components/user_actions/description.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/description.tsx @@ -43,6 +43,7 @@ export const getDescriptionUserAction = ({ handleManageMarkdownEditId, handleManageQuote, }: GetDescriptionUserActionArgs): EuiCommentProps => { + const isEditable = manageMarkdownEditIds.includes(DESCRIPTION_ID); return { username: ( , children: ( (commentRefs.current[DESCRIPTION_ID] = element)} id={DESCRIPTION_ID} content={caseData.description} - isEditable={manageMarkdownEditIds.includes(DESCRIPTION_ID)} + isEditable={isEditable} onSaveContent={(content: string) => { onUpdateField({ key: DESCRIPTION_ID, value: content }); }} diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx index 19f60d7cb8c72..ae242fc64aafa 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx @@ -8,8 +8,9 @@ import React from 'react'; import { mount } from 'enzyme'; import { UserActionMarkdown } from './markdown_form'; -import { TestProviders } from '../../common/mock'; +import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; import { waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; const onChangeEditable = jest.fn(); const onSaveContent = jest.fn(); @@ -86,4 +87,113 @@ describe('UserActionMarkdown ', () => { expect(onChangeEditable).toHaveBeenCalledWith(defaultProps.id); }); }); + + describe('useForm stale state bug', () => { + let appMockRenderer: AppMockRenderer; + const oldContent = defaultProps.content; + const appendContent = ' appended content'; + const newContent = defaultProps.content + appendContent; + + beforeEach(() => { + appMockRenderer = createAppMockRenderer(); + }); + + it('creates a stale state if a key is not passed to the component', async () => { + const TestComponent = () => { + const [isEditable, setIsEditable] = React.useState(true); + const [saveContent, setSaveContent] = React.useState(defaultProps.content); + return ( +
+ +
+ ); + }; + + const result = appMockRenderer.render(); + + expect(result.getByTestId('user-action-markdown-form')).toBeTruthy(); + + // append some content and save + userEvent.type(result.container.querySelector('textarea')!, appendContent); + userEvent.click(result.getByTestId('user-action-save-markdown')); + + // wait for the state to update + await waitFor(() => { + expect(onChangeEditable).toHaveBeenCalledWith(defaultProps.id); + }); + + // toggle to non-edit state + userEvent.click(result.getByTestId('test-button')); + expect(result.getByTestId('user-action-markdown')).toBeTruthy(); + + // toggle to edit state again + userEvent.click(result.getByTestId('test-button')); + + // the text area holds a stale value + // this is the wrong behaviour. The textarea holds the old content + expect(result.container.querySelector('textarea')!.value).toEqual(oldContent); + expect(result.container.querySelector('textarea')!.value).not.toEqual(newContent); + }); + + it("doesn't create a stale state if a key is passed to the component", async () => { + const TestComponent = () => { + const [isEditable, setIsEditable] = React.useState(true); + const [saveContent, setSaveContent] = React.useState(defaultProps.content); + return ( +
+ +
+ ); + }; + const result = appMockRenderer.render(); + expect(result.getByTestId('user-action-markdown-form')).toBeTruthy(); + + // append content and save + userEvent.type(result.container.querySelector('textarea')!, appendContent); + userEvent.click(result.getByTestId('user-action-save-markdown')); + + // wait for the state to update + await waitFor(() => { + expect(onChangeEditable).toHaveBeenCalledWith(defaultProps.id); + }); + + // toggle to non-edit state + userEvent.click(result.getByTestId('test-button')); + expect(result.getByTestId('user-action-markdown')).toBeTruthy(); + + // toggle to edit state again + userEvent.click(result.getByTestId('test-button')); + + // this is the correct behaviour. The textarea holds the new content + expect(result.container.querySelector('textarea')!.value).toEqual(newContent); + expect(result.container.querySelector('textarea')!.value).not.toEqual(oldContent); + }); + }); }); From bdb49661db64b85220d38b3fd36e448022141f04 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 23 May 2022 13:13:23 +0100 Subject: [PATCH 080/120] styling (#132539) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/common/components/charts/areachart.tsx | 7 ++++--- .../public/common/components/charts/barchart.tsx | 7 ++++--- .../public/common/components/charts/common.tsx | 5 +++++ .../common/components/visualization_actions/index.tsx | 1 + 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx index 313b216eb19ea..8da0b0b707be4 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx @@ -19,7 +19,7 @@ import { import { getOr, get, isNull, isNumber } from 'lodash/fp'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; import { useThrottledResizeObserver } from '../utils'; import { ChartPlaceHolder } from './chart_place_holder'; import { useTimeZone } from '../../lib/kibana'; @@ -32,6 +32,7 @@ import { WrappedByAutoSizer, useTheme, Wrapper, + ChartWrapper, } from './common'; import { VisualizationActions, HISTOGRAM_ACTIONS_BUTTON_CLASS } from '../visualization_actions'; import { VisualizationActionsProps } from '../visualization_actions/types'; @@ -165,7 +166,7 @@ export const AreaChartComponent: React.FC = ({ {isValidSeriesExist && areaChart && ( - + = ({ /> - + )} {!isValidSeriesExist && ( diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx index fcea5c8d77dc9..91e328c876775 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; import React, { useMemo } from 'react'; import { Chart, BarSeries, Axis, Position, ScaleType, Settings } from '@elastic/charts'; import { getOr, get, isNumber } from 'lodash/fp'; @@ -32,6 +32,7 @@ import { WrappedByAutoSizer, useTheme, Wrapper, + ChartWrapper, } from './common'; import { DraggableLegend } from './draggable_legend'; import { LegendItem } from './draggable_legend_item'; @@ -210,7 +211,7 @@ export const BarChartComponent: React.FC = ({ {isValidSeriesExist && barChart && ( - + = ({ - + )} {!isValidSeriesExist && ( diff --git a/x-pack/plugins/security_solution/public/common/components/charts/common.tsx b/x-pack/plugins/security_solution/public/common/components/charts/common.tsx index b96d016d9b186..cc24da4f27eb7 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/common.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/common.tsx @@ -20,6 +20,7 @@ import { AxisStyle, BarSeriesStyle, } from '@elastic/charts'; +import { EuiFlexGroup } from '@elastic/eui'; import React, { useMemo } from 'react'; import styled from 'styled-components'; @@ -152,3 +153,7 @@ export const checkIfAllValuesAreZero = (data: ChartSeriesData[] | null | undefin export const Wrapper = styled.div` position: relative; `; + +export const ChartWrapper = styled(EuiFlexGroup)` + z-index: 0; +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/index.tsx index 4ee0034ed4d02..e22865c18bd99 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/index.tsx @@ -30,6 +30,7 @@ const Wrapper = styled.div` position: absolute; top: 0; right: 0; + z-index: 1; } &.histogram-viz-actions { padding: ${({ theme }) => theme.eui.paddingSizes.s}; From 1135ee71b4da3a9eea715abe04b26d59e0ee1a03 Mon Sep 17 00:00:00 2001 From: Dmitrii Shevchenko Date: Mon, 23 May 2022 14:24:13 +0200 Subject: [PATCH 081/120] [Security Solution] Implement generic approach to execution context propagation (#131805) --- .eslintrc.js | 7 ++ src/core/public/apm_system.ts | 23 ++++- src/plugins/kibana_react/public/index.ts | 2 + .../kibana_react/public/router/index.ts | 9 ++ .../kibana_react/public/router/router.tsx | 86 +++++++++++++++++++ .../use_execution_context.ts | 6 +- .../cases/public/components/app/routes.tsx | 3 +- .../security_solution/public/app/index.tsx | 3 +- .../security_solution/public/app/routes.tsx | 3 +- .../ml_host_conditional_container.tsx | 3 +- .../ml_network_conditional_container.tsx | 3 +- .../public/detections/pages/alerts/index.tsx | 3 +- .../public/exceptions/routes.tsx | 3 +- .../hosts/pages/details/details_tabs.tsx | 3 +- .../public/hosts/pages/hosts_tabs.tsx | 3 +- .../public/hosts/pages/index.tsx | 3 +- .../management/pages/blocklist/index.tsx | 3 +- .../management/pages/endpoint_hosts/index.tsx | 3 +- .../management/pages/event_filters/index.tsx | 3 +- .../pages/host_isolation_exceptions/index.tsx | 3 +- .../public/management/pages/index.tsx | 3 +- .../public/management/pages/policy/index.tsx | 3 +- .../management/pages/trusted_apps/index.tsx | 3 +- .../public/network/pages/index.tsx | 3 +- .../pages/navigation/network_routes.tsx | 3 +- .../security_solution/public/rules/routes.tsx | 3 +- .../public/timelines/pages/index.tsx | 3 +- .../users/pages/details/details_tabs.tsx | 3 +- .../public/users/pages/index.tsx | 3 +- .../public/users/pages/users_tabs.tsx | 3 +- 30 files changed, 176 insertions(+), 29 deletions(-) create mode 100644 src/plugins/kibana_react/public/router/index.ts create mode 100644 src/plugins/kibana_react/public/router/router.tsx diff --git a/.eslintrc.js b/.eslintrc.js index a921718a97f79..3ec2fe38b4d6f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -956,6 +956,13 @@ module.exports = { // to help deprecation and prevent accidental re-use/continued use of code we plan on removing. If you are // finding yourself turning this off a lot for "new code" consider renaming the file and functions if it is has valid uses. patterns: ['*legacy*'], + paths: [ + { + name: 'react-router-dom', + importNames: ['Route'], + message: "import { Route } from '@kbn/kibana-react-plugin/public'", + }, + ], }, ], }, diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts index 4e116c0a0182d..81d2c5ec0896f 100644 --- a/src/core/public/apm_system.ts +++ b/src/core/public/apm_system.ts @@ -36,6 +36,7 @@ export class ApmSystem { private pageLoadTransaction?: Transaction; private resourceObserver: CachedResourceObserver; private apm?: ApmBase; + private executionContext?: ExecutionContextStart; /** * `apmConfig` would be populated with relevant APM RUM agent @@ -56,6 +57,7 @@ export class ApmSystem { } this.addHttpRequestNormalization(apm); + this.addRouteChangeNormalization(apm); init(apmConfig); // hold page load transaction blocks a transaction implicitly created by init. @@ -65,6 +67,7 @@ export class ApmSystem { async start(start?: StartDeps) { if (!this.enabled || !start) return; + this.executionContext = start.executionContext; this.markPageLoadStart(); start.executionContext.context$.subscribe((c) => { @@ -126,7 +129,7 @@ export class ApmSystem { /** * Adds an observer to the APM configuration for normalizing transactions of the 'http-request' type to remove the - * hostname, protocol, port, and base path. Allows for coorelating data cross different deployments. + * hostname, protocol, port, and base path. Allows for correlating data cross different deployments. */ private addHttpRequestNormalization(apm: ApmBase) { apm.observe('transaction:end', (t) => { @@ -154,7 +157,7 @@ export class ApmSystem { return; } - // Strip the protocol, hostnname, port, and protocol slashes to normalize + // Strip the protocol, hostname, port, and protocol slashes to normalize parts.protocol = null; parts.hostname = null; parts.port = null; @@ -171,4 +174,20 @@ export class ApmSystem { t.name = `${method} ${normalizedUrl}`; }); } + + /** + * Set route-change transaction name to the destination page name taken from + * the execution context. Otherwise, all route change transactions would have + * default names, like 'Click - span' or 'Click - a' instead of more + * descriptive '/security/rules/:id/edit'. + */ + private addRouteChangeNormalization(apm: ApmBase) { + apm.observe('transaction:end', (t) => { + const executionContext = this.executionContext?.get(); + if (executionContext && t.type === 'route-change') { + const { name, page } = executionContext; + t.name = `${name} ${page || 'unknown'}`; + } + }); + } } diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index c1f0a520a1e9c..4b4c0b6e5ab20 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -47,6 +47,8 @@ export { TableListView } from './table_list_view'; export type { ToolbarButtonProps } from './toolbar_button'; export { POSITIONS, WEIGHTS, TOOLBAR_BUTTON_SIZES, ToolbarButton } from './toolbar_button'; +export { Route } from './router'; + export { reactRouterNavigate, reactRouterOnClickHandler } from './react_router_navigate'; export type { diff --git a/src/plugins/kibana_react/public/router/index.ts b/src/plugins/kibana_react/public/router/index.ts new file mode 100644 index 0000000000000..8659ff73ced36 --- /dev/null +++ b/src/plugins/kibana_react/public/router/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { Route } from './router'; diff --git a/src/plugins/kibana_react/public/router/router.tsx b/src/plugins/kibana_react/public/router/router.tsx new file mode 100644 index 0000000000000..15e2c1df30ced --- /dev/null +++ b/src/plugins/kibana_react/public/router/router.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo } from 'react'; +import { + Route as ReactRouterRoute, + RouteComponentProps, + RouteProps, + useRouteMatch, +} from 'react-router-dom'; +import { useKibana } from '../context'; +import { useExecutionContext } from '../use_execution_context'; + +/** + * It's a wrapper around the react-router-dom Route component that inserts + * MatchPropagator in every application route. It helps track all route changes + * and send them to the execution context, later used to enrich APM + * 'route-change' transactions. + */ +export const Route = ({ children, component: Component, render, ...rest }: RouteProps) => { + const component = useMemo(() => { + if (!Component) { + return undefined; + } + return (props: RouteComponentProps) => ( + <> + + + + ); + }, [Component]); + + if (component) { + return ; + } + if (render) { + return ( + ( + <> + + {render(props)} + + )} + /> + ); + } + if (typeof children === 'function') { + return ( + ( + <> + + {children(props)} + + )} + /> + ); + } + return ( + + + {children} + + ); +}; + +const MatchPropagator = () => { + const { executionContext } = useKibana().services; + const match = useRouteMatch(); + + useExecutionContext(executionContext, { + type: 'application', + page: match.path, + id: Object.keys(match.params).length > 0 ? JSON.stringify(match.params) : undefined, + }); + + return null; +}; diff --git a/src/plugins/kibana_react/public/use_execution_context/use_execution_context.ts b/src/plugins/kibana_react/public/use_execution_context/use_execution_context.ts index 9614176bb262e..6e925ff971a6f 100644 --- a/src/plugins/kibana_react/public/use_execution_context/use_execution_context.ts +++ b/src/plugins/kibana_react/public/use_execution_context/use_execution_context.ts @@ -15,14 +15,14 @@ import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; * @param context */ export function useExecutionContext( - executionContext: CoreStart['executionContext'], + executionContext: CoreStart['executionContext'] | undefined, context: KibanaExecutionContext ) { useDeepCompareEffect(() => { - executionContext.set(context); + executionContext?.set(context); return () => { - executionContext.clear(); + executionContext?.clear(); }; }, [context]); } diff --git a/x-pack/plugins/cases/public/components/app/routes.tsx b/x-pack/plugins/cases/public/components/app/routes.tsx index 6fc87f691b2a2..031f86cf4d697 100644 --- a/x-pack/plugins/cases/public/components/app/routes.tsx +++ b/x-pack/plugins/cases/public/components/app/routes.tsx @@ -6,7 +6,8 @@ */ import React, { useCallback } from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; +import { Redirect, Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { AllCases } from '../all_cases'; import { CaseView } from '../case_view'; import { CreateCase } from '../create'; diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 4e0824f527d1e..1e4817307c227 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -7,7 +7,8 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Route, Switch } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { NotFoundPage } from './404'; import { SecurityApp } from './app'; diff --git a/x-pack/plugins/security_solution/public/app/routes.tsx b/x-pack/plugins/security_solution/public/app/routes.tsx index 587c4dd230191..a5a82a68d06ef 100644 --- a/x-pack/plugins/security_solution/public/app/routes.tsx +++ b/x-pack/plugins/security_solution/public/app/routes.tsx @@ -7,7 +7,8 @@ import { History } from 'history'; import React, { FC, memo, useEffect } from 'react'; -import { Route, Router, Switch } from 'react-router-dom'; +import { Router, Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { useDispatch } from 'react-redux'; import { AppLeaveHandler, AppMountParameters } from '@kbn/core/public'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx index 7dcaa1de58d10..a18d51cb63773 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx @@ -8,7 +8,8 @@ import { parse, stringify } from 'query-string'; import React from 'react'; -import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; +import { Redirect, Switch, useRouteMatch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { url as urlUtils } from '@kbn/kibana-utils-plugin/public'; import { addEntitiesToKql } from './add_entities_to_kql'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx index a8dcf18702b97..7261ef7335a99 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx @@ -8,7 +8,8 @@ import { parse, stringify } from 'query-string'; import React from 'react'; -import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; +import { Redirect, Switch, useRouteMatch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { url as urlUtils } from '@kbn/kibana-utils-plugin/public'; import { addEntitiesToKql } from './add_entities_to_kql'; import { replaceKQLParts } from './replace_kql_parts'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx index 9945d5225ea02..288b389215e48 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { ALERTS_PATH, SecurityPageName } from '../../../../common/constants'; diff --git a/x-pack/plugins/security_solution/public/exceptions/routes.tsx b/x-pack/plugins/security_solution/public/exceptions/routes.tsx index 5e89ec8e4d40b..9dad970be0fe9 100644 --- a/x-pack/plugins/security_solution/public/exceptions/routes.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/routes.tsx @@ -5,7 +5,8 @@ * 2.0. */ import React from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx index 7baa72b31ae07..5b1f84fe5373c 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx @@ -6,7 +6,8 @@ */ import React, { useCallback } from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { UpdateDateRange } from '../../../common/components/charts/common'; import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx index af619db5e9fa0..6f593b676d5d3 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx @@ -6,7 +6,8 @@ */ import React, { memo, useCallback } from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { HostsTabsProps } from './types'; import { scoreIntervalToDateTime } from '../../common/components/ml/score/score_interval_to_datetime'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx index 00afec5da3756..764281a48b737 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { Route, Switch, Redirect } from 'react-router-dom'; +import { Switch, Redirect } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { HOSTS_PATH } from '../../../common/constants'; import { HostDetails } from './details'; import { HostsTableType } from '../store/model'; diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/index.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/index.tsx index 59638f2e529ab..0a57b49f0c31b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/index.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { Switch, Route } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import React, { memo } from 'react'; import { MANAGEMENT_ROUTING_BLOCKLIST_PATH } from '../../common/constants'; import { NotFoundPage } from '../../../app/404'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx index ac361ff8f16ad..14aa195e0be81 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { Switch, Route } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import React, { memo } from 'react'; import { EndpointList } from './view'; import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../common/constants'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx index 54d18f85b739a..9cb825bd06a69 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { Route, Switch } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import React from 'react'; import { NotFoundPage } from '../../../app/404'; import { MANAGEMENT_ROUTING_EVENT_FILTERS_PATH } from '../../common/constants'; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/index.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/index.tsx index 7ed2adf8c94fa..bf1966d93dda3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/index.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { Switch, Route } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import React, { memo } from 'react'; import { MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH } from '../../common/constants'; import { NotFoundPage } from '../../../app/404'; diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 2da77bb24c919..f4a83860ceed4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -6,7 +6,8 @@ */ import React, { memo } from 'react'; -import { Route, Switch, Redirect } from 'react-router-dom'; +import { Switch, Redirect } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { EuiLoadingSpinner } from '@elastic/eui'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx index e6680af5c7daf..8157cc65e3fba 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx @@ -6,7 +6,8 @@ */ import React, { memo } from 'react'; -import { Route, Switch, Redirect } from 'react-router-dom'; +import { Switch, Redirect } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { PolicyDetails, PolicyList } from './view'; import { MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/index.tsx index d9edd9986e156..6d1d56ea25e2d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/index.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { Switch, Route } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import React, { memo } from 'react'; import { TrustedAppsList } from './view/trusted_apps_list'; import { MANAGEMENT_ROUTING_TRUSTED_APPS_PATH } from '../../common/constants'; diff --git a/x-pack/plugins/security_solution/public/network/pages/index.tsx b/x-pack/plugins/security_solution/public/network/pages/index.tsx index e5c130fbe761d..30510e3269f5c 100644 --- a/x-pack/plugins/security_solution/public/network/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/index.tsx @@ -6,7 +6,8 @@ */ import React, { useMemo } from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; +import { Redirect, Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx index ea026664ce1e4..fc4f9d3831ad2 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx @@ -6,7 +6,8 @@ */ import React, { useCallback } from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { FlowTargetSourceDest } from '../../../../common/search_strategy/security_solution/network'; diff --git a/x-pack/plugins/security_solution/public/rules/routes.tsx b/x-pack/plugins/security_solution/public/rules/routes.tsx index 468e2b41e9290..60f37ee5a1dbe 100644 --- a/x-pack/plugins/security_solution/public/rules/routes.tsx +++ b/x-pack/plugins/security_solution/public/rules/routes.tsx @@ -5,8 +5,9 @@ * 2.0. */ import React from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import * as i18n from './translations'; import { RULES_PATH, SecurityPageName } from '../../common/constants'; diff --git a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx index 5ad969adba5cd..8cd417f904023 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { Switch, Route, Redirect } from 'react-router-dom'; +import { Switch, Redirect } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { TimelineType } from '../../../common/types/timeline'; diff --git a/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx index 22b394f41bfaf..3cc2970b9d650 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx @@ -6,7 +6,8 @@ */ import React, { useCallback, useMemo } from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { UsersTableType } from '../../store/model'; import { AnomaliesUserTable } from '../../../common/components/ml/tables/anomalies_user_table'; diff --git a/x-pack/plugins/security_solution/public/users/pages/index.tsx b/x-pack/plugins/security_solution/public/users/pages/index.tsx index 04b1122a39b54..055bb2bb71ab2 100644 --- a/x-pack/plugins/security_solution/public/users/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/index.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { Route, Switch, Redirect } from 'react-router-dom'; +import { Switch, Redirect } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { USERS_PATH } from '../../../common/constants'; import { UsersTableType } from '../store/model'; import { Users } from './users'; diff --git a/x-pack/plugins/security_solution/public/users/pages/users_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/users_tabs.tsx index fb2cecee75ea6..4d154ee5e3e7c 100644 --- a/x-pack/plugins/security_solution/public/users/pages/users_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/users_tabs.tsx @@ -6,7 +6,8 @@ */ import React, { memo, useCallback } from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Switch } from 'react-router-dom'; +import { Route } from '@kbn/kibana-react-plugin/public'; import { UsersTabsProps } from './types'; import { UsersTableType } from '../store/model'; From 3e8e89069f9c244cf7b077e3f74d04a27fb68073 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Mon, 23 May 2022 14:51:32 +0200 Subject: [PATCH 082/120] Rename the menu group title, file names and variable. (#132679) --- .../security_solution/common/constants.ts | 7 +- .../public/app/deep_links/index.ts | 18 +- .../public/app/translations.ts | 4 - .../public/common/components/link_to/index.ts | 2 +- .../navigation/breadcrumbs/index.test.ts | 237 +++++++----------- .../solution_grouped_nav.test.tsx | 16 +- .../common/components/url_state/constants.ts | 2 +- .../utils/timeline/use_show_timeline.tsx | 2 +- .../public/landing_pages/links.ts | 14 +- .../pages/{threat_hunting.tsx => explore.tsx} | 12 +- .../landing_pages/pages/translations.ts | 4 +- .../public/landing_pages/routes.tsx | 10 +- 12 files changed, 124 insertions(+), 204 deletions(-) rename x-pack/plugins/security_solution/public/landing_pages/pages/{threat_hunting.tsx => explore.tsx} (67%) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index f8c159241d00e..deac55e74c0fb 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -89,7 +89,6 @@ export enum SecurityPageName { endpoints = 'endpoints', eventFilters = 'event_filters', exceptions = 'exceptions', - explore = 'explore', hostIsolationExceptions = 'host_isolation_exceptions', hosts = 'hosts', hostsAnomalies = 'hosts-anomalies', @@ -120,11 +119,11 @@ export enum SecurityPageName { sessions = 'sessions', usersEvents = 'users-events', usersExternalAlerts = 'users-external_alerts', - threatHuntingLanding = 'threat_hunting', + exploreLanding = 'explore', dashboardsLanding = 'dashboards', } -export const THREAT_HUNTING_PATH = '/threat_hunting' as const; +export const EXPLORE_PATH = '/explore' as const; export const DASHBOARDS_PATH = '/dashboards' as const; export const MANAGE_PATH = '/manage' as const; export const TIMELINES_PATH = '/timelines' as const; @@ -153,7 +152,7 @@ export const APP_OVERVIEW_PATH = `${APP_PATH}${OVERVIEW_PATH}` as const; export const APP_LANDING_PATH = `${APP_PATH}${LANDING_PATH}` as const; export const APP_DETECTION_RESPONSE_PATH = `${APP_PATH}${DETECTION_RESPONSE_PATH}` as const; export const APP_MANAGEMENT_PATH = `${APP_PATH}${MANAGEMENT_PATH}` as const; -export const APP_THREAT_HUNTING_PATH = `${APP_PATH}${THREAT_HUNTING_PATH}` as const; +export const APP_EXPLORE_PATH = `${APP_PATH}${EXPLORE_PATH}` as const; export const APP_DASHBOARDS_PATH = `${APP_PATH}${DASHBOARDS_PATH}` as const; export const APP_ALERTS_PATH = `${APP_PATH}${ALERTS_PATH}` as const; diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index 6598e0dc29426..b265eff2dff19 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -34,7 +34,6 @@ import { POLICIES, ENDPOINTS, GETTING_STARTED, - THREAT_HUNTING, DASHBOARDS, CREATE_NEW_RULE, } from '../translations'; @@ -58,7 +57,7 @@ import { HOST_ISOLATION_EXCEPTIONS_PATH, SERVER_APP_ID, USERS_PATH, - THREAT_HUNTING_PATH, + EXPLORE_PATH, DASHBOARDS_PATH, MANAGE_PATH, RULES_CREATE_PATH, @@ -115,18 +114,6 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ }), ], }, - { - id: SecurityPageName.threatHuntingLanding, - title: THREAT_HUNTING, - path: THREAT_HUNTING_PATH, - navLinkStatus: AppNavLinkStatus.hidden, - features: [FEATURE.general], - keywords: [ - i18n.translate('xpack.securitySolution.search.threatHunting', { - defaultMessage: 'Threat Hunting', - }), - ], - }, { id: SecurityPageName.dashboardsLanding, title: DASHBOARDS, @@ -213,8 +200,9 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ ], }, { - id: SecurityPageName.explore, + id: SecurityPageName.exploreLanding, title: EXPLORE, + path: EXPLORE_PATH, navLinkStatus: AppNavLinkStatus.hidden, features: [FEATURE.general], keywords: [ diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 354ba438ff52a..7586cff6e0da0 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -26,10 +26,6 @@ export const GETTING_STARTED = i18n.translate('xpack.securitySolution.navigation defaultMessage: 'Get started', }); -export const THREAT_HUNTING = i18n.translate('xpack.securitySolution.navigation.threatHunting', { - defaultMessage: 'Threat Hunting', -}); - export const DASHBOARDS = i18n.translate('xpack.securitySolution.navigation.dashboards', { defaultMessage: 'Dashboards', }); diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts index ba86842106e23..fe0b8fbdbfba7 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts @@ -90,7 +90,7 @@ function formatPath(path: string, search: string, skipSearch?: boolean) { function needsUrlState(pageId: SecurityPageName) { return ( pageId !== SecurityPageName.dashboardsLanding && - pageId !== SecurityPageName.threatHuntingLanding && + pageId !== SecurityPageName.exploreLanding && pageId !== SecurityPageName.administration && pageId !== SecurityPageName.rules && pageId !== SecurityPageName.exceptions && diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index 05dd7145ba785..e545d4f19bbb9 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -132,6 +132,36 @@ jest.mock('../../../lib/kibana/kibana_react', () => { }; }); +const securityBreadCrumb = { + href: 'securitySolutionUI/get_started', + text: 'Security', +}; + +const hostsBreadcrumbs = { + href: 'securitySolutionUI/hosts', + text: 'Hosts', +}; + +const networkBreadcrumb = { + text: 'Network', + href: 'securitySolutionUI/network', +}; + +const exploreBreadcrumbs = { + href: 'securitySolutionUI/explore', + text: 'Explore', +}; + +const rulesBReadcrumb = { + text: 'Rules', + href: 'securitySolutionUI/rules', +}; + +const manageBreadcrumbs = { + text: 'Manage', + href: 'securitySolutionUI/administration', +}; + describe('Navigation Breadcrumbs', () => { beforeAll(async () => { const appLinks = await getAppLinks(coreMock.createStart(), {} as StartPlugins); @@ -169,10 +199,7 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { - href: 'securitySolutionUI/get_started', - text: 'Security', - }, + securityBreadCrumb, { href: '', text: 'Overview', @@ -187,14 +214,8 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { - href: 'securitySolutionUI/get_started', - text: 'Security', - }, - { - href: 'securitySolutionUI/hosts', - text: 'Hosts', - }, + securityBreadCrumb, + hostsBreadcrumbs, { href: '', text: 'Authentications', @@ -209,11 +230,8 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Network', - href: 'securitySolutionUI/network', - }, + securityBreadCrumb, + networkBreadcrumb, { text: 'Flows', href: '', @@ -228,7 +246,7 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, + securityBreadCrumb, { text: 'Timelines', href: '', @@ -243,11 +261,8 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Hosts', - href: 'securitySolutionUI/hosts', - }, + securityBreadCrumb, + hostsBreadcrumbs, { text: 'siem-kibana', href: 'securitySolutionUI/hosts/siem-kibana', @@ -263,11 +278,8 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Network', - href: 'securitySolutionUI/network', - }, + securityBreadCrumb, + networkBreadcrumb, { text: ipv4, href: `securitySolutionUI/network/ip/${ipv4}/source`, @@ -283,11 +295,8 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Network', - href: 'securitySolutionUI/network', - }, + securityBreadCrumb, + networkBreadcrumb, { text: ipv6, href: `securitySolutionUI/network/ip/${ipv6Encoded}/source`, @@ -303,7 +312,7 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, + securityBreadCrumb, { text: 'Alerts', href: '', @@ -318,7 +327,7 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, + securityBreadCrumb, { text: 'Exception lists', href: '', @@ -333,7 +342,7 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, + securityBreadCrumb, { text: 'Rules', href: '', @@ -348,11 +357,8 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Rules', - href: 'securitySolutionUI/rules', - }, + securityBreadCrumb, + rulesBReadcrumb, { text: 'Create', href: '', @@ -375,11 +381,8 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Rules', - href: 'securitySolutionUI/rules', - }, + securityBreadCrumb, + rulesBReadcrumb, { text: mockRuleName, href: ``, @@ -402,11 +405,8 @@ describe('Navigation Breadcrumbs', () => { false ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Rules', - href: 'securitySolutionUI/rules', - }, + securityBreadCrumb, + rulesBReadcrumb, { text: 'ALERT_RULE_NAME', href: `securitySolutionUI/rules/id/${mockDetailName}`, @@ -451,7 +451,7 @@ describe('Navigation Breadcrumbs', () => { ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, + securityBreadCrumb, { text: 'Endpoints', href: '', @@ -504,10 +504,7 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { - href: 'securitySolutionUI/get_started', - text: 'Security', - }, + securityBreadCrumb, { href: 'securitySolutionUI/dashboards', text: 'Dashboards', @@ -526,18 +523,9 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { - href: 'securitySolutionUI/get_started', - text: 'Security', - }, - { - href: 'securitySolutionUI/threat_hunting', - text: 'Threat Hunting', - }, - { - href: 'securitySolutionUI/hosts', - text: 'Hosts', - }, + securityBreadCrumb, + exploreBreadcrumbs, + hostsBreadcrumbs, { href: '', text: 'Authentications', @@ -552,15 +540,9 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - href: 'securitySolutionUI/threat_hunting', - text: 'Threat Hunting', - }, - { - text: 'Network', - href: 'securitySolutionUI/network', - }, + securityBreadCrumb, + exploreBreadcrumbs, + networkBreadcrumb, { text: 'Flows', href: '', @@ -575,7 +557,7 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, + securityBreadCrumb, { text: 'Timelines', href: '', @@ -590,15 +572,9 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - href: 'securitySolutionUI/threat_hunting', - text: 'Threat Hunting', - }, - { - text: 'Hosts', - href: 'securitySolutionUI/hosts', - }, + securityBreadCrumb, + exploreBreadcrumbs, + hostsBreadcrumbs, { text: 'siem-kibana', href: 'securitySolutionUI/hosts/siem-kibana', @@ -614,15 +590,9 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - href: 'securitySolutionUI/threat_hunting', - text: 'Threat Hunting', - }, - { - text: 'Network', - href: 'securitySolutionUI/network', - }, + securityBreadCrumb, + exploreBreadcrumbs, + networkBreadcrumb, { text: ipv4, href: `securitySolutionUI/network/ip/${ipv4}/source`, @@ -638,15 +608,9 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - href: 'securitySolutionUI/threat_hunting', - text: 'Threat Hunting', - }, - { - text: 'Network', - href: 'securitySolutionUI/network', - }, + securityBreadCrumb, + exploreBreadcrumbs, + networkBreadcrumb, { text: ipv6, href: `securitySolutionUI/network/ip/${ipv6Encoded}/source`, @@ -662,7 +626,7 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, + securityBreadCrumb, { text: 'Alerts', href: '', @@ -677,11 +641,8 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Manage', - href: 'securitySolutionUI/administration', - }, + securityBreadCrumb, + manageBreadcrumbs, { text: 'Exception lists', href: '', @@ -696,11 +657,8 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Manage', - href: 'securitySolutionUI/administration', - }, + securityBreadCrumb, + manageBreadcrumbs, { text: 'Rules', href: '', @@ -715,15 +673,9 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Manage', - href: 'securitySolutionUI/administration', - }, - { - text: 'Rules', - href: 'securitySolutionUI/rules', - }, + securityBreadCrumb, + manageBreadcrumbs, + rulesBReadcrumb, { text: 'Create', href: '', @@ -746,15 +698,9 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Manage', - href: 'securitySolutionUI/administration', - }, - { - text: 'Rules', - href: 'securitySolutionUI/rules', - }, + securityBreadCrumb, + manageBreadcrumbs, + rulesBReadcrumb, { text: mockRuleName, href: ``, @@ -777,15 +723,9 @@ describe('Navigation Breadcrumbs', () => { true ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Manage', - href: 'securitySolutionUI/administration', - }, - { - text: 'Rules', - href: 'securitySolutionUI/rules', - }, + securityBreadCrumb, + manageBreadcrumbs, + rulesBReadcrumb, { text: 'ALERT_RULE_NAME', href: `securitySolutionUI/rules/id/${mockDetailName}`, @@ -830,11 +770,8 @@ describe('Navigation Breadcrumbs', () => { ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Manage', - href: 'securitySolutionUI/administration', - }, + securityBreadCrumb, + manageBreadcrumbs, { text: 'Endpoints', href: '', @@ -858,8 +795,8 @@ describe('Navigation Breadcrumbs', () => { onClick: expect.any(Function), }), expect.objectContaining({ - text: 'Threat Hunting', - href: `securitySolutionUI/threat_hunting`, + text: 'Explore', + href: `securitySolutionUI/explore`, onClick: expect.any(Function), }), expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.test.tsx index e41b566bbc7c8..5f5fd14605643 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.test.tsx @@ -74,14 +74,14 @@ describe('SolutionGroupedNav', () => { const items = [ ...mockItems, { - id: SecurityPageName.threatHuntingLanding, - label: 'Threat Hunting', - href: '/threat_hunting', + id: SecurityPageName.exploreLanding, + label: 'Explore', + href: '/explore', onClick: mockOnClick, }, ]; const result = renderNav({ items }); - result.getByTestId(`groupedNavItemLink-${SecurityPageName.threatHuntingLanding}`).click(); + result.getByTestId(`groupedNavItemLink-${SecurityPageName.exploreLanding}`).click(); expect(mockOnClick).toHaveBeenCalled(); }); }); @@ -122,9 +122,9 @@ describe('SolutionGroupedNav', () => { const items = [ ...mockItems, { - id: SecurityPageName.threatHuntingLanding, - label: 'Threat Hunting', - href: '/threat_hunting', + id: SecurityPageName.exploreLanding, + label: 'Explore', + href: '/explore', items: [ { id: SecurityPageName.users, @@ -141,7 +141,7 @@ describe('SolutionGroupedNav', () => { expect(result.getByTestId('groupedNavPanel')).toBeInTheDocument(); expect(result.getByText('Overview')).toBeInTheDocument(); - result.getByTestId(`groupedNavItemButton-${SecurityPageName.threatHuntingLanding}`).click(); + result.getByTestId(`groupedNavItemButton-${SecurityPageName.exploreLanding}`).click(); expect(result.queryByTestId('groupedNavPanel')).toBeInTheDocument(); expect(result.getByText('Users')).toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts index dcedec945575d..0595428fa83af 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts @@ -38,5 +38,5 @@ export type UrlStateType = | 'overview' | 'rules' | 'timeline' - | 'threat_hunting' + | 'explore' | 'dashboards'; diff --git a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx index bb9eb075d735f..c2dad5ff8f065 100644 --- a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx @@ -16,7 +16,7 @@ const DEPRECATED_HIDDEN_TIMELINE_ROUTES: readonly string[] = [ '/administration', '/rules/create', '/get_started', - '/threat_hunting', + '/explore', '/dashboards', '/manage', ]; diff --git a/x-pack/plugins/security_solution/public/landing_pages/links.ts b/x-pack/plugins/security_solution/public/landing_pages/links.ts index 48cd31485ea7f..3c8ec59632deb 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/links.ts +++ b/x-pack/plugins/security_solution/public/landing_pages/links.ts @@ -8,11 +8,11 @@ import { i18n } from '@kbn/i18n'; import { DASHBOARDS_PATH, + EXPLORE_PATH, SecurityPageName, SERVER_APP_ID, - THREAT_HUNTING_PATH, } from '../../common/constants'; -import { DASHBOARDS, THREAT_HUNTING } from '../app/translations'; +import { DASHBOARDS, EXPLORE } from '../app/translations'; import { LinkItem } from '../common/links/types'; import { overviewLinks, detectionResponseLinks } from '../overview/links'; import { links as hostsLinks } from '../hosts/links'; @@ -36,14 +36,14 @@ export const dashboardsLandingLinks: LinkItem = { }; export const threatHuntingLandingLinks: LinkItem = { - id: SecurityPageName.threatHuntingLanding, - title: THREAT_HUNTING, - path: THREAT_HUNTING_PATH, + id: SecurityPageName.exploreLanding, + title: EXPLORE, + path: EXPLORE_PATH, globalNavEnabled: false, capabilities: [`${SERVER_APP_ID}.show`], globalSearchKeywords: [ - i18n.translate('xpack.securitySolution.appLinks.threatHunting', { - defaultMessage: 'Threat hunting', + i18n.translate('xpack.securitySolution.appLinks.explore', { + defaultMessage: 'Explore', }), ], links: [hostsLinks, networkLinks, usersLinks], diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/threat_hunting.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/explore.tsx similarity index 67% rename from x-pack/plugins/security_solution/public/landing_pages/pages/threat_hunting.tsx rename to x-pack/plugins/security_solution/public/landing_pages/pages/explore.tsx index 605a1baeedbd6..17a0d7569b965 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/threat_hunting.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/explore.tsx @@ -11,16 +11,16 @@ import { useAppRootNavLink } from '../../common/components/navigation/nav_links' import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { LandingLinksImages } from '../components/landing_links_images'; -import { THREAT_HUNTING_PAGE_TITLE } from './translations'; +import { EXPLORE_PAGE_TITLE } from './translations'; -export const ThreatHuntingLandingPage = () => { - const threatHuntinglinks = useAppRootNavLink(SecurityPageName.threatHuntingLanding)?.links ?? []; +export const ExploreLandingPage = () => { + const exploreLinks = useAppRootNavLink(SecurityPageName.exploreLanding)?.links ?? []; return ( - - - + + + ); }; diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/translations.ts b/x-pack/plugins/security_solution/public/landing_pages/pages/translations.ts index 13a2396201cc5..4986c6b5f31ec 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/translations.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; -export const THREAT_HUNTING_PAGE_TITLE = i18n.translate( +export const EXPLORE_PAGE_TITLE = i18n.translate( 'xpack.securitySolution.landing.threatHunting.pageTitle', { - defaultMessage: 'Threat hunting', + defaultMessage: 'Explore', } ); diff --git a/x-pack/plugins/security_solution/public/landing_pages/routes.tsx b/x-pack/plugins/security_solution/public/landing_pages/routes.tsx index 3fbe33cc0ec88..038100cda463f 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/routes.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/routes.tsx @@ -9,14 +9,14 @@ import React from 'react'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { SecurityPageName, SecuritySubPluginRoutes } from '../app/types'; -import { DASHBOARDS_PATH, MANAGE_PATH, THREAT_HUNTING_PATH } from '../../common/constants'; -import { ThreatHuntingLandingPage } from './pages/threat_hunting'; +import { DASHBOARDS_PATH, MANAGE_PATH, EXPLORE_PATH } from '../../common/constants'; +import { ExploreLandingPage } from './pages/explore'; import { DashboardsLandingPage } from './pages/dashboards'; import { ManageLandingPage } from './pages/manage'; export const ThreatHuntingRoutes = () => ( - - + + ); @@ -34,7 +34,7 @@ export const ManageRoutes = () => ( export const routes: SecuritySubPluginRoutes = [ { - path: THREAT_HUNTING_PATH, + path: EXPLORE_PATH, render: ThreatHuntingRoutes, }, { From c16bcdc15dcda785aba3bc2ac173359e0ccc5a68 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 23 May 2022 15:27:09 +0200 Subject: [PATCH 083/120] [APM] Trace explorer (#131897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [APM] Trace explorer * Make sure tabs work on trace explorer * Add links to trace explorer from error detail view & service map * Add API tests * Fix failing E2E test * don't select edges when explorer is disabled * Fix lint error * Update x-pack/plugins/observability/server/ui_settings.ts Co-authored-by: Søren Louv-Jansen * Review feedback * Rename const in API tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Søren Louv-Jansen --- .../components/code_editor/code_editor.tsx | 3 + ...rn_constants.ts => data_view_constants.ts} | 3 +- x-pack/plugins/apm/common/trace_explorer.ts | 16 + .../errors/error_details.spec.ts | 2 +- x-pack/plugins/apm/kibana.json | 11 +- .../plugins/apm/public/application/index.tsx | 2 + .../__snapshots__/index.test.tsx.snap | 78 ----- .../detail_view/index.test.tsx | 82 +++-- .../error_group_details/detail_view/index.tsx | 111 +++++-- .../components/app/service_map/cytoscape.tsx | 4 +- .../app/service_map/cytoscape_options.ts | 24 +- .../service_map/popover/backend_contents.tsx | 5 +- .../app/service_map/popover/edge_contents.tsx | 84 +++++ .../popover/externals_list_contents.tsx | 4 +- .../app/service_map/popover/index.tsx | 81 +++-- .../service_map/popover/resource_contents.tsx | 4 +- .../service_map/popover/service_contents.tsx | 4 +- .../app/top_traces_overview/index.tsx | 61 ++++ .../trace_list.tsx | 17 +- .../components/app/trace_explorer/index.tsx | 157 +++++++++ .../trace_explorer/trace_search_box/index.tsx | 186 +++++++++++ .../components/app/trace_overview/index.tsx | 122 +++---- .../distribution/index.tsx | 56 +++- .../use_waterfall_fetcher.ts | 24 +- .../waterfall_with_summary/index.tsx | 40 ++- .../maybe_view_trace_link.tsx | 15 +- .../transaction_tabs.tsx | 60 ++-- .../waterfall_container/index.tsx | 18 +- .../waterfall/flyout_top_level_properties.tsx | 17 +- .../span_flyout/sticky_span_properties.tsx | 18 +- .../waterfall/waterfall_item.tsx | 8 +- .../waterfall_container.stories.tsx | 70 +++- .../public/components/routing/app_root.tsx | 5 +- .../public/components/routing/home/index.tsx | 71 +++- .../shared/date_picker/apm_date_picker.tsx | 49 +++ .../shared/eql_code_editor/completer.ts | 195 +++++++++++ .../shared/eql_code_editor/constants.ts | 15 + .../eql_code_editor/eql_highlight_rules.ts | 145 +++++++++ .../shared/eql_code_editor/eql_mode.ts | 24 ++ .../shared/eql_code_editor/index.tsx | 54 +++ .../lazily_loaded_code_editor.tsx | 39 +++ .../shared/eql_code_editor/theme.ts | 91 ++++++ .../shared/eql_code_editor/tokens.ts | 25 ++ .../shared/eql_code_editor/types.ts | 33 ++ .../links/discover_links/discover_link.tsx | 4 +- .../public/components/shared/search_bar.tsx | 38 +-- .../context/apm_plugin/apm_plugin_context.tsx | 4 + .../apm/public/hooks/use_apm_route_path.ts | 14 + .../apm/public/hooks/use_static_data_view.ts | 16 + .../use_trace_explorer_enabled_setting.ts | 15 + x-pack/plugins/apm/public/plugin.ts | 3 + .../create_es_client/call_async_with_debug.ts | 2 +- .../cancel_es_request_on_abort.ts | 4 +- .../create_apm_event_client/index.ts | 54 ++- .../unpack_processor_events.ts | 16 +- .../create_internal_es_client/index.ts | 6 +- .../data_view/create_static_data_view.ts | 6 +- .../transform_service_map_responses.ts | 9 +- .../traces/get_trace_samples_by_query.ts | 168 ++++++++++ .../plugins/apm/server/routes/traces/route.ts | 51 ++- x-pack/plugins/apm/server/tutorial/index.ts | 4 +- x-pack/plugins/observability/common/index.ts | 1 + .../observability/common/ui_settings_keys.ts | 1 + .../common/utils/get_inspect_response.ts | 2 +- .../observability/server/ui_settings.ts | 16 + .../tests/data_view/static.spec.ts | 6 +- .../tests/traces/find_traces.spec.ts | 307 ++++++++++++++++++ 67 files changed, 2476 insertions(+), 404 deletions(-) rename x-pack/plugins/apm/common/{index_pattern_constants.ts => data_view_constants.ts} (67%) create mode 100644 x-pack/plugins/apm/common/trace_explorer.ts delete mode 100644 x-pack/plugins/apm/public/components/app/error_group_details/detail_view/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/plugins/apm/public/components/app/service_map/popover/edge_contents.tsx create mode 100644 x-pack/plugins/apm/public/components/app/top_traces_overview/index.tsx rename x-pack/plugins/apm/public/components/app/{trace_overview => top_traces_overview}/trace_list.tsx (92%) create mode 100644 x-pack/plugins/apm/public/components/app/trace_explorer/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/trace_explorer/trace_search_box/index.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/date_picker/apm_date_picker.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/eql_code_editor/completer.ts create mode 100644 x-pack/plugins/apm/public/components/shared/eql_code_editor/constants.ts create mode 100644 x-pack/plugins/apm/public/components/shared/eql_code_editor/eql_highlight_rules.ts create mode 100644 x-pack/plugins/apm/public/components/shared/eql_code_editor/eql_mode.ts create mode 100644 x-pack/plugins/apm/public/components/shared/eql_code_editor/index.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/eql_code_editor/lazily_loaded_code_editor.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/eql_code_editor/theme.ts create mode 100644 x-pack/plugins/apm/public/components/shared/eql_code_editor/tokens.ts create mode 100644 x-pack/plugins/apm/public/components/shared/eql_code_editor/types.ts create mode 100644 x-pack/plugins/apm/public/hooks/use_apm_route_path.ts create mode 100644 x-pack/plugins/apm/public/hooks/use_static_data_view.ts create mode 100644 x-pack/plugins/apm/public/hooks/use_trace_explorer_enabled_setting.ts create mode 100644 x-pack/plugins/apm/server/routes/traces/get_trace_samples_by_query.ts create mode 100644 x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts diff --git a/src/plugins/es_ui_shared/public/components/code_editor/code_editor.tsx b/src/plugins/es_ui_shared/public/components/code_editor/code_editor.tsx index 5f172d010b836..d5ed974d0e40b 100644 --- a/src/plugins/es_ui_shared/public/components/code_editor/code_editor.tsx +++ b/src/plugins/es_ui_shared/public/components/code_editor/code_editor.tsx @@ -58,6 +58,8 @@ export interface EuiCodeEditorProps extends SupportedAriaAttributes, Omit void; } export interface EuiCodeEditorState { @@ -98,6 +100,7 @@ class EuiCodeEditor extends Component { setOrRemoveAttribute(textbox, 'aria-labelledby', this.props['aria-labelledby']); setOrRemoveAttribute(textbox, 'aria-describedby', this.props['aria-describedby']); } + this.props.onAceEditorRef?.(aceEditor); }; onEscToExit = () => { diff --git a/x-pack/plugins/apm/common/index_pattern_constants.ts b/x-pack/plugins/apm/common/data_view_constants.ts similarity index 67% rename from x-pack/plugins/apm/common/index_pattern_constants.ts rename to x-pack/plugins/apm/common/data_view_constants.ts index 4b67bba1fef91..b448918f8facf 100644 --- a/x-pack/plugins/apm/common/index_pattern_constants.ts +++ b/x-pack/plugins/apm/common/data_view_constants.ts @@ -5,4 +5,5 @@ * 2.0. */ -export const APM_STATIC_INDEX_PATTERN_ID = 'apm_static_index_pattern_id'; +// value of const needs to be backwards compatible +export const APM_STATIC_DATA_VIEW_ID = 'apm_static_index_pattern_id'; diff --git a/x-pack/plugins/apm/common/trace_explorer.ts b/x-pack/plugins/apm/common/trace_explorer.ts new file mode 100644 index 0000000000000..9ce3bc8df0bd6 --- /dev/null +++ b/x-pack/plugins/apm/common/trace_explorer.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface TraceSearchQuery { + query: string; + type: TraceSearchType; +} + +export enum TraceSearchType { + kql = 'kql', + eql = 'eql', +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts index c131cb2dd36d7..caec7a23115ff 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts @@ -89,7 +89,7 @@ describe('Error details', () => { describe('when clicking on View x occurences in discover', () => { it('should redirects the user to discover', () => { cy.visit(errorDetailsPageHref); - cy.contains('View 1 occurrence in Discover.').click(); + cy.contains('View 1 occurrence in Discover').click(); cy.url().should('include', 'app/discover'); }); }); diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 9bb1c52b52d7c..f354556c97b5b 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -17,7 +17,8 @@ "observability", "ruleRegistry", "triggersActionsUi", - "unifiedSearch" + "unifiedSearch", + "dataViews" ], "optionalPlugins": [ "actions", @@ -33,12 +34,16 @@ ], "server": true, "ui": true, - "configPath": ["xpack", "apm"], + "configPath": [ + "xpack", + "apm" + ], "requiredBundles": [ "fleet", "kibanaReact", "kibanaUtils", "ml", - "observability" + "observability", + "esUiShared" ] } diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 19e16237c1272..b471655c6b7d5 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -52,6 +52,8 @@ export const renderApp = ({ inspector: pluginsStart.inspector, observability: pluginsStart.observability, observabilityRuleTypeRegistry, + dataViews: pluginsStart.dataViews, + unifiedSearch: pluginsStart.unifiedSearch, }; // render APM feedback link in global help menu diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/__snapshots__/index.test.tsx.snap b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 5f300b45de80a..0000000000000 --- a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,78 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DetailView should render Discover button 1`] = ` - - View 10 occurrences in Discover. - -`; - -exports[`DetailView should render TabContent 1`] = ` - -`; - -exports[`DetailView should render tabs 1`] = ` - - - Exception stack trace - - - Metadata - - -`; diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.test.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.test.tsx index a6743f5b7d768..ac38a84bf47d7 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.test.tsx @@ -5,10 +5,33 @@ * 2.0. */ -import { shallow } from 'enzyme'; +import { render } from '@testing-library/react'; import React from 'react'; import { mockMoment } from '../../../../utils/test_helpers'; import { DetailView } from '.'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import { createMemoryHistory } from 'history'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; + +const history = createMemoryHistory({ + initialEntries: [ + '/services/opbeans-java/errors/0000?rangeFrom=now-15m&rangeTo=now', + ], +}); + +function MockContext({ children }: { children: React.ReactElement }) { + return ( + + + {children} + + + ); +} + +function renderWithMockContext(element: React.ReactElement) { + return render(element, { wrapper: MockContext }); +} describe('DetailView', () => { beforeEach(() => { @@ -17,10 +40,10 @@ describe('DetailView', () => { }); it('should render empty state', () => { - const wrapper = shallow( + const wrapper = renderWithMockContext( ); - expect(wrapper.isEmptyRender()).toBe(true); + expect(wrapper.baseElement.innerHTML).toBe('
'); }); it('should render Discover button', () => { @@ -35,23 +58,25 @@ describe('DetailView', () => { url: { full: 'myUrl' }, service: { name: 'myService' }, user: { id: 'myUserId' }, - error: { exception: { handled: true } }, + error: { exception: [{ handled: true }] }, transaction: { id: 'myTransactionId', sampled: true }, } as any, }; - const wrapper = shallow( + const discoverLink = renderWithMockContext( - ).find('DiscoverErrorLink'); + ).getByText(`View 10 occurrences in Discover`); - expect(wrapper.exists()).toBe(true); - expect(wrapper).toMatchSnapshot(); + expect(discoverLink).toBeInTheDocument(); }); it('should render a Summary', () => { const errorGroup = { occurrencesCount: 10, error: { + service: { + name: 'opbeans-python', + }, error: {}, timestamp: { us: 0, @@ -59,11 +84,14 @@ describe('DetailView', () => { } as any, transaction: undefined, }; - const wrapper = shallow( + + const rendered = renderWithMockContext( - ).find('Summary'); + ); - expect(wrapper.exists()).toBe(true); + expect( + rendered.getByText('1337 minutes ago (mocking 0)') + ).toBeInTheDocument(); }); it('should render tabs', () => { @@ -79,12 +107,14 @@ describe('DetailView', () => { user: {}, } as any, }; - const wrapper = shallow( + + const rendered = renderWithMockContext( - ).find('EuiTabs'); + ); + + expect(rendered.getByText('Exception stack trace')).toBeInTheDocument(); - expect(wrapper.exists()).toBe(true); - expect(wrapper).toMatchSnapshot(); + expect(rendered.getByText('Metadata')).toBeInTheDocument(); }); it('should render TabContent', () => { @@ -92,19 +122,23 @@ describe('DetailView', () => { occurrencesCount: 10, transaction: undefined, error: { + service: { + name: 'opbeans-python', + }, timestamp: { us: 0, }, - error: {}, + error: { + exception: [{ handled: true }], + }, context: {}, } as any, }; - const wrapper = shallow( + const rendered = renderWithMockContext( - ).find('TabContent'); + ); - expect(wrapper.exists()).toBe(true); - expect(wrapper).toMatchSnapshot(); + expect(rendered.getByText('No stack trace available.')).toBeInTheDocument(); }); it('should render without http request info', () => { @@ -115,16 +149,20 @@ describe('DetailView', () => { timestamp: { us: 0, }, + error: { + exception: [{ handled: true }], + }, http: { response: { status_code: 404 } }, url: { full: 'myUrl' }, service: { name: 'myService' }, user: { id: 'myUserId' }, - error: { exception: { handled: true } }, transaction: { id: 'myTransactionId', sampled: true }, } as any, }; expect(() => - shallow() + renderWithMockContext( + + ) ).not.toThrowError(); }); }); diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx index 03c866dcc7f06..220f276f62152 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx @@ -6,7 +6,10 @@ */ import { + EuiFlexGroup, + EuiFlexItem, EuiIcon, + EuiLink, EuiPanel, EuiSpacer, EuiTab, @@ -38,13 +41,12 @@ import { logStacktraceTab, } from './error_tabs'; import { ExceptionStacktrace } from './exception_stacktrace'; - -const HeaderContainer = euiStyled.div` - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: ${({ theme }) => theme.eui.euiSize}; -`; +import { useApmRouter } from '../../../../hooks/use_apm_router'; +import { useApmParams } from '../../../../hooks/use_apm_params'; +import { ERROR_GROUP_ID } from '../../../../../common/elasticsearch_fieldnames'; +import { TraceSearchType } from '../../../../../common/trace_explorer'; +import { TransactionTab } from '../../transaction_details/waterfall_with_summary/transaction_tabs'; +import { useTraceExplorerEnabledSetting } from '../../../../hooks/use_trace_explorer_enabled_setting'; const TransactionLinkName = euiStyled.div` margin-left: ${({ theme }) => theme.eui.euiSizeS}; @@ -73,6 +75,15 @@ export function DetailView({ errorGroup, urlParams, kuery }: Props) { const { detailTab, offset, comparisonEnabled } = urlParams; + const router = useApmRouter(); + + const isTraceExplorerEnabled = useTraceExplorerEnabledSetting(); + + const { + path: { groupId }, + query, + } = useApmParams('/services/{serviceName}/errors/{groupId}'); + if (!error) { return null; } @@ -85,30 +96,72 @@ export function DetailView({ errorGroup, urlParams, kuery }: Props) { const method = error.http?.request?.method; const status = error.http?.response?.status_code; + const traceExplorerLink = router.link('/traces/explorer', { + query: { + ...query, + query: `${ERROR_GROUP_ID}:${groupId}`, + type: TraceSearchType.kql, + traceId: '', + transactionId: '', + waterfallItemId: '', + detailTab: TransactionTab.timeline, + }, + }); + return ( - - -

- {i18n.translate( - 'xpack.apm.errorGroupDetails.errorOccurrenceTitle', - { - defaultMessage: 'Error occurrence', - } - )} -

-
- - {i18n.translate( - 'xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel', - { - defaultMessage: - 'View {occurrencesCount} {occurrencesCount, plural, one {occurrence} other {occurrences}} in Discover.', - values: { occurrencesCount }, - } - )} - -
+ + + +

+ {i18n.translate( + 'xpack.apm.errorGroupDetails.errorOccurrenceTitle', + { + defaultMessage: 'Error occurrence', + } + )} +

+
+
+ {isTraceExplorerEnabled && ( + + + + + + + + {i18n.translate( + 'xpack.apm.errorGroupDetails.viewOccurrencesInTraceExplorer', + { + defaultMessage: 'Explore traces with this error', + } + )} + + + + + )} + + + + + + + + {i18n.translate( + 'xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel', + { + defaultMessage: + 'View {occurrencesCount} {occurrencesCount, plural, one {occurrence} other {occurrences}} in Discover', + values: { occurrencesCount }, + } + )} + + + + +
{ +const getStyle = ( + theme: EuiTheme, + isTraceExplorerEnabled: boolean +): cytoscape.Stylesheet[] => { const lineColor = theme.eui.euiColorMediumShade; return [ { @@ -211,6 +214,20 @@ const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => { 'target-arrow-color': theme.eui.euiColorDarkShade, }, }, + ...(isTraceExplorerEnabled + ? [ + { + selector: 'edge.hover', + style: { + width: 4, + 'z-index': zIndexEdgeHover, + 'line-color': theme.eui.euiColorDarkShade, + 'source-arrow-color': theme.eui.euiColorDarkShade, + 'target-arrow-color': theme.eui.euiColorDarkShade, + }, + }, + ] + : []), { selector: 'node.hover', style: { @@ -256,10 +273,11 @@ ${theme.eui.euiColorLightShade}`, }); export const getCytoscapeOptions = ( - theme: EuiTheme + theme: EuiTheme, + isTraceExplorerEnabled: boolean ): cytoscape.CytoscapeOptions => ({ boxSelectionEnabled: false, maxZoom: 3, minZoom: 0.2, - style: getStyle(theme), + style: getStyle(theme, isTraceExplorerEnabled), }); diff --git a/x-pack/plugins/apm/public/components/app/service_map/popover/backend_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/popover/backend_contents.tsx index ffff0fa9d56d0..3c6b0f5a2d6f7 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/popover/backend_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/popover/backend_contents.tsx @@ -11,6 +11,7 @@ import { TypeOf } from '@kbn/typed-react-router-config'; import { METRIC_TYPE } from '@kbn/analytics'; import React from 'react'; import { useUiTracker } from '@kbn/observability-plugin/public'; +import { NodeDataDefinition } from 'cytoscape'; import { ContentsProps } from '.'; import { useAnyOfApmParams } from '../../../../hooks/use_apm_params'; import { useApmRouter } from '../../../../hooks/use_apm_router'; @@ -27,11 +28,13 @@ const INITIAL_STATE: Partial = { }; export function BackendContents({ - nodeData, + elementData, environment, start, end, }: ContentsProps) { + const nodeData = elementData as NodeDataDefinition; + const { query } = useAnyOfApmParams( '/service-map', '/services/{serviceName}/service-map' diff --git a/x-pack/plugins/apm/public/components/app/service_map/popover/edge_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/popover/edge_contents.tsx new file mode 100644 index 0000000000000..4bcc8bb8ca53b --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_map/popover/edge_contents.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; +import React from 'react'; +import { useUiTracker } from '@kbn/observability-plugin/public'; +import { EdgeDataDefinition } from 'cytoscape'; +import { ContentsProps } from '.'; +import { useAnyOfApmParams } from '../../../../hooks/use_apm_params'; +import { useApmRouter } from '../../../../hooks/use_apm_router'; +import { TraceSearchType } from '../../../../../common/trace_explorer'; +import { TransactionTab } from '../../transaction_details/waterfall_with_summary/transaction_tabs'; +import { + SERVICE_NAME, + SPAN_DESTINATION_SERVICE_RESOURCE, +} from '../../../../../common/elasticsearch_fieldnames'; + +export function EdgeContents({ elementData }: ContentsProps) { + const edgeData = elementData as EdgeDataDefinition; + + const { query } = useAnyOfApmParams( + '/service-map', + '/services/{serviceName}/service-map' + ); + + const apmRouter = useApmRouter(); + + const sourceService = edgeData.sourceData['service.name']; + + const trackEvent = useUiTracker(); + + let traceQuery: string; + + if (SERVICE_NAME in edgeData.targetData) { + traceQuery = + `sequence by trace.id\n` + + ` [ span where service.name == "${sourceService}" and span.destination.service.resource != null ] by span.id\n` + + ` [ transaction where service.name == "${edgeData.targetData[SERVICE_NAME]}" ] by parent.id`; + } else { + traceQuery = + `sequence by trace.id\n` + + ` [ transaction where service.name == "${sourceService}" ]\n` + + ` [ span where service.name == "${sourceService}" and span.destination.service.resource == "${edgeData.targetData[SPAN_DESTINATION_SERVICE_RESOURCE]}" ]`; + } + + const url = apmRouter.link('/traces/explorer', { + query: { + ...query, + type: TraceSearchType.eql, + query: traceQuery, + waterfallItemId: '', + traceId: '', + transactionId: '', + detailTab: TransactionTab.timeline, + }, + }); + + return ( + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click*/} + { + trackEvent({ + app: 'apm', + metricType: METRIC_TYPE.CLICK, + metric: 'service_map_to_trace_explorer', + }); + }} + > + {i18n.translate('xpack.apm.serviceMap.viewInTraceExplorer', { + defaultMessage: 'Explore traces', + })} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_map/popover/externals_list_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/popover/externals_list_contents.tsx index a564e0419046f..19a914a7a3c5a 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/popover/externals_list_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/popover/externals_list_contents.tsx @@ -13,6 +13,7 @@ import { } from '@elastic/eui'; import React, { Fragment } from 'react'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import { NodeDataDefinition } from 'cytoscape'; import { ContentsProps } from '.'; import { SPAN_DESTINATION_SERVICE_RESOURCE, @@ -26,7 +27,8 @@ const ExternalResourcesList = euiStyled.section` overflow: auto; `; -export function ExternalsListContents({ nodeData }: ContentsProps) { +export function ExternalsListContents({ elementData }: ContentsProps) { + const nodeData = elementData as NodeDataDefinition; return ( diff --git a/x-pack/plugins/apm/public/components/app/service_map/popover/index.tsx b/x-pack/plugins/apm/public/components/app/service_map/popover/index.tsx index 937ad89293a7c..78543fa18cb7b 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/popover/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/popover/index.tsx @@ -28,32 +28,47 @@ import { } from '../../../../../common/elasticsearch_fieldnames'; import { Environment } from '../../../../../common/environment_rt'; import { useTheme } from '../../../../hooks/use_theme'; +import { useTraceExplorerEnabledSetting } from '../../../../hooks/use_trace_explorer_enabled_setting'; import { CytoscapeContext } from '../cytoscape'; import { getAnimationOptions, popoverWidth } from '../cytoscape_options'; import { BackendContents } from './backend_contents'; +import { EdgeContents } from './edge_contents'; import { ExternalsListContents } from './externals_list_contents'; import { ResourceContents } from './resource_contents'; import { ServiceContents } from './service_contents'; -function getContentsComponent(selectedNodeData: cytoscape.NodeDataDefinition) { +function getContentsComponent( + selectedElementData: + | cytoscape.NodeDataDefinition + | cytoscape.EdgeDataDefinition, + isTraceExplorerEnabled: boolean +) { if ( - selectedNodeData.groupedConnections && - Array.isArray(selectedNodeData.groupedConnections) + selectedElementData.groupedConnections && + Array.isArray(selectedElementData.groupedConnections) ) { return ExternalsListContents; } - if (selectedNodeData[SERVICE_NAME]) { + if (selectedElementData[SERVICE_NAME]) { return ServiceContents; } - if (selectedNodeData[SPAN_TYPE] === 'resource') { + if (selectedElementData[SPAN_TYPE] === 'resource') { return ResourceContents; } + if ( + isTraceExplorerEnabled && + selectedElementData.source && + selectedElementData.target + ) { + return EdgeContents; + } + return BackendContents; } export interface ContentsProps { - nodeData: cytoscape.NodeDataDefinition; + elementData: cytoscape.NodeDataDefinition | cytoscape.ElementDataDefinition; environment: Environment; kuery: string; start: string; @@ -78,19 +93,26 @@ export function Popover({ }: PopoverProps) { const theme = useTheme(); const cy = useContext(CytoscapeContext); - const [selectedNode, setSelectedNode] = useState< - cytoscape.NodeSingular | undefined + const [selectedElement, setSelectedElement] = useState< + cytoscape.NodeSingular | cytoscape.EdgeSingular | undefined >(undefined); const deselect = useCallback(() => { if (cy) { cy.elements().unselect(); } - setSelectedNode(undefined); - }, [cy, setSelectedNode]); - const renderedHeight = selectedNode?.renderedHeight() ?? 0; - const renderedWidth = selectedNode?.renderedWidth() ?? 0; - const { x, y } = selectedNode?.renderedPosition() ?? { x: -10000, y: -10000 }; - const isOpen = !!selectedNode; + setSelectedElement(undefined); + }, [cy, setSelectedElement]); + + const isTraceExplorerEnabled = useTraceExplorerEnabledSetting(); + + const renderedHeight = selectedElement?.renderedHeight() ?? 0; + const renderedWidth = selectedElement?.renderedWidth() ?? 0; + const box = selectedElement?.renderedBoundingBox({}); + + const x = box ? box.x1 + box.w / 2 : -10000; + const y = box ? box.y1 + box.h / 2 : -10000; + + const isOpen = !!selectedElement; const triggerStyle: CSSProperties = { background: 'transparent', height: renderedHeight, @@ -99,20 +121,20 @@ export function Popover({ }; const trigger =
; const zoom = cy?.zoom() ?? 1; - const height = selectedNode?.height() ?? 0; + const height = selectedElement?.height() ?? 0; const translateY = y - ((zoom + 1) * height) / 4; const popoverStyle: CSSProperties = { position: 'absolute', transform: `translate(${x}px, ${translateY}px)`, }; - const selectedNodeData = selectedNode?.data() ?? {}; + const selectedElementData = selectedElement?.data() ?? {}; const popoverRef = useRef(null); - const selectedNodeId = selectedNodeData.id; + const selectedElementId = selectedElementData.id; // Set up Cytoscape event handlers useEffect(() => { const selectHandler: cytoscape.EventHandler = (event) => { - setSelectedNode(event.target); + setSelectedElement(event.target); }; if (cy) { @@ -120,6 +142,10 @@ export function Popover({ cy.on('unselect', 'node', deselect); cy.on('viewport', deselect); cy.on('drag', 'node', deselect); + if (isTraceExplorerEnabled) { + cy.on('select', 'edge', selectHandler); + cy.on('unselect', 'edge', deselect); + } } return () => { @@ -128,9 +154,11 @@ export function Popover({ cy.removeListener('unselect', 'node', deselect); cy.removeListener('viewport', undefined, deselect); cy.removeListener('drag', 'node', deselect); + cy.removeListener('select', 'edge', selectHandler); + cy.removeListener('unselect', 'edge', deselect); } }; - }, [cy, deselect]); + }, [cy, deselect, isTraceExplorerEnabled]); // Handle positioning of popover. This makes it so the popover positions // itself correctly and the arrows are always pointing to where they should. @@ -146,20 +174,23 @@ export function Popover({ if (cy) { cy.animate({ ...getAnimationOptions(theme), - center: { eles: cy.getElementById(selectedNodeId) }, + center: { eles: cy.getElementById(selectedElementId) }, }); } }, - [cy, selectedNodeId, theme] + [cy, selectedElementId, theme] ); - const isAlreadyFocused = focusedServiceName === selectedNodeId; + const isAlreadyFocused = focusedServiceName === selectedElementId; const onFocusClick = isAlreadyFocused ? centerSelectedNode : (_event: MouseEvent) => deselect(); - const ContentsComponent = getContentsComponent(selectedNodeData); + const ContentsComponent = getContentsComponent( + selectedElementData, + isTraceExplorerEnabled + ); return (

- {selectedNodeData.label ?? selectedNodeId} + {selectedElementData.label ?? selectedElementId}

{ + if (start && end) { + return callApmApi('GET /internal/apm/traces', { + params: { + query: { + environment, + kuery, + start, + end, + }, + }, + }); + } + }, + [environment, kuery, start, end] + ); + + return ( + <> + + + {fallbackToTransactions && ( + + + + + + )} + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx b/x-pack/plugins/apm/public/components/app/top_traces_overview/trace_list.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx rename to x-pack/plugins/apm/public/components/app/top_traces_overview/trace_list.tsx index 5fbaae7411ff7..0d1914d7de455 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx +++ b/x-pack/plugins/apm/public/components/app/top_traces_overview/trace_list.tsx @@ -16,6 +16,7 @@ import { asTransactionRate, } from '../../../../common/utils/formatters'; import { useApmParams } from '../../../hooks/use_apm_params'; +import { FetcherResult, FETCH_STATUS } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/create_call_apm_api'; import { truncate } from '../../../utils/style'; import { EmptyMessage } from '../../shared/empty_message'; @@ -26,19 +27,17 @@ import { ServiceLink } from '../../shared/service_link'; import { TruncateWithTooltip } from '../../shared/truncate_with_tooltip'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; -type TraceGroup = APIReturnType<'GET /internal/apm/traces'>['items'][0]; - const StyledTransactionLink = euiStyled(TransactionDetailLink)` font-size: ${({ theme }) => theme.eui.euiFontSizeS}; ${truncate('100%')}; `; interface Props { - items: TraceGroup[]; - isLoading: boolean; - isFailure: boolean; + response: FetcherResult>; } +type TraceGroup = Required['data']['items'][number]; + export function getTraceListColumns({ query, }: { @@ -153,7 +152,9 @@ const noItemsMessage = ( /> ); -export function TraceList({ items = [], isLoading, isFailure }: Props) { +export function TraceList({ response }: Props) { + const { data: { items } = { items: [] }, status } = response; + const { query } = useApmParams('/traces'); const traceListColumns = useMemo( @@ -162,8 +163,8 @@ export function TraceList({ items = [], isLoading, isFailure }: Props) { ); return ( ({ + query: '', + type: TraceSearchType.kql, + }); + + const { + query: { + rangeFrom, + rangeTo, + environment, + query: queryFromUrlParams, + type: typeFromUrlParams, + traceId, + transactionId, + waterfallItemId, + detailTab, + }, + } = useApmParams('/traces/explorer'); + + const history = useHistory(); + + useEffect(() => { + setQuery({ + query: queryFromUrlParams, + type: typeFromUrlParams, + }); + }, [queryFromUrlParams, typeFromUrlParams]); + + const { start, end } = useTimeRange({ + rangeFrom, + rangeTo, + }); + + const { data: traceSamplesData, status: traceSamplesStatus } = useFetcher( + (callApmApi) => { + return callApmApi('GET /internal/apm/traces/find', { + params: { + query: { + start, + end, + environment, + query: queryFromUrlParams, + type: typeFromUrlParams, + }, + }, + }); + }, + [start, end, environment, queryFromUrlParams, typeFromUrlParams] + ); + + useEffect(() => { + const nextSample = traceSamplesData?.samples[0]; + const nextWaterfallItemId = ''; + history.replace({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + traceId: nextSample?.traceId ?? '', + transactionId: nextSample?.transactionId, + waterfallItemId: nextWaterfallItemId, + }), + }); + }, [traceSamplesData, history]); + + const { waterfall, status: waterfallStatus } = useWaterfallFetcher({ + traceId, + transactionId, + start, + end, + }); + + const isLoading = + traceSamplesStatus === FETCH_STATUS.LOADING || + waterfallStatus === FETCH_STATUS.LOADING; + + return ( + + + + + { + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + query: query.query, + type: query.type, + }), + }); + }} + onQueryChange={(nextQuery) => { + setQuery(nextQuery); + }} + /> + + + + + + + + { + push(history, { + query: { + traceId: sample.traceId, + transactionId: sample.transactionId, + waterfallItemId: '', + }, + }); + }} + onTabClick={(nextDetailTab) => { + push(history, { + query: { + detailTab: nextDetailTab, + }, + }); + }} + traceSamples={traceSamplesData?.samples ?? []} + waterfall={waterfall} + detailTab={detailTab} + waterfallItemId={waterfallItemId} + serviceName={waterfall.entryWaterfallTransaction?.doc.service.name} + /> + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/trace_explorer/trace_search_box/index.tsx b/x-pack/plugins/apm/public/components/app/trace_explorer/trace_search_box/index.tsx new file mode 100644 index 0000000000000..4951a378c03c3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/trace_explorer/trace_search_box/index.tsx @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiSelect, + EuiSelectOption, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { QueryStringInput } from '@kbn/unified-search-plugin/public'; +import React from 'react'; +import { + TraceSearchQuery, + TraceSearchType, +} from '../../../../../common/trace_explorer'; +import { useStaticDataView } from '../../../../hooks/use_static_data_view'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { EQLCodeEditorSuggestionType } from '../../../shared/eql_code_editor/constants'; +import { LazilyLoadedEQLCodeEditor } from '../../../shared/eql_code_editor/lazily_loaded_code_editor'; + +interface Props { + query: TraceSearchQuery; + message?: string; + error: boolean; + onQueryChange: (query: TraceSearchQuery) => void; + onQueryCommit: () => void; + loading: boolean; +} + +const options: EuiSelectOption[] = [ + { + value: TraceSearchType.kql, + text: i18n.translate('xpack.apm.traceSearchBox.traceSearchTypeKql', { + defaultMessage: 'KQL', + }), + }, + { + value: TraceSearchType.eql, + text: i18n.translate('xpack.apm.traceSearchBox.traceSearchTypeEql', { + defaultMessage: 'EQL', + }), + }, +]; + +export function TraceSearchBox({ + query, + onQueryChange, + onQueryCommit, + message, + error, + loading, +}: Props) { + const { unifiedSearch } = useApmPluginContext(); + const { value: dataView } = useStaticDataView(); + + return ( + + + + + + + {query.type === TraceSearchType.eql ? ( + { + onQueryChange({ + ...query, + query: value, + }); + }} + onBlur={() => { + onQueryCommit(); + }} + getSuggestions={async (request) => { + switch (request.type) { + case EQLCodeEditorSuggestionType.EventType: + return ['transaction', 'span', 'error']; + + case EQLCodeEditorSuggestionType.Field: + return ( + dataView?.fields.map((field) => field.name) ?? [] + ); + + case EQLCodeEditorSuggestionType.Value: + const field = dataView?.getFieldByName(request.field); + + if (!dataView || !field) { + return []; + } + + const suggestions: string[] = + await unifiedSearch.autocomplete.getValueSuggestions( + { + field, + indexPattern: dataView, + query: request.value, + useTimeRange: true, + method: 'terms_agg', + } + ); + + return suggestions.slice(0, 15); + } + }} + width="100%" + height="100px" + /> + ) : ( +
+ { + onQueryCommit(); + }} + disableAutoFocus + submitOnBlur + isClearable + onChange={(e) => { + onQueryChange({ + ...query, + query: String(e.query ?? ''), + }); + }} + /> + + )} +
+ + { + onQueryChange({ + query: '', + type: e.target.value as TraceSearchType, + }); + }} + options={options} + /> + +
+
+ + + + + {message} + + + + { + onQueryCommit(); + }} + iconType="search" + > + {i18n.translate('xpack.apm.traceSearchBox.refreshButton', { + defaultMessage: 'Search', + })} + + + + +
+
+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index 21ae0f9820890..c586b782fc6e2 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -4,69 +4,81 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useApmRouter } from '../../../hooks/use_apm_router'; import { useApmParams } from '../../../hooks/use_apm_params'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { APIReturnType } from '../../../services/rest/create_call_apm_api'; -import { SearchBar } from '../../shared/search_bar'; -import { TraceList } from './trace_list'; -import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; -import { AggregatedTransactionsBadge } from '../../shared/aggregated_transactions_badge'; -import { useTimeRange } from '../../../hooks/use_time_range'; -import { useProgressiveFetcher } from '../../../hooks/use_progressive_fetcher'; +import { useApmRoutePath } from '../../../hooks/use_apm_route_path'; +import { TraceSearchType } from '../../../../common/trace_explorer'; +import { TransactionTab } from '../transaction_details/waterfall_with_summary/transaction_tabs'; +import { useTraceExplorerEnabledSetting } from '../../../hooks/use_trace_explorer_enabled_setting'; -type TracesAPIResponse = APIReturnType<'GET /internal/apm/traces'>; -const DEFAULT_RESPONSE: TracesAPIResponse = { - items: [], -}; +export function TraceOverview({ children }: { children: React.ReactElement }) { + const isTraceExplorerEnabled = useTraceExplorerEnabledSetting(); -export function TraceOverview() { - const { - query: { environment, kuery, rangeFrom, rangeTo }, - } = useApmParams('/traces'); - const { fallbackToTransactions } = useFallbackToTransactionsFetcher({ - kuery, - }); + const router = useApmRouter(); - const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + const { query } = useApmParams('/traces'); - const { status, data = DEFAULT_RESPONSE } = useProgressiveFetcher( - (callApmApi) => { - if (start && end) { - return callApmApi('GET /internal/apm/traces', { - params: { - query: { - environment, - kuery, - start, - end, - }, - }, - }); - } - }, - [environment, kuery, start, end] - ); + const routePath = useApmRoutePath(); - return ( - <> - + if (!isTraceExplorerEnabled) { + return children; + } - {fallbackToTransactions && ( - - - - - - )} + const explorerLink = router.link('/traces/explorer', { + query: { + comparisonEnabled: query.comparisonEnabled, + environment: query.environment, + kuery: query.kuery, + rangeFrom: query.rangeFrom, + rangeTo: query.rangeTo, + offset: query.offset, + refreshInterval: query.refreshInterval, + refreshPaused: query.refreshPaused, + query: '', + type: TraceSearchType.kql, + waterfallItemId: '', + traceId: '', + transactionId: '', + detailTab: TransactionTab.timeline, + }, + }); - - + const topTracesLink = router.link('/traces', { + query: { + comparisonEnabled: query.comparisonEnabled, + environment: query.environment, + kuery: query.kuery, + rangeFrom: query.rangeFrom, + rangeTo: query.rangeTo, + offset: query.offset, + refreshInterval: query.refreshInterval, + refreshPaused: query.refreshPaused, + }, + }); + + return ( + + + + + {i18n.translate('xpack.apm.traceOverview.topTracesTab', { + defaultMessage: 'Top traces', + })} + + + {i18n.translate('xpack.apm.traceOverview.traceExplorerTab', { + defaultMessage: 'Explorer', + })} + + + + {children} + ); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index fe551cc0e96b8..35d25e9ab406d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import React from 'react'; import { BrushEndListener, XYBrushEvent } from '@elastic/charts'; import { EuiBadge, @@ -16,6 +15,8 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; @@ -32,9 +33,14 @@ import type { TabContentProps } from '../types'; import { useWaterfallFetcher } from '../use_waterfall_fetcher'; import { WaterfallWithSummary } from '../waterfall_with_summary'; -import { useTransactionDistributionChartData } from './use_transaction_distribution_chart_data'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; +import { useApmParams } from '../../../../hooks/use_apm_params'; +import { useTimeRange } from '../../../../hooks/use_time_range'; import { HeightRetainer } from '../../../shared/height_retainer'; +import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; import { ChartTitleToolTip } from '../../correlations/chart_title_tool_tip'; +import { useTransactionDistributionChartData } from './use_transaction_distribution_chart_data'; +import { TransactionTab } from '../waterfall_with_summary/transaction_tabs'; // Enforce min height so it's consistent across all tabs on the same level // to prevent "flickering" behavior @@ -70,8 +76,28 @@ export function TransactionDistribution({ traceSamplesStatus, }: TransactionDistributionProps) { const { urlParams } = useLegacyUrlParams(); - const { waterfall, status: waterfallStatus } = useWaterfallFetcher(); + const { traceId, transactionId } = urlParams; + + const { + query: { rangeFrom, rangeTo }, + } = useApmParams('/services/{serviceName}/transactions/view'); + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + + const history = useHistory(); + const { waterfall, status: waterfallStatus } = useWaterfallFetcher({ + traceId, + transactionId, + start, + end, + }); + const { waterfallItemId, detailTab } = urlParams; + + const { + query: { environment }, + } = useApmParams('/services/{serviceName}/transactions/view'); + const { serviceName } = useApmServiceContext(); const isLoading = waterfallStatus === FETCH_STATUS.LOADING || traceSamplesStatus === FETCH_STATUS.LOADING; @@ -193,7 +219,29 @@ export function TransactionDistribution({ { + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + transactionId: sample.transactionId, + traceId: sample.traceId, + }), + }); + }} + onTabClick={(tab) => { + history.replace({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + detailTab: tab, + }), + }); + }} + serviceName={serviceName} + waterfallItemId={waterfallItemId} + detailTab={detailTab as TransactionTab | undefined} waterfall={waterfall} isLoading={isLoading} traceSamples={traceSamples} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts b/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts index 72d52ae09c9bd..793524c7e17f1 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts @@ -6,10 +6,7 @@ */ import { useMemo } from 'react'; -import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; -import { useApmParams } from '../../../hooks/use_apm_params'; import { useFetcher } from '../../../hooks/use_fetcher'; -import { useTimeRange } from '../../../hooks/use_time_range'; import { APIReturnType } from '../../../services/rest/create_call_apm_api'; import { getWaterfall } from './waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; @@ -20,16 +17,17 @@ const INITIAL_DATA: APIReturnType<'GET /internal/apm/traces/{traceId}'> = { linkedChildrenOfSpanCountBySpanId: {}, }; -export function useWaterfallFetcher() { - const { urlParams } = useLegacyUrlParams(); - const { traceId, transactionId } = urlParams; - - const { - query: { rangeFrom, rangeTo }, - } = useApmParams('/services/{serviceName}/transactions/view'); - - const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - +export function useWaterfallFetcher({ + traceId, + transactionId, + start, + end, +}: { + traceId?: string; + transactionId?: string; + start: string; + end: string; +}) { const { data = INITIAL_DATA, status, diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx index 93913aff6cb6b..f9642630766bd 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx @@ -15,38 +15,40 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import type { ApmUrlParams } from '../../../../context/url_params_context/types'; -import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; import { LoadingStatePrompt } from '../../../shared/loading_state_prompt'; import { TransactionSummary } from '../../../shared/summary/transaction_summary'; import { TransactionActionMenu } from '../../../shared/transaction_action_menu/transaction_action_menu'; import type { TraceSample } from '../../../../hooks/use_transaction_trace_samples_fetcher'; import { MaybeViewTraceLink } from './maybe_view_trace_link'; -import { TransactionTabs } from './transaction_tabs'; +import { TransactionTab, TransactionTabs } from './transaction_tabs'; import { IWaterfall } from './waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; -import { useApmParams } from '../../../../hooks/use_apm_params'; +import { Environment } from '../../../../../common/environment_rt'; interface Props { - urlParams: ApmUrlParams; waterfall: IWaterfall; isLoading: boolean; traceSamples: TraceSample[]; + environment: Environment; + onSampleClick: (sample: { transactionId: string; traceId: string }) => void; + onTabClick: (tab: string) => void; + serviceName?: string; + waterfallItemId?: string; + detailTab?: TransactionTab; } export function WaterfallWithSummary({ - urlParams, waterfall, isLoading, traceSamples, + environment, + onSampleClick, + onTabClick, + serviceName, + waterfallItemId, + detailTab, }: Props) { - const history = useHistory(); const [sampleActivePage, setSampleActivePage] = useState(0); - const { - query: { environment }, - } = useApmParams('/services/{serviceName}/transactions/view'); - useEffect(() => { setSampleActivePage(0); }, [traceSamples]); @@ -54,14 +56,7 @@ export function WaterfallWithSummary({ const goToSample = (index: number) => { setSampleActivePage(index); const sample = traceSamples[index]; - history.push({ - ...history.location, - search: fromQuery({ - ...toQuery(history.location.search), - transactionId: sample.transactionId, - traceId: sample.traceId, - }), - }); + onSampleClick(sample); }; const { entryWaterfallTransaction } = waterfall; @@ -137,7 +132,10 @@ export function WaterfallWithSummary({ diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx index ecb9fb9a3bc39..1621ea72b39a1 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx @@ -13,7 +13,8 @@ import { Transaction as ITransaction } from '../../../../../typings/es_schemas/u import { TransactionDetailLink } from '../../../shared/links/apm/transaction_detail_link'; import { IWaterfall } from './waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; import { Environment } from '../../../../../common/environment_rt'; -import { useApmParams } from '../../../../hooks/use_apm_params'; +import { useAnyOfApmParams } from '../../../../hooks/use_apm_params'; +import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; function FullTraceButton({ isLoading, @@ -48,8 +49,16 @@ export function MaybeViewTraceLink({ environment: Environment; }) { const { - query: { latencyAggregationType, comparisonEnabled, offset }, - } = useApmParams('/services/{serviceName}/transactions/view'); + query, + query: { comparisonEnabled, offset }, + } = useAnyOfApmParams( + '/services/{serviceName}/transactions/view', + '/traces/explorer' + ); + + const latencyAggregationType = + ('latencyAggregationType' in query && query.latencyAggregationType) || + LatencyAggregationType.avg; if (isLoading || !transaction) { return ; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx index aeaea322fff96..655b1c28c3dac 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx @@ -7,27 +7,32 @@ import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useHistory } from 'react-router-dom'; import { LogStream } from '@kbn/infra-plugin/public'; +import React from 'react'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import type { ApmUrlParams } from '../../../../context/url_params_context/types'; -import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; import { TransactionMetadata } from '../../../shared/metadata_table/transaction_metadata'; import { WaterfallContainer } from './waterfall_container'; import { IWaterfall } from './waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; interface Props { transaction: Transaction; - urlParams: ApmUrlParams; waterfall: IWaterfall; + detailTab?: TransactionTab; + serviceName?: string; + waterfallItemId?: string; + onTabClick: (tab: TransactionTab) => void; } -export function TransactionTabs({ transaction, urlParams, waterfall }: Props) { - const history = useHistory(); +export function TransactionTabs({ + transaction, + waterfall, + detailTab, + waterfallItemId, + serviceName, + onTabClick, +}: Props) { const tabs = [timelineTab, metadataTab, logsTab]; - const currentTab = - tabs.find(({ key }) => key === urlParams.detailTab) ?? timelineTab; + const currentTab = tabs.find(({ key }) => key === detailTab) ?? timelineTab; const TabContent = currentTab.component; return ( @@ -37,13 +42,7 @@ export function TransactionTabs({ transaction, urlParams, waterfall }: Props) { return ( { - history.replace({ - ...history.location, - search: fromQuery({ - ...toQuery(history.location.search), - detailTab: key, - }), - }); + onTabClick(key); }} isSelected={currentTab.key === key} key={key} @@ -57,7 +56,8 @@ export function TransactionTabs({ transaction, urlParams, waterfall }: Props) { @@ -65,8 +65,14 @@ export function TransactionTabs({ transaction, urlParams, waterfall }: Props) { ); } +export enum TransactionTab { + timeline = 'timeline', + metadata = 'metadata', + logs = 'logs', +} + const timelineTab = { - key: 'timeline', + key: TransactionTab.timeline, label: i18n.translate('xpack.apm.propertiesTable.tabs.timelineLabel', { defaultMessage: 'Timeline', }), @@ -74,7 +80,7 @@ const timelineTab = { }; const metadataTab = { - key: 'metadata', + key: TransactionTab.metadata, label: i18n.translate('xpack.apm.propertiesTable.tabs.metadataLabel', { defaultMessage: 'Metadata', }), @@ -82,7 +88,7 @@ const metadataTab = { }; const logsTab = { - key: 'logs', + key: TransactionTab.logs, label: i18n.translate('xpack.apm.propertiesTable.tabs.logsLabel', { defaultMessage: 'Logs', }), @@ -90,13 +96,21 @@ const logsTab = { }; function TimelineTabContent({ - urlParams, waterfall, + waterfallItemId, + serviceName, }: { - urlParams: ApmUrlParams; + waterfallItemId?: string; + serviceName?: string; waterfall: IWaterfall; }) { - return ; + return ( + + ); } function MetadataTabContent({ transaction }: { transaction: Transaction }) { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx index 71e4b6a6f1aad..2dd74aeae3eef 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx @@ -7,23 +7,24 @@ import React from 'react'; import { keyBy } from 'lodash'; -import type { ApmUrlParams } from '../../../../../context/url_params_context/types'; import { IWaterfall, WaterfallLegendType, } from './waterfall/waterfall_helpers/waterfall_helpers'; import { Waterfall } from './waterfall'; import { WaterfallLegends } from './waterfall_legends'; -import { useApmServiceContext } from '../../../../../context/apm_service/use_apm_service_context'; interface Props { - urlParams: ApmUrlParams; + waterfallItemId?: string; + serviceName?: string; waterfall: IWaterfall; } -export function WaterfallContainer({ urlParams, waterfall }: Props) { - const { serviceName } = useApmServiceContext(); - +export function WaterfallContainer({ + serviceName, + waterfallItemId, + waterfall, +}: Props) { if (!waterfall) { return null; } @@ -75,10 +76,7 @@ export function WaterfallContainer({ urlParams, waterfall }: Props) { return (
- +
); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/flyout_top_level_properties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/flyout_top_level_properties.tsx index 2a168904d4a2f..ce44f136f3555 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/flyout_top_level_properties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/flyout_top_level_properties.tsx @@ -12,8 +12,9 @@ import { TRANSACTION_NAME, } from '../../../../../../../common/elasticsearch_fieldnames'; import { getNextEnvironmentUrlParam } from '../../../../../../../common/environment_filter_values'; +import { LatencyAggregationType } from '../../../../../../../common/latency_aggregation_types'; import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; -import { useApmParams } from '../../../../../../hooks/use_apm_params'; +import { useAnyOfApmParams } from '../../../../../../hooks/use_apm_params'; import { TransactionDetailLink } from '../../../../../shared/links/apm/transaction_detail_link'; import { ServiceLink } from '../../../../../shared/service_link'; import { StickyProperties } from '../../../../../shared/sticky_properties'; @@ -23,9 +24,17 @@ interface Props { } export function FlyoutTopLevelProperties({ transaction }: Props) { - const { query } = useApmParams('/services/{serviceName}/transactions/view'); + const { query } = useAnyOfApmParams( + '/services/{serviceName}/transactions/view', + '/traces/explorer' + ); - const { latencyAggregationType, comparisonEnabled, offset } = query; + const latencyAggregationType = + ('latencyAggregationType' in query && query.latencyAggregationType) || + LatencyAggregationType.avg; + const serviceGroup = ('serviceGroup' in query && query.serviceGroup) || ''; + + const { comparisonEnabled, offset } = query; if (!transaction) { return null; @@ -45,7 +54,7 @@ export function FlyoutTopLevelProperties({ transaction }: Props) { val: ( ), diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx index 19d487ce35fb1..ecf121b777ff5 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx @@ -18,11 +18,12 @@ import { getNextEnvironmentUrlParam } from '../../../../../../../../common/envir import { NOT_AVAILABLE_LABEL } from '../../../../../../../../common/i18n'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; -import { useApmParams } from '../../../../../../../hooks/use_apm_params'; +import { useAnyOfApmParams } from '../../../../../../../hooks/use_apm_params'; import { BackendLink } from '../../../../../../shared/backend_link'; import { TransactionDetailLink } from '../../../../../../shared/links/apm/transaction_detail_link'; import { ServiceLink } from '../../../../../../shared/service_link'; import { StickyProperties } from '../../../../../../shared/sticky_properties'; +import { LatencyAggregationType } from '../../../../../../../../common/latency_aggregation_types'; interface Props { span: Span; @@ -30,9 +31,17 @@ interface Props { } export function StickySpanProperties({ span, transaction }: Props) { - const { query } = useApmParams('/services/{serviceName}/transactions/view'); - const { environment, latencyAggregationType, comparisonEnabled, offset } = - query; + const { query } = useAnyOfApmParams( + '/services/{serviceName}/transactions/view', + '/traces/explorer' + ); + const { environment, comparisonEnabled, offset } = query; + + const latencyAggregationType = + ('latencyAggregationType' in query && query.latencyAggregationType) || + LatencyAggregationType.avg; + + const serviceGroup = ('serviceGroup' in query && query.serviceGroup) || ''; const trackEvent = useUiTracker(); @@ -56,6 +65,7 @@ export function StickySpanProperties({ span, transaction }: Props) { agentName={transaction.agent.name} query={{ ...query, + serviceGroup, environment: nextEnvironment, }} serviceName={transaction.service.name} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx index d372cec9ce16d..1b16840e566cd 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx @@ -24,7 +24,7 @@ import { ColdStartBadge } from './badge/cold_start_badge'; import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers'; import { FailureBadge } from './failure_badge'; import { useApmRouter } from '../../../../../../hooks/use_apm_router'; -import { useApmParams } from '../../../../../../hooks/use_apm_params'; +import { useAnyOfApmParams } from '../../../../../../hooks/use_apm_params'; type ItemType = 'transaction' | 'span' | 'error'; @@ -258,12 +258,16 @@ function RelatedErrors({ }) { const apmRouter = useApmRouter(); const theme = useTheme(); - const { query } = useApmParams('/services/{serviceName}/transactions/view'); + const { query } = useAnyOfApmParams( + '/services/{serviceName}/transactions/view', + '/traces/explorer' + ); const href = apmRouter.link(`/services/{serviceName}/errors`, { path: { serviceName: item.doc.service.name }, query: { ...query, + serviceGroup: '', kuery: `${TRACE_ID} : "${item.doc.trace.id}" and ${TRANSACTION_ID} : "${item.doc.transaction?.id}"`, }, }); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx index 82d85891e36da..a10518ab58e4c 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx @@ -17,7 +17,6 @@ import { simpleTrace, traceChildStartBeforeParent, traceWithErrors, - urlParams as testUrlParams, } from './waterfall_container.stories.data'; import type { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context'; @@ -50,48 +49,87 @@ const stories: Meta = { }; export default stories; -export const Example: Story = ({ urlParams, waterfall }) => { - return ; +export const Example: Story = ({ + serviceName, + waterfallItemId, + waterfall, +}) => { + return ( + + ); }; Example.args = { - urlParams: testUrlParams, waterfall: getWaterfall(simpleTrace, '975c8d5bfd1dd20b'), }; -export const WithErrors: Story = ({ urlParams, waterfall }) => { - return ; +export const WithErrors: Story = ({ + serviceName, + waterfallItemId, + waterfall, +}) => { + return ( + + ); }; WithErrors.args = { - urlParams: testUrlParams, waterfall: getWaterfall(traceWithErrors, '975c8d5bfd1dd20b'), }; export const ChildStartsBeforeParent: Story = ({ - urlParams, + serviceName, + waterfallItemId, waterfall, }) => { - return ; + return ( + + ); }; ChildStartsBeforeParent.args = { - urlParams: testUrlParams, waterfall: getWaterfall(traceChildStartBeforeParent, '975c8d5bfd1dd20b'), }; -export const InferredSpans: Story = ({ urlParams, waterfall }) => { - return ; +export const InferredSpans: Story = ({ + serviceName, + waterfallItemId, + waterfall, +}) => { + return ( + + ); }; InferredSpans.args = { - urlParams: testUrlParams, waterfall: getWaterfall(inferredSpans, 'f2387d37260d00bd'), }; export const ManyChildrenWithSameLength: Story = ({ - urlParams, + serviceName, + waterfallItemId, waterfall, }) => { - return ; + return ( + + ); }; ManyChildrenWithSameLength.args = { - urlParams: testUrlParams, waterfall: getWaterfall(manyChildrenWithSameLength, '9a7f717439921d39'), }; diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index b82a44a598249..fe2491851b7ae 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -20,6 +20,7 @@ import { HeaderMenuPortal, InspectorContextProvider, } from '@kbn/observability-plugin/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { ScrollToTopOnPathChange } from '../app/main/scroll_to_top_on_path_change'; import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; import { @@ -39,6 +40,8 @@ import { TrackPageview } from './track_pageview'; import { RedirectWithDefaultEnvironment } from '../shared/redirect_with_default_environment'; import { RedirectWithOffset } from '../shared/redirect_with_offset'; +const storage = new Storage(localStorage); + export function ApmAppRoot({ apmPluginContextValue, pluginsStart, @@ -58,7 +61,7 @@ export function ApmAppRoot({ role="main" > - + diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index 5955b5bb5d909..a28f467c760fb 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -5,40 +5,49 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import { Outlet } from '@kbn/typed-react-router-config'; +import { Outlet, Route } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React, { ComponentProps } from 'react'; import { toBooleanRt } from '@kbn/io-ts-utils'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { environmentRt } from '../../../../common/environment_rt'; +import { TraceSearchType } from '../../../../common/trace_explorer'; import { BackendDetailOverview } from '../../app/backend_detail_overview'; import { BackendInventory } from '../../app/backend_inventory'; import { Breadcrumb } from '../../app/breadcrumb'; import { ServiceInventory } from '../../app/service_inventory'; import { ServiceMapHome } from '../../app/service_map'; import { TraceOverview } from '../../app/trace_overview'; +import { TraceExplorer } from '../../app/trace_explorer'; +import { TopTracesOverview } from '../../app/top_traces_overview'; import { ApmMainTemplate } from '../templates/apm_main_template'; import { RedirectToBackendOverviewRouteView } from './redirect_to_backend_overview_route_view'; import { ServiceGroupTemplate } from '../templates/service_group_template'; import { ServiceGroupsRedirect } from '../service_groups_redirect'; import { RedirectTo } from '../redirect_to'; import { offsetRt } from '../../../../common/offset_rt'; +import { TransactionTab } from '../../app/transaction_details/waterfall_with_summary/transaction_tabs'; -function page({ +function page< + TPath extends string, + TChildren extends Record | undefined = undefined +>({ path, element, + children, title, showServiceGroupSaveButton = false, }: { path: TPath; element: React.ReactElement; + children?: TChildren; title: string; showServiceGroupSaveButton?: boolean; }): Record< TPath, { element: React.ReactElement; - } + } & (TChildren extends Record ? { children: TChildren } : {}) > { return { [path]: { @@ -52,8 +61,9 @@ function page({ ), + children, }, - } as Record }>; + } as any; } function serviceGroupPage({ @@ -155,19 +165,58 @@ export const home = { element: , serviceGroupContextTab: 'service-inventory', }), - ...page({ - path: '/traces', - title: i18n.translate('xpack.apm.views.traceOverview.title', { - defaultMessage: 'Traces', - }), - element: , - }), ...serviceGroupPage({ path: '/service-map', title: ServiceMapTitle, element: , serviceGroupContextTab: 'service-map', }), + ...page({ + path: '/traces', + title: i18n.translate('xpack.apm.views.traceOverview.title', { + defaultMessage: 'Traces', + }), + element: ( + + + + ), + children: { + '/traces/explorer': { + element: , + params: t.type({ + query: t.type({ + query: t.string, + type: t.union([ + t.literal(TraceSearchType.kql), + t.literal(TraceSearchType.eql), + ]), + waterfallItemId: t.string, + traceId: t.string, + transactionId: t.string, + detailTab: t.union([ + t.literal(TransactionTab.timeline), + t.literal(TransactionTab.metadata), + t.literal(TransactionTab.logs), + ]), + }), + }), + defaults: { + query: { + query: '', + type: TraceSearchType.kql, + waterfallItemId: '', + traceId: '', + transactionId: '', + detailTab: TransactionTab.timeline, + }, + }, + }, + '/traces': { + element: , + }, + }, + }), '/backends': { element: , params: t.partial({ diff --git a/x-pack/plugins/apm/public/components/shared/date_picker/apm_date_picker.tsx b/x-pack/plugins/apm/public/components/shared/date_picker/apm_date_picker.tsx new file mode 100644 index 0000000000000..96d9ae768f352 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/date_picker/apm_date_picker.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useApmParams } from '../../../hooks/use_apm_params'; + +import { DatePicker } from '.'; +import { useTimeRangeId } from '../../../context/time_range_id/use_time_range_id'; +import { + toBoolean, + toNumber, +} from '../../../context/url_params_context/helpers'; + +export function ApmDatePicker() { + const { query } = useApmParams('/*'); + + if (!('rangeFrom' in query)) { + throw new Error('range not available in route parameters'); + } + + const { + rangeFrom, + rangeTo, + refreshPaused: refreshPausedFromUrl = 'true', + refreshInterval: refreshIntervalFromUrl = '0', + } = query; + + const refreshPaused = toBoolean(refreshPausedFromUrl); + + const refreshInterval = toNumber(refreshIntervalFromUrl); + + const { incrementTimeRangeId } = useTimeRangeId(); + + return ( + { + incrementTimeRangeId(); + }} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/eql_code_editor/completer.ts b/x-pack/plugins/apm/public/components/shared/eql_code_editor/completer.ts new file mode 100644 index 0000000000000..5128c288fefc2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/eql_code_editor/completer.ts @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Editor, IEditSession, TokenInfo as AceTokenInfo } from 'brace'; +import { Maybe } from '../../../../typings/common'; +import { EQLCodeEditorSuggestionType } from './constants'; +import { EQLToken } from './tokens'; +import { + EQLCodeEditorSuggestion, + EQLCodeEditorSuggestionCallback, + EQLCodeEditorSuggestionRequest, +} from './types'; + +type TokenInfo = AceTokenInfo & { + type: string; + index: number; +}; + +export class EQLCodeEditorCompleter { + callback?: EQLCodeEditorSuggestionCallback; + + private async getCompletionsAsync( + session: IEditSession, + position: { row: number; column: number }, + prefix: string | undefined + ): Promise { + const token = session.getTokenAt( + position.row, + position.column + ) as Maybe; + const tokensInLine = session.getTokens(position.row) as TokenInfo[]; + + function withWhitespace( + vals: EQLCodeEditorSuggestion[], + options: { + before?: string; + after?: string; + } = {} + ) { + const { after = ' ' } = options; + let { before = ' ' } = options; + + if ( + before && + (token?.value.match(/^\s+$/) || (token && token.type !== 'text')) + ) { + before = before.trimLeft(); + } + + return vals.map((val) => { + const suggestion = typeof val === 'string' ? { value: val } : val; + const valueAsString = suggestion.value; + + return { + ...suggestion, + caption: valueAsString, + value: [before, valueAsString, after].join(''), + }; + }); + } + + if ( + position.row === 0 && + (!token || token.index === 0) && + 'sequence by'.includes(prefix || '') + ) { + return withWhitespace(['sequence by'], { + before: '', + after: ' ', + }); + } + + const previousTokens = tokensInLine + .slice(0, token ? tokensInLine.indexOf(token) : tokensInLine.length) + .reverse(); + + const completedEqlToken = previousTokens.find((t) => + t.type.startsWith('eql.') + ); + + switch (completedEqlToken?.type) { + case undefined: + return [ + ...withWhitespace(['['], { before: '', after: ' ' }), + ...(position.row > 2 + ? withWhitespace(['until'], { before: '', after: ' [ ' }) + : []), + ]; + + case EQLToken.Sequence: + return withWhitespace( + await this.getExternalSuggestions({ + type: EQLCodeEditorSuggestionType.Field, + }), + { + after: '\n\t[ ', + } + ); + + case EQLToken.SequenceItemStart: + return withWhitespace( + [ + ...(await this.getExternalSuggestions({ + type: EQLCodeEditorSuggestionType.EventType, + })), + 'any', + ], + { after: ' where ' } + ); + + case EQLToken.EventType: + return withWhitespace(['where']); + + case EQLToken.Where: + case EQLToken.LogicalOperator: + return [ + ...withWhitespace( + await this.getExternalSuggestions({ + type: EQLCodeEditorSuggestionType.Field, + }) + ), + ...withWhitespace(['true', 'false'], { after: ' ]\n\t' }), + ]; + + case EQLToken.BoolCondition: + return withWhitespace([']'], { after: '\n\t' }); + + case EQLToken.Operator: + case EQLToken.InOperator: + const field = + previousTokens?.find((t) => t.type === EQLToken.Field)?.value ?? ''; + + const hasStartedValueLiteral = + !!prefix?.trim() || token?.value.trim() === '"'; + + return withWhitespace( + await this.getExternalSuggestions({ + type: EQLCodeEditorSuggestionType.Value, + field, + value: prefix ?? '', + }), + { before: hasStartedValueLiteral ? '' : ' "', after: '" ' } + ); + + case EQLToken.Value: + return [ + ...withWhitespace([']'], { after: '\n\t' }), + ...withWhitespace(['and', 'or']), + ]; + } + + return []; + } + + private async getExternalSuggestions( + request: EQLCodeEditorSuggestionRequest + ): Promise { + if (this.callback) { + return this.callback(request); + } + return []; + } + + getCompletions( + _: Editor, + session: IEditSession, + position: { row: number; column: number }, + prefix: string | undefined, + cb: (err: Error | null, suggestions?: EQLCodeEditorSuggestion[]) => void + ) { + this.getCompletionsAsync(session, position, prefix) + .then((suggestions) => { + cb( + null, + suggestions.map((sugg) => { + const suggestion = + typeof sugg === 'string' + ? { value: sugg, score: 1000 } + : { score: 1000, ...sugg }; + + return suggestion; + }) + ); + }) + .catch(cb); + } + + setSuggestionCb(cb?: EQLCodeEditorSuggestionCallback) { + this.callback = cb; + } +} diff --git a/x-pack/plugins/apm/public/components/shared/eql_code_editor/constants.ts b/x-pack/plugins/apm/public/components/shared/eql_code_editor/constants.ts new file mode 100644 index 0000000000000..5fe28cb602c49 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/eql_code_editor/constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const EQL_MODE_NAME = 'ace/mode/eql'; +export const EQL_THEME_NAME = 'ace/theme/eql'; + +export enum EQLCodeEditorSuggestionType { + EventType = 'eventType', + Field = 'field', + Value = 'value', +} diff --git a/x-pack/plugins/apm/public/components/shared/eql_code_editor/eql_highlight_rules.ts b/x-pack/plugins/apm/public/components/shared/eql_code_editor/eql_highlight_rules.ts new file mode 100644 index 0000000000000..8d04036451bb0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/eql_code_editor/eql_highlight_rules.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import 'brace/ext/language_tools'; +import { acequire } from 'brace'; +import { EQLToken } from './tokens'; + +const TextHighlightRules = acequire( + 'ace/mode/text_highlight_rules' +).TextHighlightRules; + +export class EQLHighlightRules extends TextHighlightRules { + constructor() { + super(); + + const fieldNameOrValueRegex = /((?:[^\s]+)|(?:".*?"))/; + const operatorRegex = /(:|==|>|>=|<|<=|!=)/; + + const sequenceItemEnd = { + token: EQLToken.SequenceItemEnd, + regex: /(\])/, + next: 'start', + }; + + this.$rules = { + start: [ + { + token: EQLToken.Sequence, + regex: /(sequence by)/, + next: 'field', + }, + { + token: EQLToken.SequenceItemStart, + regex: /(\[)/, + next: 'sequence_item', + }, + { + token: EQLToken.Until, + regex: /(until)/, + next: 'start', + }, + ], + field: [ + { + token: EQLToken.Field, + regex: fieldNameOrValueRegex, + next: 'start', + }, + ], + sequence_item: [ + { + token: EQLToken.EventType, + regex: fieldNameOrValueRegex, + next: 'where', + }, + ], + sequence_item_end: [sequenceItemEnd], + where: [ + { + token: EQLToken.Where, + regex: /(where)/, + next: 'condition', + }, + ], + condition: [ + { + token: EQLToken.BoolCondition, + regex: /(true|false)/, + next: 'sequence_item_end', + }, + { + token: EQLToken.Field, + regex: fieldNameOrValueRegex, + next: 'comparison_operator', + }, + ], + comparison_operator: [ + { + token: EQLToken.Operator, + regex: operatorRegex, + next: 'value_or_value_list', + }, + ], + value_or_value_list: [ + { + token: EQLToken.Value, + regex: /("([^"]+)")|([\d+\.]+)|(true|false|null)/, + next: 'logical_operator_or_sequence_item_end', + }, + { + token: EQLToken.InOperator, + regex: /(in)/, + next: 'value_list', + }, + ], + logical_operator_or_sequence_item_end: [ + { + token: EQLToken.LogicalOperator, + regex: /(and|or|not)/, + next: 'condition', + }, + sequenceItemEnd, + ], + value_list: [ + { + token: EQLToken.ValueListStart, + regex: /(\()/, + next: 'value_list_item', + }, + ], + value_list_item: [ + { + token: EQLToken.Value, + regex: fieldNameOrValueRegex, + next: 'comma', + }, + ], + comma: [ + { + token: EQLToken.Comma, + regex: /,/, + next: 'value_list_item_or_end', + }, + ], + value_list_item_or_end: [ + { + token: EQLToken.Value, + regex: fieldNameOrValueRegex, + next: 'comma', + }, + { + token: EQLToken.ValueListEnd, + regex: /\)/, + next: 'logical_operator_or_sequence_item_end', + }, + ], + }; + + this.normalizeRules(); + } +} diff --git a/x-pack/plugins/apm/public/components/shared/eql_code_editor/eql_mode.ts b/x-pack/plugins/apm/public/components/shared/eql_code_editor/eql_mode.ts new file mode 100644 index 0000000000000..36f923b3210c0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/eql_code_editor/eql_mode.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TextMode as TextModeInterface, acequire } from 'brace'; +import { EQL_MODE_NAME } from './constants'; +import { EQLHighlightRules } from './eql_highlight_rules'; + +type ITextMode = new () => TextModeInterface; + +const TextMode = acequire('ace/mode/text').Mode as ITextMode; + +export class EQLMode extends TextMode { + HighlightRules: typeof EQLHighlightRules; + $id: string; + constructor() { + super(); + this.$id = EQL_MODE_NAME; + this.HighlightRules = EQLHighlightRules; + } +} diff --git a/x-pack/plugins/apm/public/components/shared/eql_code_editor/index.tsx b/x-pack/plugins/apm/public/components/shared/eql_code_editor/index.tsx new file mode 100644 index 0000000000000..8a8cd1b0dff41 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/eql_code_editor/index.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import 'brace/ext/language_tools'; +import { last } from 'lodash'; +import React, { useRef } from 'react'; +import { EuiCodeEditor } from '@kbn/es-ui-shared-plugin/public'; +import { EQLCodeEditorCompleter } from './completer'; +import { EQL_THEME_NAME } from './constants'; +import { EQLMode } from './eql_mode'; +import './theme'; +import { EQLCodeEditorProps } from './types'; + +export function EQLCodeEditor(props: EQLCodeEditorProps) { + const { + showGutter = false, + setOptions, + getSuggestions, + ...restProps + } = props; + + const completer = useRef(new EQLCodeEditorCompleter()); + const eqlMode = useRef(new EQLMode()); + + completer.current.setSuggestionCb(getSuggestions); + + const options = { + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, + wrap: true, + ...setOptions, + }; + + return ( +
+ { + if (editor) { + editor.editor.completers = [completer.current]; + } + }} + {...restProps} + /> +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/shared/eql_code_editor/lazily_loaded_code_editor.tsx b/x-pack/plugins/apm/public/components/shared/eql_code_editor/lazily_loaded_code_editor.tsx new file mode 100644 index 0000000000000..3432331aaa062 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/eql_code_editor/lazily_loaded_code_editor.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { once } from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { EQLCodeEditorProps } from './types'; + +const loadEqlCodeEditor = once(() => import('.').then((m) => m.EQLCodeEditor)); + +type EQLCodeEditorComponentType = Awaited>; + +export function LazilyLoadedEQLCodeEditor(props: EQLCodeEditorProps) { + const [EQLCodeEditor, setEQLCodeEditor] = useState< + EQLCodeEditorComponentType | undefined + >(); + + useEffect(() => { + loadEqlCodeEditor().then((editor) => { + setEQLCodeEditor(() => { + return editor; + }); + }); + }, []); + + return EQLCodeEditor ? ( + + ) : ( + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/eql_code_editor/theme.ts b/x-pack/plugins/apm/public/components/shared/eql_code_editor/theme.ts new file mode 100644 index 0000000000000..2dfbecf63f428 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/eql_code_editor/theme.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { euiLightVars as theme } from '@kbn/ui-theme'; +import { EQL_THEME_NAME } from './constants'; + +// @ts-expect-error +ace.define( + EQL_THEME_NAME, + ['require', 'exports', 'module', 'ace/lib/dom'], + function (acequire: any, exports: any) { + exports.isDark = false; + exports.cssClass = 'ace-eql'; + exports.cssText = ` + .ace-eql .ace_scroller { + background-color: transparent; + } + .ace-eql .ace_marker-layer .ace_selection { + background: rgb(181, 213, 255); + } + .ace-eql .ace_placeholder { + color: ${theme.euiTextSubduedColor}; + padding: 0; + } + .ace-eql .ace_sequence, + .ace-eql .ace_where, + .ace-eql .ace_until { + color: ${theme.euiColorDarkShade}; + } + .ace-eql .ace_sequence_item_start, + .ace-eql .ace_sequence_item_end, + .ace-eql .ace_operator, + .ace-eql .ace_logical_operator { + color: ${theme.euiColorMediumShade}; + } + .ace-eql .ace_value, + .ace-eql .ace_bool_condition { + color: ${theme.euiColorAccent}; + } + .ace-eql .ace_event_type, + .ace-eql .ace_field { + color: ${theme.euiColorPrimaryText}; + } + // .ace-eql .ace_gutter { + // color: #333; + // } + .ace-eql .ace_print-margin { + width: 1px; + background: #e8e8e8; + } + .ace-eql .ace_fold { + background-color: #6B72E6; + } + .ace-eql .ace_cursor { + color: black; + } + .ace-eql .ace_invisible { + color: rgb(191, 191, 191); + } + .ace-eql .ace_marker-layer .ace_selection { + background: rgb(181, 213, 255); + } + .ace-eql.ace_multiselect .ace_selection.ace_start { + box-shadow: 0 0 3px 0px white; + } + .ace-eql .ace_marker-layer .ace_step { + background: rgb(252, 255, 0); + } + .ace-eql .ace_marker-layer .ace_stack { + background: rgb(164, 229, 101); + } + .ace-eql .ace_marker-layer .ace_bracket { + margin: -1px 0 0 -1px; + border: 1px solid rgb(192, 192, 192); + } + .ace-eql .ace_marker-layer .ace_selected-word { + background: rgb(250, 250, 255); + border: 1px solid rgb(200, 200, 250); + } + .ace-eql .ace_indent-guide { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y; + }`; + + const dom = acequire('../lib/dom'); + dom.importCssString(exports.cssText, exports.cssClass); + } +); diff --git a/x-pack/plugins/apm/public/components/shared/eql_code_editor/tokens.ts b/x-pack/plugins/apm/public/components/shared/eql_code_editor/tokens.ts new file mode 100644 index 0000000000000..5525d8318afaf --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/eql_code_editor/tokens.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum EQLToken { + Sequence = 'eql.sequence', + SequenceItemStart = 'eql.sequence_item_start', + SequenceItemEnd = 'eql.sequence_item_end', + Until = 'eql.until', + Field = 'eql.field', + EventType = 'eql.event_type', + Where = 'eql.where', + BoolCondition = 'eql.bool_condition', + Operator = 'eql.operator', + Value = 'eql.value', + LogicalOperator = 'eql.logical_operator', + InOperator = 'eql.in_operator', + ValueListStart = 'eql.value_list_start', + ValueListItem = 'eql.value_list_item', + ValueListEnd = 'eql.value_list_end', + Comma = 'eql.comma', +} diff --git a/x-pack/plugins/apm/public/components/shared/eql_code_editor/types.ts b/x-pack/plugins/apm/public/components/shared/eql_code_editor/types.ts new file mode 100644 index 0000000000000..250cda155ea18 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/eql_code_editor/types.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCodeEditorProps } from '@kbn/es-ui-shared-plugin/public'; +import { EQLCodeEditorSuggestionType } from './constants'; + +export type EQLCodeEditorSuggestion = + | string + | { value: string; score?: number }; + +export type EQLCodeEditorSuggestionRequest = + | { + type: + | EQLCodeEditorSuggestionType.EventType + | EQLCodeEditorSuggestionType.Field; + } + | { type: EQLCodeEditorSuggestionType.Value; field: string; value: string }; + +export type EQLCodeEditorSuggestionCallback = ( + request: EQLCodeEditorSuggestionRequest +) => Promise; + +export type EQLCodeEditorProps = Omit< + EuiCodeEditorProps, + 'mode' | 'theme' | 'setOptions' +> & { + getSuggestions?: EQLCodeEditorSuggestionCallback; + setOptions?: EuiCodeEditorProps['setOptions']; +}; diff --git a/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_link.tsx b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_link.tsx index 4df9bc0447aa1..e052688d73474 100644 --- a/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/links/discover_links/discover_link.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import rison, { RisonValue } from 'rison-node'; import url from 'url'; -import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../common/index_pattern_constants'; +import { APM_STATIC_DATA_VIEW_ID } from '../../../../../common/data_view_constants'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { getTimepickerRisonData } from '../rison_helpers'; @@ -46,7 +46,7 @@ export const getDiscoverHref = ({ _g: getTimepickerRisonData(location.search), _a: { ...query._a, - index: APM_STATIC_INDEX_PATTERN_ID, + index: APM_STATIC_DATA_VIEW_ID, }, }; diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index f1d82e2e307e1..c71e21bee2c38 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -13,11 +13,8 @@ import { EuiSpacer, } from '@elastic/eui'; import React from 'react'; -import { useTimeRangeId } from '../../context/time_range_id/use_time_range_id'; -import { toBoolean, toNumber } from '../../context/url_params_context/helpers'; -import { useApmParams } from '../../hooks/use_apm_params'; import { useBreakpoints } from '../../hooks/use_breakpoints'; -import { DatePicker } from './date_picker'; +import { ApmDatePicker } from './date_picker/apm_date_picker'; import { KueryBar } from './kuery_bar'; import { TimeComparison } from './time_comparison'; import { TransactionTypeSelect } from './transaction_type_select'; @@ -31,39 +28,6 @@ interface Props { kueryBarBoolFilter?: QueryDslQueryContainer[]; } -function ApmDatePicker() { - const { query } = useApmParams('/*'); - - if (!('rangeFrom' in query)) { - throw new Error('range not available in route parameters'); - } - - const { - rangeFrom, - rangeTo, - refreshPaused: refreshPausedFromUrl = 'true', - refreshInterval: refreshIntervalFromUrl = '0', - } = query; - - const refreshPaused = toBoolean(refreshPausedFromUrl); - - const refreshInterval = toNumber(refreshIntervalFromUrl); - - const { incrementTimeRangeId } = useTimeRangeId(); - - return ( - { - incrementTimeRangeId(); - }} - /> - ); -} - export function SearchBar({ hidden = false, showKueryBar = true, diff --git a/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx index 67209c23324ad..a8f5ab1b149fb 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx @@ -11,6 +11,8 @@ import type { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/pu import { MapsStartApi } from '@kbn/maps-plugin/public'; import { ObservabilityPublicStart } from '@kbn/observability-plugin/public'; import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { ApmPluginSetupDeps } from '../../plugin'; import { ConfigSchema } from '../..'; @@ -22,6 +24,8 @@ export interface ApmPluginContextValue { plugins: ApmPluginSetupDeps & { maps?: MapsStartApi }; observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; observability: ObservabilityPublicStart; + dataViews: DataViewsPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; } export const ApmPluginContext = createContext({} as ApmPluginContextValue); diff --git a/x-pack/plugins/apm/public/hooks/use_apm_route_path.ts b/x-pack/plugins/apm/public/hooks/use_apm_route_path.ts new file mode 100644 index 0000000000000..b659e26610490 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_apm_route_path.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useRoutePath, PathsOf } from '@kbn/typed-react-router-config'; +import { ApmRoutes } from '../components/routing/apm_route_config'; + +export function useApmRoutePath() { + const path = useRoutePath(); + + return path as PathsOf; +} diff --git a/x-pack/plugins/apm/public/hooks/use_static_data_view.ts b/x-pack/plugins/apm/public/hooks/use_static_data_view.ts new file mode 100644 index 0000000000000..4281f7cac83c8 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_static_data_view.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import useAsync from 'react-use/lib/useAsync'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; +import { APM_STATIC_DATA_VIEW_ID } from '../../common/data_view_constants'; + +export function useStaticDataView() { + const { dataViews } = useApmPluginContext(); + + return useAsync(() => dataViews.get(APM_STATIC_DATA_VIEW_ID)); +} diff --git a/x-pack/plugins/apm/public/hooks/use_trace_explorer_enabled_setting.ts b/x-pack/plugins/apm/public/hooks/use_trace_explorer_enabled_setting.ts new file mode 100644 index 0000000000000..51fd5f32875da --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_trace_explorer_enabled_setting.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { apmTraceExplorerTab } from '@kbn/observability-plugin/common'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; + +export function useTraceExplorerEnabledSetting() { + const { core } = useApmPluginContext(); + + return core.uiSettings.get(apmTraceExplorerTab, false); +} diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 2ee87571cb719..53f74b99c486e 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -47,6 +47,7 @@ import type { import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import { enableServiceGroups } from '@kbn/observability-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { registerApmAlerts } from './components/alerting/register_apm_alerts'; import { getApmEnrollmentFlyoutData, @@ -88,6 +89,8 @@ export interface ApmPluginStartDeps { fleet?: FleetStart; security?: SecurityPluginStart; spaces?: SpacesPluginStart; + dataViews: DataViewsPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; } const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', { diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts index 36d0de8ccde96..241814e433b62 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts @@ -36,7 +36,7 @@ export async function callAsyncWithDebug({ requestParams: Record; operationName: string; isCalledWithInternalUser: boolean; // only allow inspection of queries that were retrieved with credentials of the end user -}) { +}): Promise { if (!debug) { return cb(); } diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts index 1156930018991..5075d7dfc1237 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts @@ -11,10 +11,10 @@ export function cancelEsRequestOnAbort>( promise: T, request: KibanaRequest, controller: AbortController -) { +): T { const subscription = request.events.aborted$.subscribe(() => { controller.abort(); }); - return promise.finally(() => subscription.unsubscribe()); + return promise.finally(() => subscription.unsubscribe()) as T; } diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index 0fcf02f95ac0e..f8613bf8c9e6f 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -6,6 +6,7 @@ */ import type { + EqlSearchRequest, TermsEnumRequest, TermsEnumResponse, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; @@ -30,7 +31,10 @@ import { getDebugTitle, } from '../call_async_with_debug'; import { cancelEsRequestOnAbort } from '../cancel_es_request_on_abort'; -import { unpackProcessorEvents } from './unpack_processor_events'; +import { + unpackProcessorEvents, + processorEventsToIndex, +} from './unpack_processor_events'; export type APMEventESSearchRequest = Omit & { apm: { @@ -46,6 +50,10 @@ export type APMEventESTermsEnumRequest = Omit & { apm: { events: ProcessorEvent[] }; }; +export type APMEventEqlSearchRequest = Omit & { + apm: { events: ProcessorEvent[] }; +}; + // These keys shoul all be `ProcessorEvent.x`, but until TypeScript 4.2 we're inlining them here. // See https://github.com/microsoft/TypeScript/issues/37888 type TypeOfProcessorEvent = { @@ -114,7 +122,7 @@ export class APMEventClient { this.esClient.search(searchParams, { signal: controller.signal, meta: true, - }), + }) as Promise, this.request, controller ); @@ -139,6 +147,48 @@ export class APMEventClient { }); } + async eqlSearch(operationName: string, params: APMEventEqlSearchRequest) { + const requestType = 'eql_search'; + const index = processorEventsToIndex(params.apm.events, this.indices); + + return callAsyncWithDebug({ + cb: () => { + const { apm, ...rest } = params; + + const eqlSearchPromise = withApmSpan(operationName, () => { + const controller = new AbortController(); + return cancelEsRequestOnAbort( + this.esClient.eql.search( + { + index, + ...rest, + }, + { signal: controller.signal, meta: true } + ), + this.request, + controller + ); + }); + + return unwrapEsResponse(eqlSearchPromise); + }, + getDebugMessage: () => ({ + body: getDebugBody({ + params, + requestType, + operationName, + }), + title: getDebugTitle(this.request), + }), + isCalledWithInternalUser: false, + debug: this.debug, + request: this.request, + requestType, + operationName, + requestParams: params, + }); + } + async termsEnum( operationName: string, params: APMEventESTermsEnumRequest diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts index 3fc6b40f9cfe7..3ef5a715e2c20 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -9,7 +9,6 @@ import { uniq, defaultsDeep, cloneDeep } from 'lodash'; import { ESSearchRequest, ESFilter } from '@kbn/core/types/elasticsearch'; import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../../common/processor_event'; -import { APMEventESSearchRequest, APMEventESTermsEnumRequest } from '.'; import { ApmIndicesConfig } from '../../../../routes/settings/apm_indices/get_apm_indices'; const processorEventIndexMap = { @@ -21,13 +20,24 @@ const processorEventIndexMap = { [ProcessorEvent.profile]: 'transaction', } as const; +export function processorEventsToIndex( + events: ProcessorEvent[], + indices: ApmIndicesConfig +) { + return uniq(events.map((event) => indices[processorEventIndexMap[event]])); +} + export function unpackProcessorEvents( - request: APMEventESSearchRequest | APMEventESTermsEnumRequest, + request: { + apm: { + events: ProcessorEvent[]; + }; + }, indices: ApmIndicesConfig ) { const { apm, ...params } = request; const events = uniq(apm.events); - const index = events.map((event) => indices[processorEventIndexMap[event]]); + const index = processorEventsToIndex(events, indices); const withFilterForProcessorEvent: ESSearchRequest & { body: { query: { bool: { filter: ESFilter[] } } }; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index 522fd5c078af3..900f9afe66653 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -74,7 +74,11 @@ export async function createInternalESClient({ ): Promise> => { return callEs(operationName, { requestType: 'search', - cb: (signal) => asInternalUser.search(params, { signal, meta: true }), + cb: (signal) => + asInternalUser.search(params, { + signal, + meta: true, + }) as Promise<{ body: any }>, params, }); }, diff --git a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts index 9f22eaa6b9c30..4a0878a5dc135 100644 --- a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts +++ b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsErrorHelpers } from '@kbn/core/server'; -import { APM_STATIC_INDEX_PATTERN_ID } from '../../../common/index_pattern_constants'; +import { APM_STATIC_DATA_VIEW_ID } from '../../../common/data_view_constants'; import { hasHistoricalAgentData } from '../historical_data/has_historical_agent_data'; import { Setup } from '../../lib/helpers/setup_request'; import { APMRouteHandlerResources } from '../typings'; @@ -55,7 +55,7 @@ export async function createStaticDataView({ 'index-pattern', getApmDataViewAttributes(apmDataViewTitle), { - id: APM_STATIC_INDEX_PATTERN_ID, + id: APM_STATIC_DATA_VIEW_ID, overwrite: forceOverwrite, namespace: spaceId, } @@ -86,7 +86,7 @@ async function getForceOverwrite({ const existingDataView = await savedObjectsClient.get( 'index-pattern', - APM_STATIC_INDEX_PATTERN_ID + APM_STATIC_DATA_VIEW_ID ); // if the existing data view does not matches the new one, force an update diff --git a/x-pack/plugins/apm/server/routes/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/routes/service_map/transform_service_map_responses.ts index 0cdbdc26c69df..4d4522461252b 100644 --- a/x-pack/plugins/apm/server/routes/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/routes/service_map/transform_service_map_responses.ts @@ -115,7 +115,7 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { ? anomalies.serviceAnomalies.find( (item) => item.serviceName === serviceName ) - : null; + : undefined; if (matchedServiceNodes.length) { return { @@ -158,9 +158,16 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { const sourceData = getConnectionNode(connection.source); const targetData = getConnectionNode(connection.destination); + const label = + sourceData[SERVICE_NAME] + + ' to ' + + (targetData[SERVICE_NAME] || + targetData[SPAN_DESTINATION_SERVICE_RESOURCE]); + return { source: sourceData.id, target: targetData.id, + label, id: getConnectionId({ source: sourceData, destination: targetData }), sourceData, targetData, diff --git a/x-pack/plugins/apm/server/routes/traces/get_trace_samples_by_query.ts b/x-pack/plugins/apm/server/routes/traces/get_trace_samples_by_query.ts new file mode 100644 index 0000000000000..48ca80780d77f --- /dev/null +++ b/x-pack/plugins/apm/server/routes/traces/get_trace_samples_by_query.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + rangeQuery, + kqlQuery, + termsQuery, +} from '@kbn/observability-plugin/server'; +import { Environment } from '../../../common/environment_rt'; +import { Setup } from '../../lib/helpers/setup_request'; +import { TraceSearchType } from '../../../common/trace_explorer'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { environmentQuery } from '../../../common/utils/environment_query'; +import { + PARENT_ID, + PROCESSOR_EVENT, + TRACE_ID, + TRANSACTION_ID, + TRANSACTION_SAMPLED, +} from '../../../common/elasticsearch_fieldnames'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; + +export async function getTraceSamplesByQuery({ + setup, + start, + end, + environment, + query, + type, +}: { + setup: Setup; + start: number; + end: number; + environment: Environment; + query: string; + type: TraceSearchType; +}) { + const size = 500; + + let traceIds: string[] = []; + + if (type === TraceSearchType.kql) { + traceIds = + ( + await setup.apmEventClient.search('get_trace_ids_by_kql_query', { + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.span, + ProcessorEvent.error, + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(query), + ], + }, + }, + aggs: { + traceId: { + terms: { + field: TRACE_ID, + execution_hint: 'map', + size, + }, + }, + }, + }, + }) + ).aggregations?.traceId.buckets.map((bucket) => bucket.key as string) ?? + []; + } else if (type === TraceSearchType.eql) { + traceIds = + ( + await setup.apmEventClient.eqlSearch('get_trace_ids_by_eql_query', { + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.span, + ProcessorEvent.error, + ], + }, + body: { + size: 1000, + filter: { + bool: { + filter: [ + ...rangeQuery(start, end), + ...environmentQuery(environment), + ], + }, + }, + event_category_field: PROCESSOR_EVENT, + query, + }, + filter_path: 'hits.sequences.events._source.trace.id', + }) + ).hits?.sequences?.flatMap((sequence) => + sequence.events.map( + (event) => (event._source as { trace: { id: string } }).trace.id + ) + ) ?? []; + } + + if (!traceIds.length) { + return []; + } + + const traceSamplesResponse = await setup.apmEventClient.search( + 'get_trace_samples_by_trace_ids', + { + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { + [TRANSACTION_SAMPLED]: true, + }, + }, + ...termsQuery(TRACE_ID, ...traceIds), + ...rangeQuery(start, end), + ], + must_not: [{ exists: { field: PARENT_ID } }], + }, + }, + aggs: { + transactionId: { + terms: { + field: TRANSACTION_ID, + size, + }, + aggs: { + latest: { + top_metrics: { + metrics: asMutableArray([{ field: TRACE_ID }] as const), + size: 1, + sort: { + '@timestamp': 'desc' as const, + }, + }, + }, + }, + }, + }, + }, + } + ); + + return ( + traceSamplesResponse.aggregations?.transactionId.buckets.map((bucket) => ({ + traceId: bucket.latest.top[0].metrics['trace.id'] as string, + transactionId: bucket.key as string, + })) ?? [] + ); +} diff --git a/x-pack/plugins/apm/server/routes/traces/route.ts b/x-pack/plugins/apm/server/routes/traces/route.ts index afca332fea0b5..f47e85778e16d 100644 --- a/x-pack/plugins/apm/server/routes/traces/route.ts +++ b/x-pack/plugins/apm/server/routes/traces/route.ts @@ -6,9 +6,9 @@ */ import * as t from 'io-ts'; +import { TraceSearchType } from '../../../common/trace_explorer'; import { setupRequest } from '../../lib/helpers/setup_request'; -import { getTraceItems } from './get_trace_items'; -import { getTopTracesPrimaryStats } from './get_top_traces_primary_stats'; +import { getSearchAggregatedTransactions } from '../../lib/helpers/transactions'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { environmentRt, @@ -16,9 +16,11 @@ import { probabilityRt, rangeRt, } from '../default_api_types'; -import { getSearchAggregatedTransactions } from '../../lib/helpers/transactions'; -import { getRootTransactionByTraceId } from '../transactions/get_transaction_by_trace'; import { getTransaction } from '../transactions/get_transaction'; +import { getRootTransactionByTraceId } from '../transactions/get_transaction_by_trace'; +import { getTopTracesPrimaryStats } from './get_top_traces_primary_stats'; +import { getTraceItems } from './get_trace_items'; +import { getTraceSamplesByQuery } from './get_trace_samples_by_query'; const tracesRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/traces', @@ -135,9 +137,50 @@ const transactionByIdRoute = createApmServerRoute({ }, }); +const findTracesRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/traces/find', + params: t.type({ + query: t.intersection([ + rangeRt, + environmentRt, + t.type({ + query: t.string, + type: t.union([ + t.literal(TraceSearchType.kql), + t.literal(TraceSearchType.eql), + ]), + }), + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async ( + resources + ): Promise<{ + samples: Array<{ traceId: string; transactionId: string }>; + }> => { + const { start, end, environment, query, type } = resources.params.query; + + const setup = await setupRequest(resources); + + return { + samples: await getTraceSamplesByQuery({ + setup, + start, + end, + environment, + query, + type, + }), + }; + }, +}); + export const traceRouteRepository = { ...tracesByIdRoute, ...tracesRoute, ...rootTransactionByTraceIdRoute, ...transactionByIdRoute, + ...findTracesRoute, }; diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index 682d1979e12c7..e211e708c8ab3 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -13,7 +13,7 @@ import { } from '@kbn/home-plugin/server'; import { CloudSetup } from '@kbn/cloud-plugin/server'; import { APMConfig } from '..'; -import { APM_STATIC_INDEX_PATTERN_ID } from '../../common/index_pattern_constants'; +import { APM_STATIC_DATA_VIEW_ID } from '../../common/data_view_constants'; import { getApmDataViewAttributes } from '../routes/data_view/get_apm_data_view_attributes'; import { getApmDataViewTitle } from '../routes/data_view/get_apm_data_view_title'; import { ApmIndicesConfig } from '../routes/settings/apm_indices/get_apm_indices'; @@ -42,7 +42,7 @@ export const tutorialProvider = const dataViewTitle = getApmDataViewTitle(apmIndices); const savedObjects = [ { - id: APM_STATIC_INDEX_PATTERN_ID, + id: APM_STATIC_DATA_VIEW_ID, attributes: getApmDataViewAttributes(dataViewTitle), type: 'index-pattern', }, diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index e01b9ba3f9922..b380fa6c80745 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -17,6 +17,7 @@ export { defaultApmServiceEnvironment, apmServiceInventoryOptimizedSorting, apmProgressiveLoading, + apmTraceExplorerTab, } from './ui_settings_keys'; export { diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index 287fe541cc7b6..c49f95053bb8a 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -15,3 +15,4 @@ export const apmProgressiveLoading = 'observability:apmProgressiveLoading'; export const enableServiceGroups = 'observability:enableServiceGroups'; export const apmServiceInventoryOptimizedSorting = 'observability:apmServiceInventoryOptimizedSorting'; +export const apmTraceExplorerTab = 'observability:apmTraceExplorerTab'; diff --git a/x-pack/plugins/observability/common/utils/get_inspect_response.ts b/x-pack/plugins/observability/common/utils/get_inspect_response.ts index eaf467cf4c95e..dbd0cd68736db 100644 --- a/x-pack/plugins/observability/common/utils/get_inspect_response.ts +++ b/x-pack/plugins/observability/common/utils/get_inspect_response.ts @@ -71,7 +71,7 @@ function getStats({ }, }; - if (esResponse?.hits) { + if (esResponse?.hits?.hits) { stats.hits = { label: i18n.translate('xpack.observability.inspector.stats.hitsLabel', { defaultMessage: 'Hits', diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index 5b21b07d1cea3..67a7a6bf0bd54 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -19,6 +19,7 @@ import { enableServiceGroups, apmServiceInventoryOptimizedSorting, enableNewSyntheticsView, + apmTraceExplorerTab, } from '../common/ui_settings_keys'; const technicalPreviewLabel = i18n.translate( @@ -187,4 +188,19 @@ export const uiSettings: Record[${technicalPreviewLabel}]` }, + }), + schema: schema.boolean(), + value: false, + requiresPageReload: true, + type: 'boolean', + }, }; diff --git a/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts b/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts index 0e5fb6d1c98fd..0f265343b9d01 100644 --- a/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts +++ b/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts @@ -7,7 +7,7 @@ import { apm, ApmSynthtraceEsClient, timerange } from '@elastic/apm-synthtrace'; import expect from '@kbn/expect'; -import { APM_STATIC_INDEX_PATTERN_ID } from '@kbn/apm-plugin/common/index_pattern_constants'; +import { APM_STATIC_DATA_VIEW_ID } from '@kbn/apm-plugin/common/data_view_constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { SupertestReturnType } from '../../common/apm_api_supertest'; @@ -26,13 +26,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { function deleteDataView() { // return supertest.delete('/api/saved_objects//').set('kbn-xsrf', 'foo').expect(200) return supertest - .delete(`/api/saved_objects/index-pattern/${APM_STATIC_INDEX_PATTERN_ID}`) + .delete(`/api/saved_objects/index-pattern/${APM_STATIC_DATA_VIEW_ID}`) .set('kbn-xsrf', 'foo') .expect(200); } function getDataView() { - return supertest.get(`/api/saved_objects/index-pattern/${APM_STATIC_INDEX_PATTERN_ID}`); + return supertest.get(`/api/saved_objects/index-pattern/${APM_STATIC_DATA_VIEW_ID}`); } function getDataViewSuggestions(field: string) { diff --git a/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts b/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts new file mode 100644 index 0000000000000..ebbdbda5e2e1e --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts @@ -0,0 +1,307 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { apm, timerange } from '@elastic/apm-synthtrace'; +import expect from '@kbn/expect'; +import { TraceSearchType } from '@kbn/apm-plugin/common/trace_explorer'; +import { Environment } from '@kbn/apm-plugin/common/environment_rt'; +import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; +import { sortBy } from 'lodash'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { ApmApiError } from '../../common/apm_api_supertest'; + +type Instance = ReturnType['instance']>; +type Transaction = ReturnType; + +export default function ApiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + const apmApiClient = getService('apmApiClient'); + const synthtraceEsClient = getService('synthtraceEsClient'); + + const start = new Date('2022-01-01T00:00:00.000Z').getTime(); + const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; + + // for EQL sequences to work, events need a slight time offset, + // as ES will sort based on @timestamp. to acommodate this offset + // we also add a little bit of a buffer to the requested time range + const endWithOffset = end + 100000; + + async function fetchTraceSamples({ + query, + type, + environment, + }: { + query: string; + type: TraceSearchType; + environment: Environment; + }) { + return apmApiClient.readUser({ + endpoint: `GET /internal/apm/traces/find`, + params: { + query: { + query, + type, + start: new Date(start).toISOString(), + end: new Date(endWithOffset).toISOString(), + environment, + }, + }, + }); + } + + function fetchTraces(samples: Array<{ traceId: string; transactionId: string }>) { + if (!samples.length) { + return []; + } + + return Promise.all( + samples.map(async ({ traceId }) => { + const response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/traces/{traceId}`, + params: { + path: { traceId }, + query: { + start: new Date(start).toISOString(), + end: new Date(endWithOffset).toISOString(), + }, + }, + }); + return response.body.traceDocs; + }) + ); + } + + registry.when( + 'Find traces when traces do not exist', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + it('handles empty state', async () => { + const response = await fetchTraceSamples({ + query: '', + type: TraceSearchType.kql, + environment: ENVIRONMENT_ALL.value, + }); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ + samples: [], + }); + }); + } + ); + + registry.when( + 'Find traces when traces exist', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + before(() => { + const java = apm.service('java', 'production', 'java').instance('java'); + + const node = apm.service('node', 'development', 'nodejs').instance('node'); + + const python = apm.service('python', 'production', 'python').instance('python'); + + function generateTrace( + timestamp: number, + order: Instance[], + db?: 'elasticsearch' | 'redis' + ) { + return order + .concat() + .reverse() + .reduce((prev, instance, index) => { + const invertedIndex = order.length - index - 1; + + const duration = 50; + const time = timestamp + invertedIndex * 10; + + const transaction: Transaction = instance + .transaction(`GET /${instance.fields['service.name']!}/api`) + .timestamp(time) + .duration(duration); + + if (prev) { + const next = order[invertedIndex + 1].fields['service.name']!; + transaction.children( + instance + .span(`GET ${next}/api`, 'external', 'http') + .destination(next) + .duration(duration) + .timestamp(time + 1) + .children(prev) + ); + } else if (db) { + transaction.children( + instance + .span(db, 'db', db) + .destination(db) + .duration(duration) + .timestamp(time + 1) + ); + } + + return transaction; + }, undefined)!; + } + + return synthtraceEsClient.index( + timerange(start, end) + .interval('15m') + .rate(1) + .generator((timestamp) => { + return [ + generateTrace(timestamp, [java, node]), + generateTrace(timestamp, [node, java], 'redis'), + generateTrace(timestamp, [python], 'redis'), + generateTrace(timestamp, [python, node, java], 'elasticsearch'), + generateTrace(timestamp, [java, python, node]), + ]; + }) + ); + }); + + describe('when using KQL', () => { + describe('and the query is empty', () => { + it('returns all trace samples', async () => { + const { + body: { samples }, + } = await fetchTraceSamples({ + query: '', + type: TraceSearchType.kql, + environment: 'ENVIRONMENT_ALL', + }); + + expect(samples.length).to.eql(5); + }); + }); + + describe('and query is set', () => { + it('returns the relevant traces', async () => { + const { + body: { samples }, + } = await fetchTraceSamples({ + query: 'span.destination.service.resource:elasticsearch', + type: TraceSearchType.kql, + environment: 'ENVIRONMENT_ALL', + }); + + expect(samples.length).to.eql(1); + }); + }); + }); + + describe('when using EQL', () => { + describe('and the query is invalid', () => { + it.skip('returns a 400', async function () { + try { + await fetchTraceSamples({ + query: '', + type: TraceSearchType.eql, + environment: 'ENVIRONMENT_ALL', + }); + this.fail(); + } catch (error: unknown) { + const apiError = error as ApmApiError; + expect(apiError.res.status).to.eql(400); + } + }); + }); + + describe('and the query is set', () => { + it('returns the correct trace samples for transaction sequences', async () => { + const { + body: { samples }, + } = await fetchTraceSamples({ + query: `sequence by trace.id + [ transaction where service.name == "java" ] + [ transaction where service.name == "node" ]`, + type: TraceSearchType.eql, + environment: 'ENVIRONMENT_ALL', + }); + + const traces = await fetchTraces(samples); + + expect(traces.length).to.eql(2); + + const mapped = traces.map((traceDocs) => { + return sortBy(traceDocs, '@timestamp') + .filter((doc) => doc.processor.event === 'transaction') + .map((doc) => doc.service.name); + }); + + expect(mapped).to.eql([ + ['java', 'node'], + ['java', 'python', 'node'], + ]); + }); + }); + + it('returns the correct trace samples for join sequences', async () => { + const { + body: { samples }, + } = await fetchTraceSamples({ + query: `sequence by trace.id + [ span where service.name == "java" ] by span.id + [ transaction where service.name == "python" ] by parent.id`, + type: TraceSearchType.eql, + environment: 'ENVIRONMENT_ALL', + }); + + const traces = await fetchTraces(samples); + + expect(traces.length).to.eql(1); + + const mapped = traces.map((traceDocs) => { + return sortBy(traceDocs, '@timestamp') + .filter((doc) => doc.processor.event === 'transaction') + .map((doc) => doc.service.name); + }); + + expect(mapped).to.eql([['java', 'python', 'node']]); + }); + + it('returns the correct trace samples for exit spans', async () => { + const { + body: { samples }, + } = await fetchTraceSamples({ + query: `sequence by trace.id + [ transaction where service.name == "python" ] + [ span where span.destination.service.resource == "redis" ]`, + type: TraceSearchType.eql, + environment: 'ENVIRONMENT_ALL', + }); + + const traces = await fetchTraces(samples); + + expect(traces.length).to.eql(1); + + const mapped = traces.map((traceDocs) => { + return sortBy(traceDocs, '@timestamp') + .filter( + (doc) => doc.processor.event === 'transaction' || doc.processor.event === 'span' + ) + .map((doc) => { + if (doc.span && 'destination' in doc.span) { + return doc.span.destination!.service.resource; + } + return doc.service.name; + }); + }); + + expect(mapped).to.eql([['python', 'redis']]); + }); + }); + + after(() => synthtraceEsClient.clean()); + } + ); +} From 52e51f6d1c356be0d2f93865ebad8f6d3458465f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Mon, 23 May 2022 15:27:54 +0200 Subject: [PATCH 084/120] [Stack Monitoring] Convert enterprise search routes to TypeScript (#132649) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../http_api/enterprise_search/index.ts} | 2 +- .../post_enterprise_search_overview.ts | 30 +++++++++++++++ .../server/lib/details/get_metrics.ts | 11 +++--- .../get_enterprise_search_for_clusters.ts | 15 ++++---- .../server/lib/enterprise_search/get_stats.ts | 12 ++++-- .../routes/api/v1/enterprise_search/index.ts | 13 +++++++ ...set_overview.js => metric_set_overview.ts} | 4 +- .../{overview.js => overview.ts} | 38 +++++++++---------- .../monitoring/server/routes/api/v1/index.ts | 1 + .../monitoring/server/routes/api/v1/ui.ts | 2 - .../plugins/monitoring/server/routes/index.ts | 2 + 11 files changed, 90 insertions(+), 40 deletions(-) rename x-pack/plugins/monitoring/{server/routes/api/v1/enterprise_search/index.js => common/http_api/enterprise_search/index.ts} (82%) create mode 100644 x-pack/plugins/monitoring/common/http_api/enterprise_search/post_enterprise_search_overview.ts create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/index.ts rename x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/{metric_set_overview.js => metric_set_overview.ts} (93%) rename x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/{overview.js => overview.ts} (55%) diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/index.js b/x-pack/plugins/monitoring/common/http_api/enterprise_search/index.ts similarity index 82% rename from x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/index.js rename to x-pack/plugins/monitoring/common/http_api/enterprise_search/index.ts index 4eb6af5bb116d..372cadb4b9a8f 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/index.js +++ b/x-pack/plugins/monitoring/common/http_api/enterprise_search/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { entSearchOverviewRoute } from './overview'; +export * from './post_enterprise_search_overview'; diff --git a/x-pack/plugins/monitoring/common/http_api/enterprise_search/post_enterprise_search_overview.ts b/x-pack/plugins/monitoring/common/http_api/enterprise_search/post_enterprise_search_overview.ts new file mode 100644 index 0000000000000..9f264bc7fbb27 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/enterprise_search/post_enterprise_search_overview.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { ccsRT, clusterUuidRT, timeRangeRT } from '../shared'; + +export const postEnterpriseSearchOverviewRequestParamsRT = rt.type({ + clusterUuid: clusterUuidRT, +}); + +export const postEnterpriseSearchOverviewRequestPayloadRT = rt.intersection([ + rt.partial({ + ccs: ccsRT, + }), + rt.type({ + timeRange: timeRangeRT, + }), +]); + +export type PostEnterpriseSearchOverviewRequestPayload = rt.TypeOf< + typeof postEnterpriseSearchOverviewRequestPayloadRT +>; + +export const postEnterpriseSearchOverviewResponsePayloadRT = rt.type({ + // TODO: add payload entries +}); diff --git a/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts b/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts index f7e65efa74737..57d0928a7b9f4 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts +++ b/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts @@ -6,12 +6,13 @@ */ import moment from 'moment'; -import { checkParam } from '../error_missing_required'; -import { getSeries } from './get_series'; +import { INDEX_PATTERN_TYPES } from '../../../common/constants'; +import { TimeRange } from '../../../common/http_api/shared'; +import { LegacyRequest } from '../../types'; import { calculateTimeseriesInterval } from '../calculate_timeseries_interval'; +import { checkParam } from '../error_missing_required'; import { getTimezone } from '../get_timezone'; -import { LegacyRequest } from '../../types'; -import { INDEX_PATTERN_TYPES } from '../../../common/constants'; +import { getSeries } from './get_series'; export interface NamedMetricDescriptor { keys: string | string[]; @@ -30,7 +31,7 @@ export function isNamedMetricDescriptor( // TODO: Switch to an options object argument here export async function getMetrics( - req: LegacyRequest, + req: LegacyRequest, moduleType: INDEX_PATTERN_TYPES, metricSet: MetricDescriptor[] = [], filters: Array> = [], diff --git a/x-pack/plugins/monitoring/server/lib/enterprise_search/get_enterprise_search_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/enterprise_search/get_enterprise_search_for_clusters.ts index f530912562976..d83b8e73c703e 100644 --- a/x-pack/plugins/monitoring/server/lib/enterprise_search/get_enterprise_search_for_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/enterprise_search/get_enterprise_search_for_clusters.ts @@ -5,17 +5,18 @@ * 2.0. */ +import { TimeRange } from '../../../common/http_api/shared'; import { ElasticsearchResponse } from '../../../common/types/es'; -import { LegacyRequest, Cluster } from '../../types'; -import { createEnterpriseSearchQuery } from './create_enterprise_search_query'; +import { Globals } from '../../static_globals'; +import { Cluster, LegacyRequest } from '../../types'; +import { getLegacyIndexPattern } from '../cluster/get_index_patterns'; import { EnterpriseSearchMetric } from '../metrics'; +import { createEnterpriseSearchQuery } from './create_enterprise_search_query'; import { entSearchAggFilterPath, entSearchAggResponseHandler, entSearchUuidsAgg, } from './_enterprise_search_stats'; -import { getLegacyIndexPattern } from '../cluster/get_index_patterns'; -import { Globals } from '../../static_globals'; function handleResponse(clusterUuid: string, response: ElasticsearchResponse) { const stats = entSearchAggResponseHandler(response); @@ -27,12 +28,12 @@ function handleResponse(clusterUuid: string, response: ElasticsearchResponse) { } export function getEnterpriseSearchForClusters( - req: LegacyRequest, + req: LegacyRequest, clusters: Cluster[], ccs: string ) { - const start = req.payload.timeRange.min; - const end = req.payload.timeRange.max; + const start = req.payload.timeRange?.min; + const end = req.payload.timeRange?.max; const config = req.server.config; const maxBucketSize = config.ui.max_bucket_size; diff --git a/x-pack/plugins/monitoring/server/lib/enterprise_search/get_stats.ts b/x-pack/plugins/monitoring/server/lib/enterprise_search/get_stats.ts index afe315f6a3ba7..725f5b953a2ef 100644 --- a/x-pack/plugins/monitoring/server/lib/enterprise_search/get_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/enterprise_search/get_stats.ts @@ -6,18 +6,22 @@ */ import moment from 'moment'; +import { TimeRange } from '../../../common/http_api/shared'; import { ElasticsearchResponse } from '../../../common/types/es'; +import { Globals } from '../../static_globals'; import { LegacyRequest } from '../../types'; +import { getLegacyIndexPattern } from '../cluster/get_index_patterns'; import { createEnterpriseSearchQuery } from './create_enterprise_search_query'; import { entSearchAggFilterPath, - entSearchUuidsAgg, entSearchAggResponseHandler, + entSearchUuidsAgg, } from './_enterprise_search_stats'; -import { getLegacyIndexPattern } from '../cluster/get_index_patterns'; -import { Globals } from '../../static_globals'; -export async function getStats(req: LegacyRequest, clusterUuid: string) { +export async function getStats( + req: LegacyRequest, + clusterUuid: string +) { const config = req.server.config; const start = moment.utc(req.payload.timeRange.min).valueOf(); const end = moment.utc(req.payload.timeRange.max).valueOf(); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/index.ts new file mode 100644 index 0000000000000..440fe1404af56 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MonitoringCore } from '../../../../types'; +import { entSearchOverviewRoute } from './overview'; + +export function registerV1EnterpriseSearchRoutes(server: MonitoringCore) { + entSearchOverviewRoute(server); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/metric_set_overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/metric_set_overview.ts similarity index 93% rename from x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/metric_set_overview.js rename to x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/metric_set_overview.ts index 4bec4bc3948c5..8d23a3981a320 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/metric_set_overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/metric_set_overview.ts @@ -5,7 +5,9 @@ * 2.0. */ -export const metricSet = [ +import { MetricDescriptor } from '../../../../lib/details/get_metrics'; + +export const metricSet: MetricDescriptor[] = [ // Low level usage metrics { name: 'enterprise_search_heap', diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.ts similarity index 55% rename from x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.js rename to x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.ts index e3e269d8b3148..6581b52655cee 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.ts @@ -5,31 +5,29 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { + postEnterpriseSearchOverviewRequestParamsRT, + postEnterpriseSearchOverviewRequestPayloadRT, + postEnterpriseSearchOverviewResponsePayloadRT, +} from '../../../../../common/http_api/enterprise_search'; +import { createValidationFunction } from '../../../../lib/create_route_validation_function'; import { getMetrics } from '../../../../lib/details/get_metrics'; -import { metricSet } from './metric_set_overview'; -import { handleError } from '../../../../lib/errors'; import { getStats } from '../../../../lib/enterprise_search'; +import { handleError } from '../../../../lib/errors'; +import { MonitoringCore } from '../../../../types'; +import { metricSet } from './metric_set_overview'; + +export function entSearchOverviewRoute(server: MonitoringCore) { + const validateParams = createValidationFunction(postEnterpriseSearchOverviewRequestParamsRT); + const validateBody = createValidationFunction(postEnterpriseSearchOverviewRequestPayloadRT); -export function entSearchOverviewRoute(server) { server.route({ - method: 'POST', + method: 'post', path: '/api/monitoring/v1/clusters/{clusterUuid}/enterprise_search', - config: { - validate: { - params: schema.object({ - clusterUuid: schema.string(), - }), - body: schema.object({ - ccs: schema.maybe(schema.string()), - timeRange: schema.object({ - min: schema.string(), - max: schema.string(), - }), - }), - }, + validate: { + params: validateParams, + body: validateBody, }, - async handler(req) { const clusterUuid = req.params.clusterUuid; try { @@ -40,7 +38,7 @@ export function entSearchOverviewRoute(server) { }), ]); - return { stats, metrics }; + return postEnterpriseSearchOverviewResponsePayloadRT.encode({ stats, metrics }); } catch (err) { return handleError(err, req); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/index.ts index e0f5e55c6c128..b59e98006816e 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/index.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/index.ts @@ -12,5 +12,6 @@ export { registerV1CheckAccessRoutes } from './check_access'; export { registerV1ClusterRoutes } from './cluster'; export { registerV1ElasticsearchRoutes } from './elasticsearch'; export { registerV1ElasticsearchSettingsRoutes } from './elasticsearch_settings'; +export { registerV1EnterpriseSearchRoutes } from './enterprise_search'; export { registerV1LogstashRoutes } from './logstash'; export { registerV1SetupRoutes } from './setup'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/ui.ts b/x-pack/plugins/monitoring/server/routes/api/v1/ui.ts index 7aaa6591e868e..eb76cc12d78a4 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/ui.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/ui.ts @@ -10,5 +10,3 @@ // @ts-expect-error export { kibanaInstanceRoute, kibanaInstancesRoute, kibanaOverviewRoute } from './kibana'; -// @ts-expect-error -export { entSearchOverviewRoute } from './enterprise_search'; diff --git a/x-pack/plugins/monitoring/server/routes/index.ts b/x-pack/plugins/monitoring/server/routes/index.ts index f38612d5a42da..c16b806bfb93a 100644 --- a/x-pack/plugins/monitoring/server/routes/index.ts +++ b/x-pack/plugins/monitoring/server/routes/index.ts @@ -17,6 +17,7 @@ import { registerV1ClusterRoutes, registerV1ElasticsearchRoutes, registerV1ElasticsearchSettingsRoutes, + registerV1EnterpriseSearchRoutes, registerV1LogstashRoutes, registerV1SetupRoutes, } from './api/v1'; @@ -45,6 +46,7 @@ export function requireUIRoutes( registerV1ClusterRoutes(server); registerV1ElasticsearchRoutes(server); registerV1ElasticsearchSettingsRoutes(server, npRoute); + registerV1EnterpriseSearchRoutes(server); registerV1LogstashRoutes(server); registerV1SetupRoutes(server); } From c23412b1b3022f5507dc25266da4655a21b041e7 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 23 May 2022 09:33:52 -0400 Subject: [PATCH 085/120] [Security Solution] Default Event Filters advanced policy fields and migration (#132418) * [Security Solution] Default Event Filters advanced policy fields and migration * change field name * update option names * fix typo * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * pr comments Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../fleet/server/saved_objects/index.ts | 3 +- .../migrations/security_solution/index.ts | 1 + .../security_solution/to_v8_3_0.test.ts | 155 ++++++++++++++++++ .../migrations/security_solution/to_v8_3_0.ts | 42 +++++ .../saved_objects/migrations/to_v8_3_0.ts | 17 ++ .../policy/models/advanced_policy_schema.ts | 30 ++++ 6 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_3_0.test.ts create mode 100644 x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_3_0.ts diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index edcf2ed751f3e..009cdef6aa771 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -38,7 +38,7 @@ import { migratePackagePolicyToV7150 } from './migrations/to_v7_15_0'; import { migrateInstallationToV7160, migratePackagePolicyToV7160 } from './migrations/to_v7_16_0'; import { migrateInstallationToV800, migrateOutputToV800 } from './migrations/to_v8_0_0'; import { migratePackagePolicyToV820 } from './migrations/to_v8_2_0'; -import { migrateInstallationToV830 } from './migrations/to_v8_3_0'; +import { migrateInstallationToV830, migratePackagePolicyToV830 } from './migrations/to_v8_3_0'; /* * Saved object types and mappings @@ -210,6 +210,7 @@ const getSavedObjectTypes = ( '7.15.0': migratePackagePolicyToV7150, '7.16.0': migratePackagePolicyToV7160, '8.2.0': migratePackagePolicyToV820, + '8.3.0': migratePackagePolicyToV830, }, }, [PACKAGES_SAVED_OBJECT_TYPE]: { diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/index.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/index.ts index 16c87ab3a69ee..02456661674a0 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/index.ts @@ -12,3 +12,4 @@ export { migrateEndpointPackagePolicyToV7140 } from './to_v7_14_0'; export { migratePackagePolicyToV7150 } from './to_v7_15_0'; export { migratePackagePolicyToV7160 } from './to_v7_16_0'; export { migratePackagePolicyToV820 } from './to_v8_2_0'; +export { migratePackagePolicyToV830 } from './to_v8_3_0'; diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_3_0.test.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_3_0.test.ts new file mode 100644 index 0000000000000..d5eb8c5a86547 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_3_0.test.ts @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from '@kbn/core/server'; + +import type { PackagePolicy } from '../../../../common'; + +import { migratePackagePolicyToV830 as migration } from './to_v8_3_0'; + +describe('8.3.0 Endpoint Package Policy migration', () => { + const policyDoc = ({ windowsAdvanced = {}, macAdvanced = {}, linuxAdvanced = {} }) => { + return { + id: 'mock-saved-object-id', + attributes: { + name: 'Some Policy Name', + package: { + name: 'endpoint', + title: '', + version: '', + }, + id: 'endpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { + value: { + windows: { + ...windowsAdvanced, + }, + mac: { + ...macAdvanced, + }, + linux: { + ...linuxAdvanced, + }, + }, + }, + }, + }, + ], + }, + type: ' nested', + }; + }; + + it('adds advanced event filters defaulted to false', () => { + const initialDoc = policyDoc({}); + + const migratedDoc = policyDoc({ + windowsAdvanced: { advanced: { event_filters: { default: false } } }, + macAdvanced: { advanced: { event_filters: { default: false } } }, + linuxAdvanced: { advanced: { event_filters: { default: false } } }, + }); + + expect(migration(initialDoc, {} as SavedObjectMigrationContext)).toEqual(migratedDoc); + }); + + it('adds advanced event filters defaulted to false and preserves existing advanced fields', () => { + const initialDoc = policyDoc({ + windowsAdvanced: { advanced: { existingAdvanced: true } }, + macAdvanced: { advanced: { existingAdvanced: true } }, + linuxAdvanced: { advanced: { existingAdvanced: true } }, + }); + + const migratedDoc = policyDoc({ + windowsAdvanced: { advanced: { event_filters: { default: false }, existingAdvanced: true } }, + macAdvanced: { advanced: { event_filters: { default: false }, existingAdvanced: true } }, + linuxAdvanced: { advanced: { event_filters: { default: false }, existingAdvanced: true } }, + }); + + expect(migration(initialDoc, {} as SavedObjectMigrationContext)).toEqual(migratedDoc); + }); + + it('does not modify non-endpoint package policies', () => { + const doc: SavedObjectUnsanitizedDoc = { + id: 'mock-saved-object-id', + attributes: { + name: 'Some Policy Name', + package: { + name: 'notEndpoint', + title: '', + version: '', + }, + id: 'notEndpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'notEndpoint', + enabled: true, + streams: [], + config: {}, + }, + ], + }, + type: ' nested', + }; + + expect( + migration(doc, {} as SavedObjectMigrationContext) as SavedObjectUnsanitizedDoc + ).toEqual({ + attributes: { + name: 'Some Policy Name', + package: { + name: 'notEndpoint', + title: '', + version: '', + }, + id: 'notEndpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'notEndpoint', + enabled: true, + streams: [], + config: {}, + }, + ], + }, + type: ' nested', + id: 'mock-saved-object-id', + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_3_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_3_0.ts new file mode 100644 index 0000000000000..ade6ec9ab1565 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_3_0.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from '@kbn/core/server'; +import { cloneDeep } from 'lodash'; + +import type { PackagePolicy } from '../../../../common'; + +export const migratePackagePolicyToV830: SavedObjectMigrationFn = ( + packagePolicyDoc +) => { + if (packagePolicyDoc.attributes.package?.name !== 'endpoint') { + return packagePolicyDoc; + } + + const updatedPackagePolicyDoc: SavedObjectUnsanitizedDoc = + cloneDeep(packagePolicyDoc); + + const input = updatedPackagePolicyDoc.attributes.inputs[0]; + + if (input && input.config) { + const policy = input.config.policy.value; + + const migratedPolicy = { event_filters: { default: false } }; + + policy.windows.advanced = policy.windows.advanced + ? { ...policy.windows.advanced, ...migratedPolicy } + : { ...migratedPolicy }; + policy.mac.advanced = policy.mac.advanced + ? { ...policy.mac.advanced, ...migratedPolicy } + : { ...migratedPolicy }; + policy.linux.advanced = policy.linux.advanced + ? { ...policy.linux.advanced, ...migratedPolicy } + : { ...migratedPolicy }; + } + + return updatedPackagePolicyDoc; +}; diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_3_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_3_0.ts index 843427f3cf862..b8fbeb2b0ab3a 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_3_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_3_0.ts @@ -7,8 +7,11 @@ import type { SavedObjectMigrationFn } from '@kbn/core/server'; +import type { PackagePolicy } from '../../../common'; import type { Installation } from '../../../common'; +import { migratePackagePolicyToV830 as SecSolMigratePackagePolicyToV830 } from './security_solution'; + export const migrateInstallationToV830: SavedObjectMigrationFn = ( installationDoc, migrationContext @@ -17,3 +20,17 @@ export const migrateInstallationToV830: SavedObjectMigrationFn = ( + packagePolicyDoc, + migrationContext +) => { + let updatedPackagePolicyDoc = packagePolicyDoc; + + // Endpoint specific migrations + if (packagePolicyDoc.attributes.package?.name === 'endpoint') { + updatedPackagePolicyDoc = SecSolMigratePackagePolicyToV830(packagePolicyDoc, migrationContext); + } + + return updatedPackagePolicyDoc; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts index 7f86540e36426..df13b8ba43f06 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -884,4 +884,34 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ } ), }, + { + key: 'linux.advanced.event_filter.default', + first_supported_version: '8.3', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.event_filter.default', + { + defaultMessage: 'Download default event filter rules from Elastic. Default: true', + } + ), + }, + { + key: 'mac.advanced.event_filter.default', + first_supported_version: '8.3', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.event_filter.default', + { + defaultMessage: 'Download default event filter rules from Elastic. Default: true', + } + ), + }, + { + key: 'windows.advanced.event_filter.default', + first_supported_version: '8.3', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.event_filter.default', + { + defaultMessage: 'Download default event filter rules from Elastic. Default: true', + } + ), + }, ]; From 0beb268616003a07286c97b40b916bb198c405ff Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 23 May 2022 15:34:45 +0200 Subject: [PATCH 086/120] [data.query] implement `PersistableState` interface for `QueryState` (#132623) --- src/plugins/data/common/index.ts | 1 + .../query/filters/persistable_state.test.ts | 40 ++++++++ .../common/query/filters/persistable_state.ts | 65 ++++++++++++ src/plugins/data/common/query/index.ts | 1 + .../common/query/persistable_state.test.ts | 45 +++++++-- .../data/common/query/persistable_state.ts | 98 +++++++++++-------- src/plugins/data/common/query/query_state.ts | 26 +++++ .../search_source/search_source_service.ts | 2 +- src/plugins/data/public/index.ts | 1 + .../query/filter_manager/filter_manager.ts | 19 ++-- src/plugins/data/public/query/mocks.ts | 17 ++++ .../data/public/query/query_service.test.ts | 8 ++ .../data/public/query/query_service.ts | 86 +++++++++++++--- src/plugins/data/public/query/query_state.ts | 13 +-- .../create_query_state_observable.ts | 4 +- .../data/public/query/state_sync/index.ts | 1 + .../data/server/query/query_service.ts | 30 ++++-- .../server/query/route_handler_context.ts | 2 +- .../server/saved_objects/migrations/query.ts | 2 +- 19 files changed, 359 insertions(+), 102 deletions(-) create mode 100644 src/plugins/data/common/query/filters/persistable_state.test.ts create mode 100644 src/plugins/data/common/query/filters/persistable_state.ts create mode 100644 src/plugins/data/common/query/query_state.ts diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index 8944aa7db2843..aa4da04d2e198 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -74,6 +74,7 @@ export { isQuery, isTimeRange, } from './query'; +export type { QueryState } from './query'; export * from './search'; export type { RefreshInterval, diff --git a/src/plugins/data/common/query/filters/persistable_state.test.ts b/src/plugins/data/common/query/filters/persistable_state.test.ts new file mode 100644 index 0000000000000..9ce3e2536a70a --- /dev/null +++ b/src/plugins/data/common/query/filters/persistable_state.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { extract, inject } from './persistable_state'; +import { Filter } from '@kbn/es-query'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; + +describe('filter manager persistable state tests', () => { + const filters: Filter[] = [ + { meta: { alias: 'test', disabled: false, negate: false, index: 'test' } }, + ]; + describe('reference injection', () => { + test('correctly inserts reference to filter', () => { + const updatedFilters = inject(filters, [ + { type: DATA_VIEW_SAVED_OBJECT_TYPE, name: 'test', id: '123' }, + ]); + expect(updatedFilters[0]).toHaveProperty('meta.index', '123'); + }); + + test('drops index setting if reference is missing', () => { + const updatedFilters = inject(filters, [ + { type: DATA_VIEW_SAVED_OBJECT_TYPE, name: 'test123', id: '123' }, + ]); + expect(updatedFilters[0]).toHaveProperty('meta.index', undefined); + }); + }); + + describe('reference extraction', () => { + test('correctly extracts references', () => { + const { state, references } = extract(filters); + expect(state[0]).toHaveProperty('meta.index'); + expect(references[0]).toHaveProperty('id', 'test'); + }); + }); +}); diff --git a/src/plugins/data/common/query/filters/persistable_state.ts b/src/plugins/data/common/query/filters/persistable_state.ts new file mode 100644 index 0000000000000..a309573fb9df2 --- /dev/null +++ b/src/plugins/data/common/query/filters/persistable_state.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import uuid from 'uuid'; +import { Filter } from '@kbn/es-query'; +import { SavedObjectReference } from '@kbn/core/types'; +import { MigrateFunctionsObject, VersionedState } from '@kbn/kibana-utils-plugin/common'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; + +export const extract = (filters: Filter[]) => { + const references: SavedObjectReference[] = []; + const updatedFilters = filters.map((filter) => { + if (filter.meta?.index) { + const id = uuid(); + references.push({ + type: DATA_VIEW_SAVED_OBJECT_TYPE, + name: id, + id: filter.meta.index, + }); + + return { + ...filter, + meta: { + ...filter.meta, + index: id, + }, + }; + } + return filter; + }); + return { state: updatedFilters, references }; +}; + +export const inject = (filters: Filter[], references: SavedObjectReference[]) => { + return filters.map((filter) => { + if (!filter.meta.index) { + return filter; + } + const reference = references.find((ref) => ref.name === filter.meta.index); + return { + ...filter, + meta: { + ...filter.meta, + index: reference && reference.id, + }, + }; + }); +}; + +export const telemetry = (filters: Filter[], collector: unknown) => { + return {}; +}; + +export const migrateToLatest = (filters: VersionedState) => { + return filters.state; +}; + +export const getAllMigrations = (): MigrateFunctionsObject => { + return {}; +}; diff --git a/src/plugins/data/common/query/index.ts b/src/plugins/data/common/query/index.ts index 35b1617dfa7d6..2d6e64841f6ea 100644 --- a/src/plugins/data/common/query/index.ts +++ b/src/plugins/data/common/query/index.ts @@ -9,3 +9,4 @@ export * from './timefilter'; export * from './types'; export * from './is_query'; +export * from './query_state'; diff --git a/src/plugins/data/common/query/persistable_state.test.ts b/src/plugins/data/common/query/persistable_state.test.ts index c69f7dee15e37..2fcfce910ebb8 100644 --- a/src/plugins/data/common/query/persistable_state.test.ts +++ b/src/plugins/data/common/query/persistable_state.test.ts @@ -6,35 +6,60 @@ * Side Public License, v 1. */ -import { extract, inject } from './persistable_state'; +import { extract, inject, getAllMigrations } from './persistable_state'; import { Filter } from '@kbn/es-query'; -import { DATA_VIEW_SAVED_OBJECT_TYPE } from '..'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; +import { QueryState } from './query_state'; -describe('filter manager persistable state tests', () => { +describe('query service persistable state tests', () => { const filters: Filter[] = [ { meta: { alias: 'test', disabled: false, negate: false, index: 'test' } }, ]; + const query = { language: 'kql', query: 'query' }; + const time = { from: new Date().toISOString(), to: new Date().toISOString() }; + const refreshInterval = { pause: false, value: 10 }; + + const queryState: QueryState = { + filters, + query, + time, + refreshInterval, + }; + describe('reference injection', () => { test('correctly inserts reference to filter', () => { - const updatedFilters = inject(filters, [ + const updatedQueryState = inject(queryState, [ { type: DATA_VIEW_SAVED_OBJECT_TYPE, name: 'test', id: '123' }, ]); - expect(updatedFilters[0]).toHaveProperty('meta.index', '123'); + expect(updatedQueryState.filters[0]).toHaveProperty('meta.index', '123'); + expect(updatedQueryState.query).toEqual(queryState.query); + expect(updatedQueryState.time).toEqual(queryState.time); + expect(updatedQueryState.refreshInterval).toEqual(queryState.refreshInterval); }); - test('drops index setting if reference is missing', () => { - const updatedFilters = inject(filters, [ + test('drops index setting from filter if reference is missing', () => { + const updatedQueryState = inject(queryState, [ { type: DATA_VIEW_SAVED_OBJECT_TYPE, name: 'test123', id: '123' }, ]); - expect(updatedFilters[0]).toHaveProperty('meta.index', undefined); + expect(updatedQueryState.filters[0]).toHaveProperty('meta.index', undefined); }); }); describe('reference extraction', () => { test('correctly extracts references', () => { - const { state, references } = extract(filters); - expect(state[0]).toHaveProperty('meta.index'); + const { state, references } = extract(queryState); + expect(state.filters[0]).toHaveProperty('meta.index'); expect(references[0]).toHaveProperty('id', 'test'); + + expect(state.query).toEqual(queryState.query); + expect(state.time).toEqual(queryState.time); + expect(state.refreshInterval).toEqual(queryState.refreshInterval); + }); + }); + + describe('migrations', () => { + test('getAllMigrations', () => { + expect(getAllMigrations()).toEqual({}); }); }); }); diff --git a/src/plugins/data/common/query/persistable_state.ts b/src/plugins/data/common/query/persistable_state.ts index 4860d892ab5d7..cf74bb0a2cb67 100644 --- a/src/plugins/data/common/query/persistable_state.ts +++ b/src/plugins/data/common/query/persistable_state.ts @@ -6,60 +6,72 @@ * Side Public License, v 1. */ -import uuid from 'uuid'; -import { Filter } from '@kbn/es-query'; import { SavedObjectReference } from '@kbn/core/types'; -import { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common'; -import { DATA_VIEW_SAVED_OBJECT_TYPE } from '..'; +import { mapValues } from 'lodash'; +import { + mergeMigrationFunctionMaps, + MigrateFunctionsObject, + VersionedState, +} from '@kbn/kibana-utils-plugin/common'; +import type { QueryState } from './query_state'; +import * as filtersPersistableState from './filters/persistable_state'; -export const extract = (filters: Filter[]) => { +export const extract = (queryState: QueryState) => { const references: SavedObjectReference[] = []; - const updatedFilters = filters.map((filter) => { - if (filter.meta?.index) { - const id = uuid(); - references.push({ - type: DATA_VIEW_SAVED_OBJECT_TYPE, - name: id, - id: filter.meta.index, - }); - return { - ...filter, - meta: { - ...filter.meta, - index: id, - }, - }; - } - return filter; - }); - return { state: updatedFilters, references }; + const { state: updatedFilters, references: referencesFromFilters } = + filtersPersistableState.extract(queryState.filters ?? []); + references.push(...referencesFromFilters); + + return { + state: { + ...queryState, + filters: updatedFilters, + }, + references, + }; }; -export const inject = (filters: Filter[], references: SavedObjectReference[]) => { - return filters.map((filter) => { - if (!filter.meta.index) { - return filter; - } - const reference = references.find((ref) => ref.name === filter.meta.index); - return { - ...filter, - meta: { - ...filter.meta, - index: reference && reference.id, - }, - }; - }); +export const inject = (queryState: QueryState, references: SavedObjectReference[]) => { + const updatedFilters = filtersPersistableState.inject(queryState.filters ?? [], references); + + return { + ...queryState, + filters: updatedFilters, + }; }; -export const telemetry = (filters: Filter[], collector: unknown) => { - return {}; +export const telemetry = (queryState: QueryState, collector: unknown) => { + const filtersTelemetry = filtersPersistableState.telemetry(queryState.filters ?? [], collector); + return { + ...filtersTelemetry, + }; }; -export const migrateToLatest = (filters: Filter[], version: string) => { - return filters; +export const migrateToLatest = ({ state, version }: VersionedState) => { + const migratedFilters = filtersPersistableState.migrateToLatest({ + state: state.filters ?? [], + version, + }); + + return { + ...state, + filters: migratedFilters, + }; }; export const getAllMigrations = (): MigrateFunctionsObject => { - return {}; + const queryMigrations: MigrateFunctionsObject = {}; + + const filterMigrations: MigrateFunctionsObject = mapValues( + filtersPersistableState.getAllMigrations(), + (migrate) => { + return (state: QueryState) => ({ + ...state, + filters: migrate(state.filters ?? []), + }); + } + ); + + return mergeMigrationFunctionMaps(queryMigrations, filterMigrations); }; diff --git a/src/plugins/data/common/query/query_state.ts b/src/plugins/data/common/query/query_state.ts new file mode 100644 index 0000000000000..fbc5626f9a28c --- /dev/null +++ b/src/plugins/data/common/query/query_state.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Filter } from '@kbn/es-query'; +import type { TimeRange, RefreshInterval } from './timefilter/types'; +import type { Query } from './types'; + +/** + * All query state service state + * + * @remark + * `type` instead of `interface` to make it compatible with PersistableState utils + * + */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type QueryState = { + time?: TimeRange; + refreshInterval?: RefreshInterval; + filters?: Filter[]; + query?: Query; +}; diff --git a/src/plugins/data/common/search/search_source/search_source_service.ts b/src/plugins/data/common/search/search_source/search_source_service.ts index c03053c839f4d..e87f589044f9a 100644 --- a/src/plugins/data/common/search/search_source/search_source_service.ts +++ b/src/plugins/data/common/search/search_source/search_source_service.ts @@ -20,7 +20,7 @@ import { SerializedSearchSourceFields, } from '.'; import { IndexPatternsContract } from '../..'; -import { getAllMigrations as filtersGetAllMigrations } from '../../query/persistable_state'; +import { getAllMigrations as filtersGetAllMigrations } from '../../query/filters/persistable_state'; const getAllMigrations = (): MigrateFunctionsObject => { const searchSourceMigrations = {}; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 0f50384893b18..c91e21d8683c3 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -269,6 +269,7 @@ export type { NowProviderInternalContract } from './now_provider'; export type { QueryState, + QueryState$, SavedQuery, SavedQueryService, SavedQueryTimeFilter, diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index c874a15b5efc6..cf9ad750d2809 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -11,24 +11,25 @@ import { Subject } from 'rxjs'; import { IUiSettingsClient } from '@kbn/core/public'; -import { isFilterPinned, onlyDisabledFiltersChanged, Filter } from '@kbn/es-query'; -import { PersistableStateService } from '@kbn/kibana-utils-plugin/common/persistable_state'; -import { sortFilters } from './lib/sort_filters'; -import { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; - import { - FilterStateStore, + isFilterPinned, + onlyDisabledFiltersChanged, + Filter, uniqFilters, compareFilters, COMPARE_ALL_OPTIONS, - UI_SETTINGS, -} from '../../../common'; +} from '@kbn/es-query'; +import { PersistableStateService } from '@kbn/kibana-utils-plugin/common/persistable_state'; +import { sortFilters } from './lib/sort_filters'; +import { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; + +import { FilterStateStore, UI_SETTINGS } from '../../../common'; import { getAllMigrations, inject, extract, telemetry, -} from '../../../common/query/persistable_state'; +} from '../../../common/query/filters/persistable_state'; interface PartitionedFilters { globalFilters: Filter[]; diff --git a/src/plugins/data/public/query/mocks.ts b/src/plugins/data/public/query/mocks.ts index 296a61afef2fd..14de815c0d793 100644 --- a/src/plugins/data/public/query/mocks.ts +++ b/src/plugins/data/public/query/mocks.ts @@ -22,6 +22,12 @@ const createSetupContractMock = () => { queryString: queryStringManagerMock.createSetupContract(), state$: new Observable(), getState: jest.fn(), + + inject: jest.fn(), + extract: jest.fn(), + telemetry: jest.fn(), + migrateToLatest: jest.fn(), + getAllMigrations: jest.fn(), }; return setupContract; @@ -37,6 +43,11 @@ const createStartContractMock = () => { getState: jest.fn(), timefilter: timefilterServiceMock.createStartContract(), getEsQuery: jest.fn(), + inject: jest.fn(), + extract: jest.fn(), + telemetry: jest.fn(), + migrateToLatest: jest.fn(), + getAllMigrations: jest.fn(), }; return startContract; @@ -47,6 +58,12 @@ const createMock = () => { setup: jest.fn(), start: jest.fn(), stop: jest.fn(), + + inject: jest.fn(), + extract: jest.fn(), + telemetry: jest.fn(), + migrateToLatest: jest.fn(), + getAllMigrations: jest.fn(), }; mocked.setup.mockReturnValue(createSetupContractMock()); diff --git a/src/plugins/data/public/query/query_service.test.ts b/src/plugins/data/public/query/query_service.test.ts index 5eb6815c3ba20..6ddde2d18f74f 100644 --- a/src/plugins/data/public/query/query_service.test.ts +++ b/src/plugins/data/public/query/query_service.test.ts @@ -59,6 +59,14 @@ describe('query_service', () => { queryStringManager = queryServiceStart.queryString; }); + test('implements PersistableState interface', () => { + expect(queryServiceStart).toHaveProperty('inject'); + expect(queryServiceStart).toHaveProperty('extract'); + expect(queryServiceStart).toHaveProperty('telemetry'); + expect(queryServiceStart).toHaveProperty('migrateToLatest'); + expect(queryServiceStart).toHaveProperty('getAllMigrations'); + }); + test('state is initialized with state from query service', () => { const state = queryServiceStart.getState(); diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index 8b309c9821d3e..5eb24846c3578 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -8,26 +8,32 @@ import { share } from 'rxjs/operators'; import { HttpStart, IUiSettingsClient } from '@kbn/core/public'; +import { PersistableStateService, VersionedState } from '@kbn/kibana-utils-plugin/common'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { buildEsQuery } from '@kbn/es-query'; import { FilterManager } from './filter_manager'; import { createAddToQueryLog } from './lib'; -import { TimefilterService } from './timefilter'; import type { TimefilterSetup } from './timefilter'; +import { TimefilterService } from './timefilter'; import { createSavedQueryService } from './saved_query/saved_query_service'; -import { createQueryStateObservable } from './state_sync/create_query_state_observable'; -import { getQueryState } from './query_state'; +import { + createQueryStateObservable, + QueryState$, +} from './state_sync/create_query_state_observable'; +import { getQueryState, QueryState } from './query_state'; import type { QueryStringContract } from './query_string'; import { QueryStringManager } from './query_string'; import { getEsQueryConfig, TimeRange } from '../../common'; import { getUiSettings } from '../services'; import { NowProviderInternalContract } from '../now_provider'; import { IndexPattern } from '..'; - -/** - * Query Service - * @internal - */ +import { + extract, + getAllMigrations, + inject, + migrateToLatest, + telemetry, +} from '../../common/query/persistable_state'; interface QueryServiceSetupDependencies { storage: IStorageWrapper; @@ -41,14 +47,41 @@ interface QueryServiceStartDependencies { http: HttpStart; } -export class QueryService { +export interface QuerySetup extends PersistableStateService { + filterManager: FilterManager; + timefilter: TimefilterSetup; + queryString: QueryStringContract; + state$: QueryState$; + getState(): QueryState; +} + +export interface QueryStart extends PersistableStateService { + filterManager: FilterManager; + timefilter: TimefilterSetup; + queryString: QueryStringContract; + state$: QueryState$; + getState(): QueryState; + + // TODO: type explicitly + addToQueryLog: ReturnType; + // TODO: type explicitly + savedQueries: ReturnType; + // TODO: type explicitly + getEsQuery(indexPattern: IndexPattern, timeRange?: TimeRange): ReturnType; +} + +/** + * Query Service + * @internal + */ +export class QueryService implements PersistableStateService { filterManager!: FilterManager; timefilter!: TimefilterSetup; queryStringManager!: QueryStringContract; - state$!: ReturnType; + state$!: QueryState$; - public setup({ storage, uiSettings, nowProvider }: QueryServiceSetupDependencies) { + public setup({ storage, uiSettings, nowProvider }: QueryServiceSetupDependencies): QuerySetup { this.filterManager = new FilterManager(uiSettings); const timefilterService = new TimefilterService(nowProvider); @@ -71,10 +104,11 @@ export class QueryService { queryString: this.queryStringManager, state$: this.state$, getState: () => this.getQueryState(), + ...this.getPersistableStateMethods(), }; } - public start({ storage, uiSettings, http }: QueryServiceStartDependencies) { + public start({ storage, uiSettings, http }: QueryServiceStartDependencies): QueryStart { return { addToQueryLog: createAddToQueryLog({ storage, @@ -96,6 +130,7 @@ export class QueryService { getEsQueryConfig(getUiSettings()) ); }, + ...this.getPersistableStateMethods(), }; } @@ -110,8 +145,27 @@ export class QueryService { filterManager: this.filterManager, }); } -} -/** @public */ -export type QuerySetup = ReturnType; -export type QueryStart = ReturnType; + public extract = extract; + + public inject = inject; + + public telemetry = telemetry; + + public getAllMigrations = getAllMigrations; + + public migrateToLatest = (versionedState: VersionedState) => { + // Argument of type 'VersionedState' is not assignable to parameter of type 'VersionedState'. + return migrateToLatest(versionedState as VersionedState); + }; + + private getPersistableStateMethods(): PersistableStateService { + return { + extract: this.extract.bind(this), + inject: this.inject.bind(this), + telemetry: this.telemetry.bind(this), + migrateToLatest: this.migrateToLatest.bind(this), + getAllMigrations: this.getAllMigrations.bind(this), + }; + } +} diff --git a/src/plugins/data/public/query/query_state.ts b/src/plugins/data/public/query/query_state.ts index 77242c981bda2..e07bc631ca1e8 100644 --- a/src/plugins/data/public/query/query_state.ts +++ b/src/plugins/data/public/query/query_state.ts @@ -6,21 +6,12 @@ * Side Public License, v 1. */ -import type { Filter } from '@kbn/es-query'; +import type { QueryState } from '../../common'; import type { TimefilterSetup } from './timefilter'; import type { FilterManager } from './filter_manager'; import type { QueryStringContract } from './query_string'; -import type { RefreshInterval, TimeRange, Query } from '../../common'; -/** - * All query state service state - */ -export interface QueryState { - time?: TimeRange; - refreshInterval?: RefreshInterval; - filters?: Filter[]; - query?: Query; -} +export type { QueryState }; export function getQueryState({ timefilter: { timefilter }, diff --git a/src/plugins/data/public/query/state_sync/create_query_state_observable.ts b/src/plugins/data/public/query/state_sync/create_query_state_observable.ts index 39e7802753ee2..8520423915dba 100644 --- a/src/plugins/data/public/query/state_sync/create_query_state_observable.ts +++ b/src/plugins/data/public/query/state_sync/create_query_state_observable.ts @@ -16,6 +16,8 @@ import { getQueryState, QueryState } from '../query_state'; import { QueryStateChange } from './types'; import type { QueryStringContract } from '../query_string'; +export type QueryState$ = Observable<{ changes: QueryStateChange; state: QueryState }>; + export function createQueryStateObservable({ timefilter, filterManager, @@ -24,7 +26,7 @@ export function createQueryStateObservable({ timefilter: TimefilterSetup; filterManager: FilterManager; queryString: QueryStringContract; -}): Observable<{ changes: QueryStateChange; state: QueryState }> { +}): QueryState$ { const state = createStateContainer( getQueryState({ timefilter, filterManager, queryString }) ); diff --git a/src/plugins/data/public/query/state_sync/index.ts b/src/plugins/data/public/query/state_sync/index.ts index ffeda864f5172..4137184c90a69 100644 --- a/src/plugins/data/public/query/state_sync/index.ts +++ b/src/plugins/data/public/query/state_sync/index.ts @@ -9,3 +9,4 @@ export { connectToQueryState } from './connect_to_query_state'; export { syncQueryStateWithUrl, syncGlobalQueryStateWithUrl } from './sync_state_with_url'; export type { QueryStateChange, GlobalQueryStateFromUrl } from './types'; +export type { QueryState$ } from './create_query_state_observable'; diff --git a/src/plugins/data/server/query/query_service.ts b/src/plugins/data/server/query/query_service.ts index ea7c05905efee..81e511ea75e64 100644 --- a/src/plugins/data/server/query/query_service.ts +++ b/src/plugins/data/server/query/query_service.ts @@ -7,16 +7,27 @@ */ import { CoreSetup, Plugin } from '@kbn/core/server'; +import type { Filter } from '@kbn/es-query'; +import { PersistableStateService } from '@kbn/kibana-utils-plugin/common'; import { querySavedObjectType } from '../saved_objects'; -import { extract, getAllMigrations, inject, telemetry } from '../../common/query/persistable_state'; +import * as queryPersistableState from '../../common/query/persistable_state'; +import * as filtersPersistableState from '../../common/query/filters/persistable_state'; import { registerSavedQueryRoutes } from './routes'; import { registerSavedQueryRouteHandlerContext, SavedQueryRouteHandlerContext, } from './route_handler_context'; +import { QueryState } from '../../common'; +export interface QuerySetup extends PersistableStateService { + filterManager: PersistableStateService; +} + +/** + * @internal + */ export class QueryService implements Plugin { - public setup(core: CoreSetup) { + public setup(core: CoreSetup): QuerySetup { core.savedObjects.registerType(querySavedObjectType); core.http.registerRouteHandlerContext( 'savedQuery', @@ -25,17 +36,18 @@ export class QueryService implements Plugin { registerSavedQueryRoutes(core); return { + extract: queryPersistableState.extract, + inject: queryPersistableState.inject, + telemetry: queryPersistableState.telemetry, + getAllMigrations: queryPersistableState.getAllMigrations, filterManager: { - extract, - inject, - telemetry, - getAllMigrations, + extract: filtersPersistableState.extract, + inject: filtersPersistableState.inject, + telemetry: filtersPersistableState.telemetry, + getAllMigrations: filtersPersistableState.getAllMigrations, }, }; } public start() {} } - -/** @public */ -export type QuerySetup = ReturnType; diff --git a/src/plugins/data/server/query/route_handler_context.ts b/src/plugins/data/server/query/route_handler_context.ts index 0af658d7692c5..a79063014abc0 100644 --- a/src/plugins/data/server/query/route_handler_context.ts +++ b/src/plugins/data/server/query/route_handler_context.ts @@ -9,7 +9,7 @@ import { CustomRequestHandlerContext, RequestHandlerContext, SavedObject } from '@kbn/core/server'; import { isFilters } from '@kbn/es-query'; import { isQuery, SavedQueryAttributes } from '../../common'; -import { extract, inject } from '../../common/query/persistable_state'; +import { extract, inject } from '../../common/query/filters/persistable_state'; function injectReferences({ id, diff --git a/src/plugins/data/server/saved_objects/migrations/query.ts b/src/plugins/data/server/saved_objects/migrations/query.ts index b297276f9a22a..dadba8e4c94a5 100644 --- a/src/plugins/data/server/saved_objects/migrations/query.ts +++ b/src/plugins/data/server/saved_objects/migrations/query.ts @@ -10,7 +10,7 @@ import { mapValues } from 'lodash'; import { SavedObject } from '@kbn/core/server'; import { mergeMigrationFunctionMaps } from '@kbn/kibana-utils-plugin/common'; import { SavedQueryAttributes } from '../../../common'; -import { extract, getAllMigrations } from '../../../common/query/persistable_state'; +import { extract, getAllMigrations } from '../../../common/query/filters/persistable_state'; const extractFilterReferences = (doc: SavedObject) => { const { state: filters, references } = extract(doc.attributes.filters ?? []); From d415fbac53982005312a2982085551f889bd8839 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 23 May 2022 14:43:02 +0100 Subject: [PATCH 087/120] skip flaky suite (#132628) --- .../functional/apps/home/feature_controls/home_security.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/home/feature_controls/home_security.ts b/x-pack/test/functional/apps/home/feature_controls/home_security.ts index 96a3ccbe8ea5d..bfa3df5642975 100644 --- a/x-pack/test/functional/apps/home/feature_controls/home_security.ts +++ b/x-pack/test/functional/apps/home/feature_controls/home_security.ts @@ -35,7 +35,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); }); - describe('global all privileges', () => { + // https://github.com/elastic/kibana/issues/132628 + describe.skip('global all privileges', () => { before(async () => { await security.role.create('global_all_role', { elasticsearch: {}, From 906569f6795162692d532e6a24f70a5e91b7f68c Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Mon, 23 May 2022 10:03:56 -0400 Subject: [PATCH 088/120] Update list of filterable tags when a new agent is enrolled (#132638) --- .../sections/agents/agent_list_page/index.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index bbea3284f72b8..9bb2c8107e6e1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -244,13 +244,15 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { inactive: agentsRequest.data.totalInactive, }); - // Only set tags on the first request - we don't want the list of tags to update based - // on the returned set of agents from the API - if (allTags === undefined) { - const newAllTags = Array.from( - new Set(agentsRequest.data.items.flatMap((agent) => agent.tags ?? [])) - ); + const newAllTags = Array.from( + new Set(agentsRequest.data.items.flatMap((agent) => agent.tags ?? [])) + ); + // We only want to update the list of available tags if we've either received + // more tags than we currently have from the API (e.g. new agents have been enrolled) + // or we haven't set our list of tags yet. TODO: Would it be possible to remove a tag + // from the filterable list if an agent is unenrolled and no agents remain with that tag? + if (!allTags || newAllTags.length > allTags.length) { setAllTags(newAllTags); } From 96c988abcebce335ce1af7c245ea8620921b5642 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 23 May 2022 16:12:09 +0200 Subject: [PATCH 089/120] [Discover][Alerting] Add test button to rule flyout (#132540) --- .../expression/es_query_expression.tsx | 112 +++++------------- .../search_source_expression.test.tsx | 68 ++++++++--- .../search_source_expression_form.tsx | 19 ++- .../es_query/expression/test_query_row.tsx | 68 +++++++++++ .../expression/use_test_query.test.ts | 37 ++++++ .../es_query/expression/use_test_query.ts | 57 +++++++++ 6 files changed, 264 insertions(+), 97 deletions(-) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/test_query_row.tsx create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/use_test_query.test.ts create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/use_test_query.ts diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx index afb45f90c6e52..92096ba4541c4 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx @@ -16,16 +16,13 @@ import 'brace/theme/github'; import { EuiFlexGroup, EuiFlexItem, - EuiButtonEmpty, EuiSpacer, EuiFormRow, - EuiText, EuiTitle, EuiLink, EuiIconTip, } from '@elastic/eui'; import { DocLinksStart, HttpSetup } from '@kbn/core/public'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { XJson, EuiCodeEditor } from '@kbn/es-ui-shared-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; @@ -42,10 +39,8 @@ import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_q import { EsQueryAlertParams, SearchType } from '../types'; import { IndexSelectPopover } from '../../components/index_select_popover'; import { DEFAULT_VALUES } from '../constants'; - -function totalHitsToNumber(total: estypes.SearchHitsMetadata['total']): number { - return typeof total === 'number' ? total : total?.value ?? 0; -} +import { TestQueryRow } from './test_query_row'; +import { totalHitsToNumber } from './use_test_query'; const { useXJsonMode } = XJson; const xJsonMode = new XJsonMode(); @@ -109,8 +104,6 @@ export const EsQueryExpression = ({ }> >([]); const { convertToJson, setXJson, xJson } = useXJsonMode(DEFAULT_VALUES.QUERY); - const [testQueryResult, setTestQueryResult] = useState(null); - const [testQueryError, setTestQueryError] = useState(null); const setDefaultExpressionValues = async () => { setRuleProperty('params', currentAlertParams); @@ -133,55 +126,39 @@ export const EsQueryExpression = ({ } }; - const hasValidationErrors = () => { + const hasValidationErrors = useCallback(() => { const { errors: validationErrors } = validateExpression(currentAlertParams); return Object.keys(validationErrors).some( (key) => validationErrors[key] && validationErrors[key].length ); - }; + }, [currentAlertParams]); - const onTestQuery = async () => { - if (!hasValidationErrors()) { - setTestQueryError(null); - setTestQueryResult(null); - try { - const window = `${timeWindowSize}${timeWindowUnit}`; - const timeWindow = parseDuration(window); - const parsedQuery = JSON.parse(esQuery); - const now = Date.now(); - const { rawResponse } = await firstValueFrom( - data.search.search({ - params: buildSortedEventsQuery({ - index, - from: new Date(now - timeWindow).toISOString(), - to: new Date(now).toISOString(), - filter: parsedQuery.query, - size: 0, - searchAfterSortId: undefined, - timeField: timeField ? timeField : '', - track_total_hits: true, - }), - }) - ); - - const hits = rawResponse.hits; - setTestQueryResult( - i18n.translate('xpack.stackAlerts.esQuery.ui.numQueryMatchesText', { - defaultMessage: 'Query matched {count} documents in the last {window}.', - values: { count: totalHitsToNumber(hits.total), window }, - }) - ); - } catch (err) { - const message = err?.body?.attributes?.error?.root_cause[0]?.reason || err?.body?.message; - setTestQueryError( - i18n.translate('xpack.stackAlerts.esQuery.ui.queryError', { - defaultMessage: 'Error testing query: {message}', - values: { message: message ? `${err.message}: ${message}` : err.message }, - }) - ); - } + const onTestQuery = useCallback(async () => { + const window = `${timeWindowSize}${timeWindowUnit}`; + if (hasValidationErrors()) { + return { nrOfDocs: 0, timeWindow: window }; } - }; + const timeWindow = parseDuration(window); + const parsedQuery = JSON.parse(esQuery); + const now = Date.now(); + const { rawResponse } = await firstValueFrom( + data.search.search({ + params: buildSortedEventsQuery({ + index, + from: new Date(now - timeWindow).toISOString(), + to: new Date(now).toISOString(), + filter: parsedQuery.query, + size: 0, + searchAfterSortId: undefined, + timeField: timeField ? timeField : '', + track_total_hits: true, + }), + }) + ); + + const hits = rawResponse.hits; + return { nrOfDocs: totalHitsToNumber(hits.total), timeWindow: window }; + }, [data.search, esQuery, index, timeField, timeWindowSize, timeWindowUnit, hasValidationErrors]); return ( @@ -281,36 +258,7 @@ export const EsQueryExpression = ({ }} /> - - - - - - {testQueryResult && ( - - -

{testQueryResult}

-
-
- )} - {testQueryError && ( - - -

{testQueryError}

-
-
- )} + diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx index d12833a3f258f..091fd606e1bf0 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx @@ -14,12 +14,19 @@ import { EsQueryAlertParams, SearchType } from '../types'; import { SearchSourceExpression } from './search_source_expression'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { act } from 'react-dom/test-utils'; +import { of } from 'rxjs'; +import { IKibanaSearchResponse, ISearchSource } from '@kbn/data-plugin/common'; +import { IUiSettingsClient } from '@kbn/core/public'; +import { findTestSubject } from '@elastic/eui/lib/test'; import { EuiLoadingSpinner } from '@elastic/eui'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; const dataViewPluginMock = dataViewPluginMocks.createStartContract(); const chartsStartMock = chartPluginMock.createStartContract(); const unifiedSearchMock = unifiedSearchPluginMock.createStartContract(); +export const uiSettingsMock = { + get: jest.fn(), +} as unknown as IUiSettingsClient; const defaultSearchSourceExpressionParams: EsQueryAlertParams = { size: 100, @@ -52,7 +59,23 @@ const searchSourceMock = { } return ''; }, -}; + setField: jest.fn(), + createCopy: jest.fn(() => { + return searchSourceMock; + }), + setParent: jest.fn(() => { + return searchSourceMock; + }), + fetch$: jest.fn(() => { + return of({ + rawResponse: { + hits: { + total: 1234, + }, + }, + }); + }), +} as unknown as ISearchSource; const savedQueryMock = { id: 'test-id', @@ -67,10 +90,6 @@ const savedQueryMock = { }, }; -jest.mock('./search_source_expression_form', () => ({ - SearchSourceExpressionForm: () =>
search source expression form mock
, -})); - const dataMock = dataPluginMock.createStartContract(); (dataMock.search.searchSource.create as jest.Mock).mockImplementation(() => Promise.resolve(searchSourceMock) @@ -79,6 +98,9 @@ const dataMock = dataPluginMock.createStartContract(); (dataMock.query.savedQueries.getSavedQuery as jest.Mock).mockImplementation(() => Promise.resolve(savedQueryMock) ); +dataMock.query.savedQueries.findSavedQueries = jest.fn(() => + Promise.resolve({ total: 0, queries: [] }) +); const setup = (alertParams: EsQueryAlertParams) => { const errors = { @@ -88,8 +110,8 @@ const setup = (alertParams: EsQueryAlertParams) => { searchConfiguration: [], }; - const wrapper = mountWithIntl( - + return mountWithIntl( + ) => { /> ); - - return wrapper; }; describe('SearchSourceAlertTypeExpression', () => { test('should render correctly', async () => { - let wrapper = setup(defaultSearchSourceExpressionParams).children(); + let wrapper = setup(defaultSearchSourceExpressionParams); expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); - expect(wrapper.text().includes('Cant find searchSource')).toBeFalsy(); await act(async () => { await nextTick(); }); wrapper = await wrapper.update(); + expect(findTestSubject(wrapper, 'thresholdExpression')).toBeTruthy(); + }); + test('should show success message if Test Query is successful', async () => { + let wrapper = setup(defaultSearchSourceExpressionParams); + await act(async () => { + await nextTick(); + }); + wrapper = await wrapper.update(); + await act(async () => { + findTestSubject(wrapper, 'testQuery').simulate('click'); + wrapper.update(); + }); + wrapper = await wrapper.update(); - expect(wrapper.text().includes('Cant find searchSource')).toBeFalsy(); - expect(wrapper.text().includes('search source expression form mock')).toBeTruthy(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="testQuerySuccess"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="testQueryError"]').exists()).toBeFalsy(); + expect(wrapper.find('EuiText[data-test-subj="testQuerySuccess"]').text()).toEqual( + `Query matched 1234 documents in the last 15s.` + ); }); test('should render error prompt', async () => { (dataMock.search.searchSource.create as jest.Mock).mockImplementationOnce(() => Promise.reject(new Error('Cant find searchSource')) ); - let wrapper = setup(defaultSearchSourceExpressionParams).children(); + let wrapper = setup(defaultSearchSourceExpressionParams); expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); expect(wrapper.text().includes('Cant find searchSource')).toBeFalsy(); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx index afd6a156187ee..3c4353188af81 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx @@ -7,10 +7,12 @@ import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import deepEqual from 'fast-deep-equal'; +import { firstValueFrom } from 'rxjs'; +import { Filter } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Filter, DataView, Query, ISearchSource } from '@kbn/data-plugin/common'; +import { DataView, Query, ISearchSource, getTime } from '@kbn/data-plugin/common'; import { ForLastExpression, IErrorObject, @@ -24,6 +26,8 @@ import { EsQueryAlertParams, SearchType } from '../types'; import { DEFAULT_VALUES } from '../constants'; import { DataViewSelectPopover } from '../../components/data_view_select_popover'; import { useTriggersAndActionsUiDeps } from '../util'; +import { totalHitsToNumber } from './use_test_query'; +import { TestQueryRow } from './test_query_row'; interface LocalState { index: DataView; @@ -161,6 +165,17 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp (updatedValue: number) => dispatch({ type: 'size', payload: updatedValue }), [] ); + const onTestFetch = useCallback(async () => { + const timeWindow = `${timeWindowSize}${timeWindowUnit}`; + const testSearchSource = searchSource.createCopy(); + const timeFilter = getTime(searchSource.getField('index')!, { + from: `now-${timeWindow}`, + to: 'now', + }); + testSearchSource.setField('filter', timeFilter); + const { rawResponse } = await firstValueFrom(testSearchSource.fetch$()); + return { nrOfDocs: totalHitsToNumber(rawResponse.hits.total), timeWindow }; + }, [searchSource, timeWindowSize, timeWindowUnit]); return ( @@ -264,6 +279,8 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp onChangeSelectedValue={onChangeSizeValue} /> + + ); }; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/test_query_row.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/test_query_row.tsx new file mode 100644 index 0000000000000..6d18f2f0537ce --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/test_query_row.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiButtonEmpty, EuiFormRow, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useTestQuery } from './use_test_query'; + +export function TestQueryRow({ + fetch, + hasValidationErrors, +}: { + fetch: () => Promise<{ nrOfDocs: number; timeWindow: string }>; + hasValidationErrors: boolean; +}) { + const { onTestQuery, testQueryResult, testQueryError, testQueryLoading } = useTestQuery(fetch); + + return ( + <> + + + + + + {testQueryLoading && ( + + +

+ +

+
+
+ )} + {testQueryResult && ( + + +

{testQueryResult}

+
+
+ )} + {testQueryError && ( + + +

{testQueryError}

+
+
+ )} + + ); +} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/use_test_query.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/use_test_query.test.ts new file mode 100644 index 0000000000000..e8df349e320db --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/use_test_query.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { act } from 'react-test-renderer'; +import { useTestQuery } from './use_test_query'; + +describe('useTestQuery', () => { + test('returning a valid result', async () => { + const { result } = renderHook(useTestQuery, { + initialProps: () => Promise.resolve({ nrOfDocs: 1, timeWindow: '1s' }), + }); + await act(async () => { + await result.current.onTestQuery(); + }); + expect(result.current.testQueryLoading).toBe(false); + expect(result.current.testQueryError).toBe(null); + expect(result.current.testQueryResult).toContain('1s'); + expect(result.current.testQueryResult).toContain('1 document'); + }); + test('returning an error', async () => { + const errorMsg = 'How dare you writing such a query'; + const { result } = renderHook(useTestQuery, { + initialProps: () => Promise.reject({ message: errorMsg }), + }); + await act(async () => { + await result.current.onTestQuery(); + }); + expect(result.current.testQueryLoading).toBe(false); + expect(result.current.testQueryError).toContain(errorMsg); + expect(result.current.testQueryResult).toBe(null); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/use_test_query.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/use_test_query.ts new file mode 100644 index 0000000000000..b80500471638e --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/use_test_query.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +/** + * Hook used to test the data fetching execution by returning a number of found documents + * Or in error in case it's failing + */ +export function useTestQuery(fetch: () => Promise<{ nrOfDocs: number; timeWindow: string }>) { + const [testQueryResult, setTestQueryResult] = useState(null); + const [testQueryError, setTestQueryError] = useState(null); + const [testQueryLoading, setTestQueryLoading] = useState(false); + + const onTestQuery = useCallback(async () => { + setTestQueryLoading(true); + setTestQueryError(null); + setTestQueryResult(null); + try { + const { nrOfDocs, timeWindow } = await fetch(); + + setTestQueryResult( + i18n.translate('xpack.stackAlerts.esQuery.ui.numQueryMatchesText', { + defaultMessage: 'Query matched {count} documents in the last {window}.', + values: { count: nrOfDocs, window: timeWindow }, + }) + ); + } catch (err) { + const message = err?.body?.attributes?.error?.root_cause[0]?.reason || err?.body?.message; + setTestQueryError( + i18n.translate('xpack.stackAlerts.esQuery.ui.queryError', { + defaultMessage: 'Error testing query: {message}', + values: { message: message ? `${err.message}: ${message}` : err.message }, + }) + ); + } finally { + setTestQueryLoading(false); + } + }, [fetch]); + + return { + onTestQuery, + testQueryResult, + testQueryError, + testQueryLoading, + }; +} + +export function totalHitsToNumber(total: estypes.SearchHitsMetadata['total']): number { + return typeof total === 'number' ? total : total?.value ?? 0; +} From 9754e0b0e2f540cd929c7ac20fefb596cfdc27b2 Mon Sep 17 00:00:00 2001 From: "Lucas F. da Costa" Date: Mon, 23 May 2022 15:16:52 +0100 Subject: [PATCH 090/120] [Uptime] add flag to rerun monitors when they're edited (#132639) --- .../routes/monitor_cruds/edit_monitor.ts | 21 +-- .../synthetics_service/service_api_client.ts | 10 +- .../synthetics_service.test.ts | 145 ++++++++++++------ .../synthetics_service/synthetics_service.ts | 3 +- 4 files changed, 116 insertions(+), 63 deletions(-) diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts index 669577e8d60ee..8bd29c460b8cf 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts @@ -92,16 +92,19 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ monitor.type === 'browser' ? { ...formattedMonitor, urls: '' } : formattedMonitor ); - const errors = await syntheticsService.pushConfigs([ - { - ...editedMonitor, - id: updatedMonitor.id, - fields: { - config_id: updatedMonitor.id, + const errors = await syntheticsService.pushConfigs( + [ + { + ...editedMonitor, + id: updatedMonitor.id, + fields: { + config_id: updatedMonitor.id, + }, + fields_under_root: true, }, - fields_under_root: true, - }, - ]); + ], + true + ); sendTelemetryEvents( logger, diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts index 41ae8a5a55090..a688b23baef9d 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts @@ -24,6 +24,7 @@ export interface ServiceData { api_key: string; }; runOnce?: boolean; + isEdit?: boolean; } export class ServiceAPIClient { @@ -118,7 +119,7 @@ export class ServiceAPIClient { async callAPI( method: 'POST' | 'PUT' | 'DELETE', - { monitors: allMonitors, output, runOnce }: ServiceData + { monitors: allMonitors, output, runOnce, isEdit }: ServiceData ) { if (this.username === TEST_SERVICE_USERNAME) { // we don't want to call service while local integration tests are running @@ -134,7 +135,12 @@ export class ServiceAPIClient { return axios({ method, url: url + (runOnce ? '/run' : '/monitors'), - data: { monitors: monitorsStreams, output, stack_version: this.kibanaVersion }, + data: { + monitors: monitorsStreams, + output, + stack_version: this.kibanaVersion, + is_edit: isEdit, + }, headers: process.env.NODE_ENV !== 'production' && this.authorization ? { diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts index 952e18ce9c884..6654fdbf2a132 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts @@ -12,6 +12,7 @@ import { SyntheticsService, SyntheticsConfig } from './synthetics_service'; import { loggerMock } from '@kbn/core/server/logging/logger.mock'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import axios, { AxiosResponse } from 'axios'; +import times from 'lodash/times'; describe('SyntheticsService', () => { const mockEsClient = { @@ -27,6 +28,60 @@ describe('SyntheticsService', () => { const logger = loggerMock.create(); + const getMockedService = (locationsNum: number = 1) => { + serverMock.config = { service: { devUrl: 'http://localhost' } }; + const service = new SyntheticsService(logger, serverMock, { + username: 'dev', + password: '12345', + }); + + const locations = times(locationsNum).map((n) => { + return { + id: `loc-${n}`, + label: `Location ${n}`, + url: `example.com/${n}`, + geo: { + lat: 0, + lon: 0, + }, + isServiceManaged: true, + }; + }); + + service.apiClient.locations = locations; + + jest.spyOn(service, 'getApiKey').mockResolvedValue({ name: 'example', id: 'i', apiKey: 'k' }); + jest.spyOn(service, 'getOutput').mockResolvedValue({ hosts: ['es'], api_key: 'i:k' }); + + return { service, locations }; + }; + + const getFakePayload = (locations: SyntheticsConfig['locations']) => { + return { + type: 'http', + enabled: true, + schedule: { + number: '3', + unit: 'm', + }, + name: 'my mon', + locations, + urls: 'http://google.com', + max_redirects: '0', + password: '', + proxy_url: '', + id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d', + fields: { config_id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d' }, + fields_under_root: true, + }; + }; + + beforeEach(() => { + (axios as jest.MockedFunction).mockReset(); + }); + + afterEach(() => jest.restoreAllMocks()); + it('inits properly', async () => { const service = new SyntheticsService(logger, serverMock, {}); service.init(); @@ -72,58 +127,10 @@ describe('SyntheticsService', () => { }); describe('addConfig', () => { - afterEach(() => jest.restoreAllMocks()); - it('saves configs only to the selected locations', async () => { - serverMock.config = { service: { devUrl: 'http://localhost' } }; - const service = new SyntheticsService(logger, serverMock, { - username: 'dev', - password: '12345', - }); - - service.apiClient.locations = [ - { - id: 'selected', - label: 'Selected Location', - url: 'example.com/1', - geo: { - lat: 0, - lon: 0, - }, - isServiceManaged: true, - }, - { - id: 'not selected', - label: 'Not Selected Location', - url: 'example.com/2', - geo: { - lat: 0, - lon: 0, - }, - isServiceManaged: true, - }, - ]; + const { service, locations } = getMockedService(3); - jest.spyOn(service, 'getApiKey').mockResolvedValue({ name: 'example', id: 'i', apiKey: 'k' }); - jest.spyOn(service, 'getOutput').mockResolvedValue({ hosts: ['es'], api_key: 'i:k' }); - - const payload = { - type: 'http', - enabled: true, - schedule: { - number: '3', - unit: 'm', - }, - name: 'my mon', - locations: [{ id: 'selected', isServiceManaged: true }], - urls: 'http://google.com', - max_redirects: '0', - password: '', - proxy_url: '', - id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d', - fields: { config_id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d' }, - fields_under_root: true, - }; + const payload = getFakePayload([locations[0]]); (axios as jest.MockedFunction).mockResolvedValue({} as AxiosResponse); @@ -132,7 +139,43 @@ describe('SyntheticsService', () => { expect(axios).toHaveBeenCalledTimes(1); expect(axios).toHaveBeenCalledWith( expect.objectContaining({ - url: 'example.com/1/monitors', + url: locations[0].url + '/monitors', + }) + ); + }); + }); + + describe('pushConfigs', () => { + it('does not include the isEdit flag on normal push requests', async () => { + const { service, locations } = getMockedService(); + + (axios as jest.MockedFunction).mockResolvedValue({} as AxiosResponse); + + const payload = getFakePayload([locations[0]]); + + await service.pushConfigs([payload] as SyntheticsConfig[]); + + expect(axios).toHaveBeenCalledTimes(1); + expect(axios).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ is_edit: false }), + }) + ); + }); + + it('includes the isEdit flag on edit requests', async () => { + const { service, locations } = getMockedService(); + + (axios as jest.MockedFunction).mockResolvedValue({} as AxiosResponse); + + const payload = getFakePayload([locations[0]]); + + await service.pushConfigs([payload] as SyntheticsConfig[], true); + + expect(axios).toHaveBeenCalledTimes(1); + expect(axios).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ is_edit: true }), }) ); }); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts index b1af1717e1a1c..35861786a38dc 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -251,7 +251,7 @@ export class SyntheticsService { } } - async pushConfigs(configs?: SyntheticsConfig[]) { + async pushConfigs(configs?: SyntheticsConfig[], isEdit?: boolean) { const monitors = this.formatConfigs(configs || (await this.getMonitorConfigs())); if (monitors.length === 0) { this.logger.debug('No monitor found which can be pushed to service.'); @@ -267,6 +267,7 @@ export class SyntheticsService { const data = { monitors, output: await this.getOutput(this.apiKey), + isEdit: !!isEdit, }; this.logger.debug(`${monitors.length} monitors will be pushed to synthetics service.`); From 09039ac3364bf3c939070a03117bf4e2b7b7407f Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Mon, 23 May 2022 16:20:57 +0200 Subject: [PATCH 091/120] [Osquery] Change discover to open in a new tab (#131894) --- .../cypress/integration/all/discover.spec.ts | 39 +++++++++++++++ .../cypress/integration/all/packs.spec.ts | 50 ++++++++++++------- .../packs/pack_queries_status_table.tsx | 7 ++- .../scripts/roles_users/soc_manager/role.json | 2 +- 4 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/osquery/cypress/integration/all/discover.spec.ts diff --git a/x-pack/plugins/osquery/cypress/integration/all/discover.spec.ts b/x-pack/plugins/osquery/cypress/integration/all/discover.spec.ts new file mode 100644 index 0000000000000..3e47b983dcda4 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/integration/all/discover.spec.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { login } from '../../tasks/login'; +import { navigateTo } from '../../tasks/navigation'; +import { checkResults, inputQuery, selectAllAgents, submitQuery } from '../../tasks/live_query'; +import { ROLES } from '../../test'; + +// TODO: So far just one test, but this is a good place to start. Move tests from pack view into here. +describe('ALL - Discover', () => { + beforeEach(() => { + login(ROLES.soc_manager); + navigateTo('/app/osquery'); + }); + + it('should be opened in new tab in results table', () => { + cy.contains('New live query').click(); + selectAllAgents(); + inputQuery('select * from uptime; '); + submitQuery(); + checkResults(); + cy.contains('View in Lens').should('exist'); + cy.contains('View in Discover') + .should('exist') + .should('have.attr', 'href') + .then(($href) => { + // @ts-expect-error-next-line href string - check types + cy.visit($href); + cy.getBySel('breadcrumbs').contains('Discover').should('exist'); + cy.getBySel('discoverDocTable', { timeout: 60000 }).contains( + 'action_data.queryselect * from uptime' + ); + }); + }); +}); diff --git a/x-pack/plugins/osquery/cypress/integration/all/packs.spec.ts b/x-pack/plugins/osquery/cypress/integration/all/packs.spec.ts index b765a9d16ef7e..4a8842d21c9b1 100644 --- a/x-pack/plugins/osquery/cypress/integration/all/packs.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/all/packs.spec.ts @@ -59,7 +59,7 @@ describe('ALL - Packs', () => { cy.react('EuiFormRow', { props: { label: 'Interval (s)' } }) .click() .clear() - .type('500'); + .type('10'); cy.react('EuiFlyoutFooter').react('EuiButton').contains('Save').click(); cy.react('EuiTableRow').contains(SAVED_QUERY_ID); findAndClickButton('Save pack'); @@ -96,21 +96,7 @@ describe('ALL - Packs', () => { cy.contains('ID must be unique').should('exist'); cy.react('EuiFlyoutFooter').react('EuiButtonEmpty').contains('Cancel').click(); }); - // THIS TESTS TAKES TOO LONG FOR NOW - LET ME THINK IT THROUGH - it.skip('to click the icon and visit discover', () => { - preparePack(PACK_NAME); - cy.react('CustomItemAction', { - props: { index: 0, item: { id: SAVED_QUERY_ID } }, - }).click(); - cy.getBySel('superDatePickerToggleQuickMenuButton').click(); - cy.getBySel('superDatePickerToggleRefreshButton').click(); - cy.getBySel('superDatePickerRefreshIntervalInput').clear().type('10'); - cy.get('button').contains('Apply').click(); - cy.getBySel('discoverDocTable', { timeout: 60000 }).contains( - `pack_${PACK_NAME}_${SAVED_QUERY_ID}` - ); - }); - it.skip('by clicking in Lens button', () => { + it('should open lens in new tab', () => { let lensUrl = ''; cy.window().then((win) => { cy.stub(win, 'open') @@ -122,17 +108,43 @@ describe('ALL - Packs', () => { preparePack(PACK_NAME); cy.react('CustomItemAction', { props: { index: 1, item: { id: SAVED_QUERY_ID } }, - }).click(); + }) + .should('exist') + .click(); cy.window() .its('open') .then(() => { cy.visit(lensUrl); }); - cy.getBySel('lnsWorkspace'); + cy.getBySel('lnsWorkspace').should('exist'); cy.getBySel('breadcrumbs').contains(`Action pack_${PACK_NAME}_${SAVED_QUERY_ID} results`); }); - // strange behaviour with modal + // TODO extremely strange behaviour with Cypress not finding Discover's page elements + // it('should open discover in new tab', () => { + // preparePack(PACK_NAME); + // cy.wait(1000); + // cy.react('CustomItemAction', { + // props: { index: 0, item: { id: SAVED_QUERY_ID } }, + // }) + // .should('exist') + // .within(() => { + // cy.get('a') + // .should('have.attr', 'href') + // .then(($href) => { + // // @ts-expect-error-next-line href string - check types + // cy.visit($href); + // cy.getBySel('breadcrumbs').contains('Discover').should('exist'); + // cy.contains(`action_id: pack_${PACK_NAME}_${SAVED_QUERY_ID}`); + // cy.getBySel('superDatePickerToggleQuickMenuButton').click(); + // cy.getBySel('superDatePickerCommonlyUsed_Today').click(); + // cy.getBySel('discoverDocTable', { timeout: 60000 }).contains( + // `pack_${PACK_NAME}_${SAVED_QUERY_ID}` + // ); + // }); + // }); + // }); + it('activate and deactive pack', () => { cy.contains('Packs').click(); cy.react('ActiveStateSwitchComponent', { diff --git a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx index 3aa345f986493..178bbe3536834 100644 --- a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx +++ b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx @@ -355,7 +355,12 @@ const ViewResultsInDiscoverActionComponent: React.FC - + ); }; diff --git a/x-pack/plugins/osquery/scripts/roles_users/soc_manager/role.json b/x-pack/plugins/osquery/scripts/roles_users/soc_manager/role.json index e3c97c0fe6560..e427fece0ea01 100644 --- a/x-pack/plugins/osquery/scripts/roles_users/soc_manager/role.json +++ b/x-pack/plugins/osquery/scripts/roles_users/soc_manager/role.json @@ -3,7 +3,7 @@ "cluster": ["manage"], "indices": [ { - "names": [".items-*", ".lists-*", ".alerts-security.alerts-*", ".siem-signals-*"], + "names": [".items-*", ".lists-*", ".alerts-security.alerts-*", ".siem-signals-*", "logs-*"], "privileges": ["manage", "read", "write", "view_index_metadata", "maintenance"] }, { From 9a8993268d9531a7036140e63c741b0d5c5cebdd Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 23 May 2022 16:37:52 +0200 Subject: [PATCH 092/120] Use kibana feature privileges with api key (#130918) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Joe Portner --- packages/kbn-utility-types/src/index.ts | 12 + .../api_keys/api_keys.test.mock.ts | 20 ++ .../authentication/api_keys/api_keys.test.ts | 206 ++++++++++- .../authentication/api_keys/api_keys.ts | 110 +++++- .../server/authentication/api_keys/index.ts | 2 +- .../authentication_service.test.ts | 4 + .../authentication/authentication_service.ts | 7 + x-pack/plugins/security/server/lib/index.ts | 13 + .../security/server/lib/role_schema.ts | 212 +++++++++++ .../security/server/lib/role_utils.test.ts | 26 ++ .../plugins/security/server/lib/role_utils.ts | 109 ++++++ x-pack/plugins/security/server/plugin.ts | 8 +- .../server/routes/api_keys/create.test.ts | 1 + .../security/server/routes/api_keys/create.ts | 51 ++- .../server/routes/authorization/roles/get.ts | 2 +- .../routes/authorization/roles/get_all.ts | 2 +- .../routes/authorization/roles/model/index.ts | 9 +- .../roles/model/put_payload.test.ts | 3 +- .../authorization/roles/model/put_payload.ts | 339 ++---------------- .../server/routes/authorization/roles/put.ts | 14 +- .../routes/monitor_cruds/add_monitor.ts | 20 +- .../server/synthetics_service/get_api_key.ts | 5 +- .../api_integration/apis/security/api_keys.ts | 35 ++ .../apis/uptime/rest/add_monitor.ts | 101 +++++- .../apis/uptime/rest/synthetics_enablement.ts | 8 +- 25 files changed, 939 insertions(+), 380 deletions(-) create mode 100644 x-pack/plugins/security/server/authentication/api_keys/api_keys.test.mock.ts create mode 100644 x-pack/plugins/security/server/lib/index.ts create mode 100644 x-pack/plugins/security/server/lib/role_schema.ts create mode 100644 x-pack/plugins/security/server/lib/role_utils.test.ts create mode 100644 x-pack/plugins/security/server/lib/role_utils.ts diff --git a/packages/kbn-utility-types/src/index.ts b/packages/kbn-utility-types/src/index.ts index 50741bbd96954..6d040d7aac8f7 100644 --- a/packages/kbn-utility-types/src/index.ts +++ b/packages/kbn-utility-types/src/index.ts @@ -110,3 +110,15 @@ export type PublicMethodsOf = Pick>; export type Writable = { -readonly [K in keyof T]: T[K]; }; + +/** + * XOR for some properties applied to a type + * (XOR is one of these but not both or neither) + * + * Usage: OneOf + * + * To require aria-label or aria-labelledby but not both + * Example: OneOf + */ +export type OneOf = Omit & + { [k in K]: Pick, k> & { [k1 in Exclude]?: never } }[K]; diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.mock.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.mock.ts new file mode 100644 index 0000000000000..47cd7688fb468 --- /dev/null +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.mock.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as Lib from '../../lib'; + +export const mockValidateKibanaPrivileges = jest.fn() as jest.MockedFunction< + typeof Lib['validateKibanaPrivileges'] +>; + +jest.mock('../../lib', () => { + const actual = jest.requireActual('../../lib'); + return { + ...actual, + validateKibanaPrivileges: mockValidateKibanaPrivileges, + }; +}); diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts index b5332a7296062..2aa318acff59d 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts @@ -5,12 +5,16 @@ * 2.0. */ +// eslint-disable-next-line import/order +import { mockValidateKibanaPrivileges } from './api_keys.test.mock'; + import { elasticsearchServiceMock, httpServerMock, loggingSystemMock, } from '@kbn/core/server/mocks'; +import { ALL_SPACES_ID } from '../../../common/constants'; import type { SecurityLicense } from '../../../common/licensing'; import { licenseMock } from '../../../common/licensing/index.mock'; import { APIKeys } from './api_keys'; @@ -26,6 +30,8 @@ describe('API Keys', () => { let mockLicense: jest.Mocked; beforeEach(() => { + mockValidateKibanaPrivileges.mockReset().mockReturnValue({ validationErrors: [] }); + mockClusterClient = elasticsearchServiceMock.createClusterClient(); mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockClusterClient.asScoped.mockReturnValue(mockScopedClusterClient); @@ -37,6 +43,8 @@ describe('API Keys', () => { clusterClient: mockClusterClient, logger: loggingSystemMock.create().get('api-keys'), license: mockLicense, + applicationName: 'kibana-.kibana', + kibanaFeatures: [], }); }); @@ -135,6 +143,32 @@ describe('API Keys', () => { role_descriptors: {}, }); expect(result).toBeNull(); + expect(mockValidateKibanaPrivileges).not.toHaveBeenCalled(); + expect(mockScopedClusterClient.asCurrentUser.security.createApiKey).not.toHaveBeenCalled(); + }); + + it('throws an error when kibana privilege validation fails', async () => { + mockLicense.isEnabled.mockReturnValue(true); + mockValidateKibanaPrivileges + .mockReturnValueOnce({ validationErrors: ['error1'] }) // for descriptor1 + .mockReturnValueOnce({ validationErrors: [] }) // for descriptor2 + .mockReturnValueOnce({ validationErrors: ['error2'] }); // for descriptor3 + + await expect( + apiKeys.create(httpServerMock.createKibanaRequest(), { + name: 'key-name', + kibana_role_descriptors: { + descriptor1: { elasticsearch: {}, kibana: [] }, + descriptor2: { elasticsearch: {}, kibana: [] }, + descriptor3: { elasticsearch: {}, kibana: [] }, + }, + expiration: '1d', + }) + ).rejects.toEqual( + // The validation errors from descriptor1 and descriptor3 are concatenated into the final error message + new Error('API key cannot be created due to validation errors: ["error1","error2"]') + ); + expect(mockValidateKibanaPrivileges).toHaveBeenCalledTimes(3); expect(mockScopedClusterClient.asCurrentUser.security.createApiKey).not.toHaveBeenCalled(); }); @@ -159,6 +193,7 @@ describe('API Keys', () => { id: '123', name: 'key-name', }); + expect(mockValidateKibanaPrivileges).not.toHaveBeenCalled(); // this is only called if kibana_role_descriptors is defined expect(mockScopedClusterClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({ body: { name: 'key-name', @@ -177,7 +212,37 @@ describe('API Keys', () => { role_descriptors: {}, }); expect(result).toBeNull(); + expect(mockValidateKibanaPrivileges).not.toHaveBeenCalled(); + expect(mockClusterClient.asInternalUser.security.grantApiKey).not.toHaveBeenCalled(); + }); + + it('throws an error when kibana privilege validation fails', async () => { + mockLicense.isEnabled.mockReturnValue(true); + mockValidateKibanaPrivileges + .mockReturnValueOnce({ validationErrors: ['error1'] }) // for descriptor1 + .mockReturnValueOnce({ validationErrors: [] }) // for descriptor2 + .mockReturnValueOnce({ validationErrors: ['error2'] }); // for descriptor3 + await expect( + apiKeys.grantAsInternalUser( + httpServerMock.createKibanaRequest({ + headers: { authorization: `Basic ${encodeToBase64('foo:bar')}` }, + }), + { + name: 'key-name', + kibana_role_descriptors: { + descriptor1: { elasticsearch: {}, kibana: [] }, + descriptor2: { elasticsearch: {}, kibana: [] }, + descriptor3: { elasticsearch: {}, kibana: [] }, + }, + expiration: '1d', + } + ) + ).rejects.toEqual( + // The validation errors from descriptor1 and descriptor3 are concatenated into the final error message + new Error('API key cannot be created due to validation errors: ["error1","error2"]') + ); + expect(mockValidateKibanaPrivileges).toHaveBeenCalledTimes(3); expect(mockClusterClient.asInternalUser.security.grantApiKey).not.toHaveBeenCalled(); }); @@ -192,9 +257,7 @@ describe('API Keys', () => { }); const result = await apiKeys.grantAsInternalUser( httpServerMock.createKibanaRequest({ - headers: { - authorization: `Basic ${encodeToBase64('foo:bar')}`, - }, + headers: { authorization: `Basic ${encodeToBase64('foo:bar')}` }, }), { name: 'test_api_key', @@ -208,6 +271,7 @@ describe('API Keys', () => { name: 'key-name', expires: '1d', }); + expect(mockValidateKibanaPrivileges).not.toHaveBeenCalled(); // this is only called if kibana_role_descriptors is defined expect(mockClusterClient.asInternalUser.security.grantApiKey).toHaveBeenCalledWith({ body: { api_key: { @@ -231,9 +295,7 @@ describe('API Keys', () => { }); const result = await apiKeys.grantAsInternalUser( httpServerMock.createKibanaRequest({ - headers: { - authorization: `Bearer foo-access-token`, - }, + headers: { authorization: `Bearer foo-access-token` }, }), { name: 'test_api_key', @@ -246,6 +308,7 @@ describe('API Keys', () => { id: '123', name: 'key-name', }); + expect(mockValidateKibanaPrivileges).not.toHaveBeenCalled(); // this is only called if kibana_role_descriptors is defined expect(mockClusterClient.asInternalUser.security.grantApiKey).toHaveBeenCalledWith({ body: { api_key: { @@ -277,6 +340,7 @@ describe('API Keys', () => { ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unsupported scheme \\"Digest\\" for granting API Key"` ); + expect(mockValidateKibanaPrivileges).not.toHaveBeenCalled(); expect(mockClusterClient.asInternalUser.security.grantApiKey).not.toHaveBeenCalled(); }); }); @@ -398,4 +462,134 @@ describe('API Keys', () => { }); }); }); + + describe('with kibana privileges', () => { + it('creates api key with application privileges', async () => { + mockLicense.isEnabled.mockReturnValue(true); + + mockScopedClusterClient.asCurrentUser.security.createApiKey.mockResponseOnce({ + id: '123', + name: 'key-name', + // @ts-expect-error @elastic/elsticsearch CreateApiKeyResponse.expiration: number + expiration: '1d', + api_key: 'abc123', + }); + const result = await apiKeys.create(httpServerMock.createKibanaRequest(), { + name: 'key-name', + kibana_role_descriptors: { + synthetics_writer: { + elasticsearch: { cluster: ['manage'], indices: [], run_as: [] }, + kibana: [ + { + base: [], + spaces: [ALL_SPACES_ID], + feature: { + uptime: ['all'], + }, + }, + ], + }, + }, + expiration: '1d', + }); + expect(result).toEqual({ + api_key: 'abc123', + expiration: '1d', + id: '123', + name: 'key-name', + }); + expect(mockScopedClusterClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({ + body: { + name: 'key-name', + role_descriptors: { + synthetics_writer: { + applications: [ + { + application: 'kibana-.kibana', + privileges: ['feature_uptime.all'], + resources: ['*'], + }, + ], + cluster: ['manage'], + indices: [], + run_as: [], + }, + }, + expiration: '1d', + }, + }); + }); + + it('creates api key with application privileges as internal user', async () => { + mockLicense.isEnabled.mockReturnValue(true); + + mockClusterClient.asInternalUser.security.grantApiKey.mockResponseOnce({ + id: '123', + name: 'key-name', + api_key: 'abc123', + // @ts-expect-error invalid definition + expires: '1d', + }); + const result = await apiKeys.grantAsInternalUser( + httpServerMock.createKibanaRequest({ + headers: { + authorization: `Basic ${encodeToBase64('foo:bar')}`, + }, + }), + { + name: 'key-name', + kibana_role_descriptors: { + synthetics_writer: { + elasticsearch: { + cluster: ['manage'], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + spaces: [ALL_SPACES_ID], + feature: { + uptime: ['all'], + }, + }, + ], + }, + }, + expiration: '1d', + } + ); + expect(result).toEqual({ + api_key: 'abc123', + expires: '1d', + id: '123', + name: 'key-name', + }); + expect(mockClusterClient.asInternalUser.security.grantApiKey).toHaveBeenCalledWith({ + body: { + api_key: { + name: 'key-name', + role_descriptors: { + synthetics_writer: { + applications: [ + { + application: 'kibana-.kibana', + privileges: ['feature_uptime.all'], + resources: ['*'], + }, + ], + cluster: ['manage'], + indices: [], + run_as: [], + }, + }, + expiration: '1d', + }, + grant_type: 'password', + password: 'bar', + username: 'foo', + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts index 82b27212182d6..0eef6fac74035 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts @@ -5,9 +5,15 @@ * 2.0. */ +/* eslint-disable max-classes-per-file */ + import type { IClusterClient, KibanaRequest, Logger } from '@kbn/core/server'; +import type { KibanaFeature } from '@kbn/features-plugin/server'; +import type { OneOf } from '@kbn/utility-types'; import type { SecurityLicense } from '../../../common/licensing'; +import type { ElasticsearchPrivilegesType, KibanaPrivilegesType } from '../../lib'; +import { transformPrivilegesToElasticsearchPrivileges, validateKibanaPrivileges } from '../../lib'; import { BasicHTTPAuthorizationHeaderCredentials, HTTPAuthorizationHeader, @@ -21,18 +27,29 @@ export interface ConstructorOptions { logger: Logger; clusterClient: IClusterClient; license: SecurityLicense; + applicationName: string; + kibanaFeatures: KibanaFeature[]; } -/** - * Represents the params for creating an API key - */ -export interface CreateAPIKeyParams { +interface BaseCreateAPIKeyParams { name: string; - role_descriptors: Record; expiration?: string; metadata?: Record; + role_descriptors: Record; + kibana_role_descriptors: Record< + string, + { elasticsearch: ElasticsearchPrivilegesType; kibana: KibanaPrivilegesType } + >; } +/** + * Represents the params for creating an API key + */ +export type CreateAPIKeyParams = OneOf< + BaseCreateAPIKeyParams, + 'role_descriptors' | 'kibana_role_descriptors' +>; + type GrantAPIKeyParams = | { api_key: CreateAPIKeyParams; @@ -129,11 +146,21 @@ export class APIKeys { private readonly logger: Logger; private readonly clusterClient: IClusterClient; private readonly license: SecurityLicense; - - constructor({ logger, clusterClient, license }: ConstructorOptions) { + private readonly applicationName: string; + private readonly kibanaFeatures: KibanaFeature[]; + + constructor({ + logger, + clusterClient, + license, + applicationName, + kibanaFeatures, + }: ConstructorOptions) { this.logger = logger; this.clusterClient = clusterClient; this.license = license; + this.applicationName = applicationName; + this.kibanaFeatures = kibanaFeatures; } /** @@ -171,30 +198,33 @@ export class APIKeys { * Returns newly created API key or `null` if API keys are disabled. * * @param request Request instance. - * @param params The params to create an API key + * @param createParams The params to create an API key */ async create( request: KibanaRequest, - params: CreateAPIKeyParams + createParams: CreateAPIKeyParams ): Promise { if (!this.license.isEnabled()) { return null; } + const { expiration, metadata, name } = createParams; + + const roleDescriptors = this.parseRoleDescriptorsWithKibanaPrivileges(createParams); + this.logger.debug('Trying to create an API key'); // User needs `manage_api_key` privilege to use this API let result: CreateAPIKeyResult; try { - result = await this.clusterClient - .asScoped(request) - .asCurrentUser.security.createApiKey({ body: params }); + result = await this.clusterClient.asScoped(request).asCurrentUser.security.createApiKey({ + body: { role_descriptors: roleDescriptors, name, metadata, expiration }, + }); this.logger.debug('API key was created successfully'); } catch (e) { this.logger.error(`Failed to create API key: ${e.message}`); throw e; } - return result; } @@ -215,7 +245,14 @@ export class APIKeys { `Unable to grant an API Key, request does not contain an authorization header` ); } - const params = this.getGrantParams(createParams, authorizationHeader); + const { expiration, metadata, name } = createParams; + + const roleDescriptors = this.parseRoleDescriptorsWithKibanaPrivileges(createParams); + + const params = this.getGrantParams( + { expiration, metadata, name, role_descriptors: roleDescriptors }, + authorizationHeader + ); // User needs `manage_api_key` or `grant_api_key` privilege to use this API let result: GrantAPIKeyResult; @@ -329,4 +366,49 @@ export class APIKeys { throw new Error(`Unsupported scheme "${authorizationHeader.scheme}" for granting API Key`); } + + private parseRoleDescriptorsWithKibanaPrivileges(createParams: CreateAPIKeyParams) { + if (createParams.role_descriptors) { + return createParams.role_descriptors; + } + + const roleDescriptors = Object.create(null); + + const { kibana_role_descriptors: kibanaRoleDescriptors } = createParams; + + const allValidationErrors: string[] = []; + if (kibanaRoleDescriptors) { + Object.entries(kibanaRoleDescriptors).forEach(([roleKey, roleDescriptor]) => { + const { validationErrors } = validateKibanaPrivileges( + this.kibanaFeatures, + roleDescriptor.kibana + ); + allValidationErrors.push(...validationErrors); + + const applications = transformPrivilegesToElasticsearchPrivileges( + this.applicationName, + roleDescriptor.kibana + ); + if (applications.length > 0 && roleDescriptors) { + roleDescriptors[roleKey] = { + ...roleDescriptor.elasticsearch, + applications, + }; + } + }); + } + if (allValidationErrors.length) { + throw new CreateApiKeyValidationError( + `API key cannot be created due to validation errors: ${JSON.stringify(allValidationErrors)}` + ); + } + + return roleDescriptors; + } +} + +export class CreateApiKeyValidationError extends Error { + constructor(message: string) { + super(message); + } } diff --git a/x-pack/plugins/security/server/authentication/api_keys/index.ts b/x-pack/plugins/security/server/authentication/api_keys/index.ts index 44fe48f04debb..c3ef6f6f139a9 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/index.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/index.ts @@ -12,4 +12,4 @@ export type { InvalidateAPIKeysParams, GrantAPIKeyResult, } from './api_keys'; -export { APIKeys } from './api_keys'; +export { APIKeys, CreateApiKeyValidationError } from './api_keys'; diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.ts index 585dd54dd8d5a..8882797380397 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.test.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.test.ts @@ -69,6 +69,8 @@ describe('AuthenticationService', () => { clusterClient: ReturnType; featureUsageService: jest.Mocked; session: jest.Mocked>; + applicationName: 'kibana-.kibana'; + kibanaFeatures: []; }; beforeEach(() => { logger = loggingSystemMock.createLogger(); @@ -107,6 +109,8 @@ describe('AuthenticationService', () => { loggers: loggingSystemMock.create(), featureUsageService: securityFeatureUsageServiceMock.createStartContract(), session: sessionMock.create(), + applicationName: 'kibana-.kibana', + kibanaFeatures: [], }; (mockStartAuthenticationParams.http.basePath.get as jest.Mock).mockImplementation( () => mockStartAuthenticationParams.http.basePath.serverBasePath diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index ed461b0148a89..0318fe3823a46 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -14,6 +14,7 @@ import type { Logger, LoggerFactory, } from '@kbn/core/server'; +import type { KibanaFeature } from '@kbn/features-plugin/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; import type { AuthenticatedUser, SecurityLicense } from '../../common'; @@ -49,6 +50,8 @@ interface AuthenticationServiceStartParams { featureUsageService: SecurityFeatureUsageServiceStart; session: PublicMethodsOf; loggers: LoggerFactory; + applicationName: string; + kibanaFeatures: KibanaFeature[]; } export interface InternalAuthenticationServiceStart extends AuthenticationServiceStart { @@ -296,11 +299,15 @@ export class AuthenticationService { http, loggers, session, + applicationName, + kibanaFeatures, }: AuthenticationServiceStartParams): InternalAuthenticationServiceStart { const apiKeys = new APIKeys({ clusterClient, logger: this.logger.get('api-key'), license: this.license, + applicationName, + kibanaFeatures, }); /** diff --git a/x-pack/plugins/security/server/lib/index.ts b/x-pack/plugins/security/server/lib/index.ts new file mode 100644 index 0000000000000..1a1ae84e2af65 --- /dev/null +++ b/x-pack/plugins/security/server/lib/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { ElasticsearchPrivilegesType, KibanaPrivilegesType } from './role_schema'; +export { elasticsearchRoleSchema, getKibanaRoleSchema } from './role_schema'; +export { + validateKibanaPrivileges, + transformPrivilegesToElasticsearchPrivileges, +} from './role_utils'; diff --git a/x-pack/plugins/security/server/lib/role_schema.ts b/x-pack/plugins/security/server/lib/role_schema.ts new file mode 100644 index 0000000000000..135a24cf04085 --- /dev/null +++ b/x-pack/plugins/security/server/lib/role_schema.ts @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import _ from 'lodash'; + +import type { TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; + +import { GLOBAL_RESOURCE } from '../../common/constants'; + +/** + * Elasticsearch specific portion of the role definition. + * See more details at https://www.elastic.co/guide/en/elasticsearch/reference/master/security-api.html#security-role-apis. + */ +export const elasticsearchRoleSchema = schema.object({ + /** + * An optional list of cluster privileges. These privileges define the cluster level actions that + * users with this role are able to execute + */ + cluster: schema.maybe(schema.arrayOf(schema.string())), + + /** + * An optional list of indices permissions entries. + */ + indices: schema.maybe( + schema.arrayOf( + schema.object({ + /** + * Required list of indices (or index name patterns) to which the permissions in this + * entry apply. + */ + names: schema.arrayOf(schema.string(), { minSize: 1 }), + + /** + * An optional set of the document fields that the owners of the role have read access to. + */ + field_security: schema.maybe( + schema.recordOf( + schema.oneOf([schema.literal('grant'), schema.literal('except')]), + schema.arrayOf(schema.string()) + ) + ), + + /** + * Required list of the index level privileges that the owners of the role have on the + * specified indices. + */ + privileges: schema.arrayOf(schema.string(), { minSize: 1 }), + + /** + * An optional search query that defines the documents the owners of the role have read access + * to. A document within the specified indices must match this query in order for it to be + * accessible by the owners of the role. + */ + query: schema.maybe(schema.string()), + + /** + * An optional flag used to indicate if index pattern wildcards or regexps should cover + * restricted indices. + */ + allow_restricted_indices: schema.maybe(schema.boolean()), + }) + ) + ), + + /** + * An optional list of users that the owners of this role can impersonate. + */ + run_as: schema.maybe(schema.arrayOf(schema.string())), +}); + +const allSpacesSchema = schema.arrayOf(schema.literal(GLOBAL_RESOURCE), { + minSize: 1, + maxSize: 1, +}); + +/** + * Schema for the list of space IDs used within Kibana specific role definition. + */ +const spacesSchema = schema.oneOf( + [ + allSpacesSchema, + schema.arrayOf( + schema.string({ + validate(value) { + if (!/^[a-z0-9_-]+$/.test(value)) { + return `must be lower case, a-z, 0-9, '_', and '-' are allowed`; + } + }, + }) + ), + ], + { defaultValue: [GLOBAL_RESOURCE] } +); + +const FEATURE_NAME_VALUE_REGEX = /^[a-zA-Z0-9_-]+$/; + +/** + * Kibana specific portion of the role definition. It's represented as a list of base and/or + * feature Kibana privileges. None of the entries should apply to the same spaces. + */ +export const getKibanaRoleSchema = ( + getBasePrivilegeNames: () => { global: string[]; space: string[] } +) => + schema.arrayOf( + schema.object( + { + /** + * An optional list of space IDs to which the permissions in this entry apply. If not + * specified it defaults to special "global" space ID (all spaces). + */ + spaces: spacesSchema, + + /** + * An optional list of Kibana base privileges. If this entry applies to special "global" + * space (all spaces) then specified base privileges should be within known base "global" + * privilege list, otherwise - within known "space" privilege list. Base privileges + * definition isn't allowed when feature privileges are defined and required otherwise. + */ + base: schema.maybe( + schema.conditional( + schema.siblingRef('spaces'), + allSpacesSchema, + schema.arrayOf( + schema.string({ + validate(value) { + const globalPrivileges = getBasePrivilegeNames().global; + if (!globalPrivileges.some((privilege) => privilege === value)) { + return `unknown global privilege "${value}", must be one of [${globalPrivileges}]`; + } + }, + }) + ), + schema.arrayOf( + schema.string({ + validate(value) { + const spacePrivileges = getBasePrivilegeNames().space; + if (!spacePrivileges.some((privilege) => privilege === value)) { + return `unknown space privilege "${value}", must be one of [${spacePrivileges}]`; + } + }, + }) + ) + ) + ), + + /** + * An optional dictionary of Kibana feature privileges where the key is the ID of the + * feature and the value is a list of feature specific privilege IDs. Both feature and + * privilege IDs should consist of allowed set of characters. Feature privileges + * definition isn't allowed when base privileges are defined and required otherwise. + */ + feature: schema.maybe( + schema.recordOf( + schema.string({ + validate(value) { + if (!FEATURE_NAME_VALUE_REGEX.test(value)) { + return `only a-z, A-Z, 0-9, '_', and '-' are allowed`; + } + }, + }), + schema.arrayOf( + schema.string({ + validate(value) { + if (!FEATURE_NAME_VALUE_REGEX.test(value)) { + return `only a-z, A-Z, 0-9, '_', and '-' are allowed`; + } + }, + }) + ) + ) + ), + }, + { + validate(value) { + if ( + (value.base === undefined || value.base.length === 0) && + (value.feature === undefined || Object.values(value.feature).flat().length === 0) + ) { + return 'either [base] or [feature] is expected, but none of them specified'; + } + + if ( + value.base !== undefined && + value.base.length > 0 && + value.feature !== undefined && + Object.keys(value.feature).length > 0 + ) { + return `definition of [feature] isn't allowed when non-empty [base] is defined.`; + } + }, + } + ), + { + validate(value) { + for (const [indexA, valueA] of value.entries()) { + for (const valueB of value.slice(indexA + 1)) { + const spaceIntersection = _.intersection(valueA.spaces, valueB.spaces); + if (spaceIntersection.length !== 0) { + return `more than one privilege is applied to the following spaces: [${spaceIntersection}]`; + } + } + } + }, + } + ); + +export type ElasticsearchPrivilegesType = TypeOf; +export type KibanaPrivilegesType = TypeOf>; diff --git a/x-pack/plugins/security/server/lib/role_utils.test.ts b/x-pack/plugins/security/server/lib/role_utils.test.ts new file mode 100644 index 0000000000000..6c04b6121c2d1 --- /dev/null +++ b/x-pack/plugins/security/server/lib/role_utils.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ALL_SPACES_ID } from '../../common/constants'; +import { transformPrivilegesToElasticsearchPrivileges } from './role_utils'; + +describe('transformPrivilegesToElasticsearchPrivileges', () => { + test('returns expected result', () => { + expect( + transformPrivilegesToElasticsearchPrivileges('kibana,-kibana', [ + { + spaces: [ALL_SPACES_ID], + feature: { + uptime: ['all'], + }, + }, + ]) + ).toEqual([ + { application: 'kibana,-kibana', privileges: ['feature_uptime.all'], resources: ['*'] }, + ]); + }); +}); diff --git a/x-pack/plugins/security/server/lib/role_utils.ts b/x-pack/plugins/security/server/lib/role_utils.ts new file mode 100644 index 0000000000000..c852079081821 --- /dev/null +++ b/x-pack/plugins/security/server/lib/role_utils.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaFeature } from '@kbn/features-plugin/server'; + +import { ALL_SPACES_ID, GLOBAL_RESOURCE } from '../../common/constants'; +import { PrivilegeSerializer } from '../authorization/privilege_serializer'; +import { ResourceSerializer } from '../authorization/resource_serializer'; +import type { KibanaPrivilegesType } from './role_schema'; + +export const transformPrivilegesToElasticsearchPrivileges = ( + application: string, + kibanaPrivileges: KibanaPrivilegesType = [] +) => { + return kibanaPrivileges.map(({ base, feature, spaces }) => { + if (spaces.length === 1 && spaces[0] === GLOBAL_RESOURCE) { + return { + privileges: [ + ...(base + ? base.map((privilege) => PrivilegeSerializer.serializeGlobalBasePrivilege(privilege)) + : []), + ...(feature + ? Object.entries(feature) + .map(([featureName, featurePrivileges]) => + featurePrivileges.map((privilege) => + PrivilegeSerializer.serializeFeaturePrivilege(featureName, privilege) + ) + ) + .flat() + : []), + ], + application, + resources: [GLOBAL_RESOURCE], + }; + } + + return { + privileges: [ + ...(base + ? base.map((privilege) => PrivilegeSerializer.serializeSpaceBasePrivilege(privilege)) + : []), + ...(feature + ? Object.entries(feature) + .map(([featureName, featurePrivileges]) => + featurePrivileges.map((privilege) => + PrivilegeSerializer.serializeFeaturePrivilege(featureName, privilege) + ) + ) + .flat() + : []), + ], + application, + resources: (spaces as string[]).map((resource) => + ResourceSerializer.serializeSpaceResource(resource) + ), + }; + }); +}; + +export const validateKibanaPrivileges = ( + kibanaFeatures: KibanaFeature[], + kibanaPrivileges: KibanaPrivilegesType = [] +) => { + const validationErrors = kibanaPrivileges.flatMap((priv) => { + const forAllSpaces = priv.spaces.includes(ALL_SPACES_ID); + + return Object.entries(priv.feature ?? {}).flatMap(([featureId, feature]) => { + const errors: string[] = []; + const kibanaFeature = kibanaFeatures.find((f) => f.id === featureId); + if (!kibanaFeature) return errors; + + if (feature.includes('all')) { + if (kibanaFeature.privileges?.all.disabled) { + errors.push(`Feature [${featureId}] does not support privilege [all].`); + } + + if (kibanaFeature.privileges?.all.requireAllSpaces && !forAllSpaces) { + errors.push( + `Feature privilege [${featureId}.all] requires all spaces to be selected but received [${priv.spaces.join( + ',' + )}]` + ); + } + } + + if (feature.includes('read')) { + if (kibanaFeature.privileges?.read.disabled) { + errors.push(`Feature [${featureId}] does not support privilege [read].`); + } + + if (kibanaFeature.privileges?.read.requireAllSpaces && !forAllSpaces) { + errors.push( + `Feature privilege [${featureId}.read] requires all spaces to be selected but received [${priv.spaces.join( + ',' + )}]` + ); + } + } + + return errors; + }); + }); + + return { validationErrors }; +}; diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 6e3b67b3eedba..07b3e1ea232ec 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -366,9 +366,15 @@ export class SecurityPlugin http: core.http, loggers: this.initializerContext.logger, session, + applicationName: this.authorizationSetup!.applicationName, + kibanaFeatures: features.getKibanaFeatures(), }); - this.authorizationService.start({ features, clusterClient, online$: watchOnlineStatus$() }); + this.authorizationService.start({ + features, + clusterClient, + online$: watchOnlineStatus$(), + }); this.anonymousAccessStart = this.anonymousAccessService.start({ capabilities: core.capabilities, diff --git a/x-pack/plugins/security/server/routes/api_keys/create.test.ts b/x-pack/plugins/security/server/routes/api_keys/create.test.ts index dbf4c93639e47..22e4bb3df96d5 100644 --- a/x-pack/plugins/security/server/routes/api_keys/create.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/create.test.ts @@ -42,6 +42,7 @@ describe('Create API Key route', () => { }); describe('failure', () => { + test.todo('actually exercise different types of payload validation'); test('returns result of license checker', async () => { const mockContext = getMockContext({ state: 'invalid', message: 'test forbidden message' }); const response = await routeHandler( diff --git a/x-pack/plugins/security/server/routes/api_keys/create.ts b/x-pack/plugins/security/server/routes/api_keys/create.ts index b9e927592a492..8bb097ee01259 100644 --- a/x-pack/plugins/security/server/routes/api_keys/create.ts +++ b/x-pack/plugins/security/server/routes/api_keys/create.ts @@ -8,29 +8,53 @@ import { schema } from '@kbn/config-schema'; import type { RouteDefinitionParams } from '..'; +import { CreateApiKeyValidationError } from '../../authentication/api_keys'; import { wrapIntoCustomErrorResponse } from '../../errors'; +import { elasticsearchRoleSchema, getKibanaRoleSchema } from '../../lib'; import { createLicensedRouteHandler } from '../licensed_route_handler'; +const bodySchema = schema.object({ + name: schema.string(), + expiration: schema.maybe(schema.string()), + role_descriptors: schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' }), { + defaultValue: {}, + }), + metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })), +}); + +const getBodySchemaWithKibanaPrivileges = ( + getBasePrivilegeNames: () => { global: string[]; space: string[] } +) => + schema.object({ + name: schema.string(), + expiration: schema.maybe(schema.string()), + kibana_role_descriptors: schema.recordOf( + schema.string(), + schema.object({ + elasticsearch: elasticsearchRoleSchema.extends({}, { unknowns: 'allow' }), + kibana: getKibanaRoleSchema(getBasePrivilegeNames), + }) + ), + metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })), + }); + export function defineCreateApiKeyRoutes({ router, + authz, getAuthenticationService, }: RouteDefinitionParams) { + const bodySchemaWithKibanaPrivileges = getBodySchemaWithKibanaPrivileges(() => { + const privileges = authz.privileges.get(); + return { + global: Object.keys(privileges.global), + space: Object.keys(privileges.space), + }; + }); router.post( { path: '/internal/security/api_key', validate: { - body: schema.object({ - name: schema.string(), - expiration: schema.maybe(schema.string()), - role_descriptors: schema.recordOf( - schema.string(), - schema.object({}, { unknowns: 'allow' }), - { - defaultValue: {}, - } - ), - metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })), - }), + body: schema.oneOf([bodySchema, bodySchemaWithKibanaPrivileges]), }, }, createLicensedRouteHandler(async (context, request, response) => { @@ -43,6 +67,9 @@ export function defineCreateApiKeyRoutes({ return response.ok({ body: apiKey }); } catch (error) { + if (error instanceof CreateApiKeyValidationError) { + return response.badRequest({ body: { message: error.message } }); + } return response.customError(wrapIntoCustomErrorResponse(error)); } }) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index 428cd9b49dac4..8a8b688fd9bb5 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -8,9 +8,9 @@ import { schema } from '@kbn/config-schema'; import type { RouteDefinitionParams } from '../..'; +import { transformElasticsearchRoleToRole } from '../../../authorization'; import { wrapIntoCustomErrorResponse } from '../../../errors'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import { transformElasticsearchRoleToRole } from './model'; export function defineGetRolesRoutes({ router, diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index 757903c3e3dbe..c6407e3784763 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -6,9 +6,9 @@ */ import type { RouteDefinitionParams } from '../..'; +import { transformElasticsearchRoleToRole } from '../../../authorization'; import { wrapIntoCustomErrorResponse } from '../../../errors'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import { transformElasticsearchRoleToRole } from './model'; export function defineGetAllRolesRoutes({ router, diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts index ef27f20f09a55..f42d6f01573a9 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts @@ -5,10 +5,5 @@ * 2.0. */ -export type { ElasticsearchRole } from '../../../../authorization'; -export { transformElasticsearchRoleToRole } from '../../../../authorization'; -export { - getPutPayloadSchema, - transformPutPayloadToElasticsearchRole, - validateKibanaPrivileges, -} from './put_payload'; +export type { RolePayloadSchemaType } from './put_payload'; +export { transformPutPayloadToElasticsearchRole, getPutPayloadSchema } from './put_payload'; diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts index 7600c99a1caf7..842a3b74853b7 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts @@ -8,7 +8,8 @@ import { KibanaFeature } from '@kbn/features-plugin/common'; import { ALL_SPACES_ID } from '../../../../../common/constants'; -import { getPutPayloadSchema, validateKibanaPrivileges } from './put_payload'; +import { validateKibanaPrivileges } from '../../../../lib'; +import { getPutPayloadSchema } from './put_payload'; const basePrivilegeNamesMap = { global: ['all', 'read'], diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts index 62e4a499e2e1c..19ce403b77d86 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts @@ -5,233 +5,18 @@ * 2.0. */ -import _ from 'lodash'; - import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; -import type { KibanaFeature } from '@kbn/features-plugin/common'; - -import type { ElasticsearchRole } from '.'; -import { ALL_SPACES_ID, GLOBAL_RESOURCE } from '../../../../../common/constants'; -import { PrivilegeSerializer } from '../../../../authorization/privilege_serializer'; -import { ResourceSerializer } from '../../../../authorization/resource_serializer'; - -/** - * Elasticsearch specific portion of the role definition. - * See more details at https://www.elastic.co/guide/en/elasticsearch/reference/master/security-api.html#security-role-apis. - */ -const elasticsearchRoleSchema = schema.object({ - /** - * An optional list of cluster privileges. These privileges define the cluster level actions that - * users with this role are able to execute - */ - cluster: schema.maybe(schema.arrayOf(schema.string())), - - /** - * An optional list of indices permissions entries. - */ - indices: schema.maybe( - schema.arrayOf( - schema.object({ - /** - * Required list of indices (or index name patterns) to which the permissions in this - * entry apply. - */ - names: schema.arrayOf(schema.string(), { minSize: 1 }), - - /** - * An optional set of the document fields that the owners of the role have read access to. - */ - field_security: schema.maybe( - schema.recordOf( - schema.oneOf([schema.literal('grant'), schema.literal('except')]), - schema.arrayOf(schema.string()) - ) - ), - - /** - * Required list of the index level privileges that the owners of the role have on the - * specified indices. - */ - privileges: schema.arrayOf(schema.string(), { minSize: 1 }), - - /** - * An optional search query that defines the documents the owners of the role have read access - * to. A document within the specified indices must match this query in order for it to be - * accessible by the owners of the role. - */ - query: schema.maybe(schema.string()), - - /** - * An optional flag used to indicate if index pattern wildcards or regexps should cover - * restricted indices. - */ - allow_restricted_indices: schema.maybe(schema.boolean()), - }) - ) - ), - - /** - * An optional list of users that the owners of this role can impersonate. - */ - run_as: schema.maybe(schema.arrayOf(schema.string())), -}); - -const allSpacesSchema = schema.arrayOf(schema.literal(GLOBAL_RESOURCE), { - minSize: 1, - maxSize: 1, -}); - -/** - * Schema for the list of space IDs used within Kibana specific role definition. - */ -const spacesSchema = schema.oneOf( - [ - allSpacesSchema, - schema.arrayOf( - schema.string({ - validate(value) { - if (!/^[a-z0-9_-]+$/.test(value)) { - return `must be lower case, a-z, 0-9, '_', and '-' are allowed`; - } - }, - }) - ), - ], - { defaultValue: [GLOBAL_RESOURCE] } -); - -const FEATURE_NAME_VALUE_REGEX = /^[a-zA-Z0-9_-]+$/; - -type PutPayloadSchemaType = TypeOf>; -export function getPutPayloadSchema( - getBasePrivilegeNames: () => { global: string[]; space: string[] } -) { - return schema.object({ - /** - * An optional meta-data dictionary. Within the metadata, keys that begin with _ are reserved - * for system usage. - */ - metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())), - - /** - * Elasticsearch specific portion of the role definition. - */ - elasticsearch: elasticsearchRoleSchema, - - /** - * Kibana specific portion of the role definition. It's represented as a list of base and/or - * feature Kibana privileges. None of the entries should apply to the same spaces. - */ - kibana: schema.maybe( - schema.arrayOf( - schema.object( - { - /** - * An optional list of space IDs to which the permissions in this entry apply. If not - * specified it defaults to special "global" space ID (all spaces). - */ - spaces: spacesSchema, - /** - * An optional list of Kibana base privileges. If this entry applies to special "global" - * space (all spaces) then specified base privileges should be within known base "global" - * privilege list, otherwise - within known "space" privilege list. Base privileges - * definition isn't allowed when feature privileges are defined and required otherwise. - */ - base: schema.maybe( - schema.conditional( - schema.siblingRef('spaces'), - allSpacesSchema, - schema.arrayOf( - schema.string({ - validate(value) { - const globalPrivileges = getBasePrivilegeNames().global; - if (!globalPrivileges.some((privilege) => privilege === value)) { - return `unknown global privilege "${value}", must be one of [${globalPrivileges}]`; - } - }, - }) - ), - schema.arrayOf( - schema.string({ - validate(value) { - const spacePrivileges = getBasePrivilegeNames().space; - if (!spacePrivileges.some((privilege) => privilege === value)) { - return `unknown space privilege "${value}", must be one of [${spacePrivileges}]`; - } - }, - }) - ) - ) - ), - - /** - * An optional dictionary of Kibana feature privileges where the key is the ID of the - * feature and the value is a list of feature specific privilege IDs. Both feature and - * privilege IDs should consist of allowed set of characters. Feature privileges - * definition isn't allowed when base privileges are defined and required otherwise. - */ - feature: schema.maybe( - schema.recordOf( - schema.string({ - validate(value) { - if (!FEATURE_NAME_VALUE_REGEX.test(value)) { - return `only a-z, A-Z, 0-9, '_', and '-' are allowed`; - } - }, - }), - schema.arrayOf( - schema.string({ - validate(value) { - if (!FEATURE_NAME_VALUE_REGEX.test(value)) { - return `only a-z, A-Z, 0-9, '_', and '-' are allowed`; - } - }, - }) - ) - ) - ), - }, - { - validate(value) { - if ( - (value.base === undefined || value.base.length === 0) && - (value.feature === undefined || Object.values(value.feature).flat().length === 0) - ) { - return 'either [base] or [feature] is expected, but none of them specified'; - } - - if ( - value.base !== undefined && - value.base.length > 0 && - value.feature !== undefined && - Object.keys(value.feature).length > 0 - ) { - return `definition of [feature] isn't allowed when non-empty [base] is defined.`; - } - }, - } - ), - { - validate(value) { - for (const [indexA, valueA] of value.entries()) { - for (const valueB of value.slice(indexA + 1)) { - const spaceIntersection = _.intersection(valueA.spaces, valueB.spaces); - if (spaceIntersection.length !== 0) { - return `more than one privilege is applied to the following spaces: [${spaceIntersection}]`; - } - } - } - }, - } - ) - ), - }); -} +import type { ElasticsearchRole } from '../../../../authorization'; +import { + elasticsearchRoleSchema, + getKibanaRoleSchema, + transformPrivilegesToElasticsearchPrivileges, +} from '../../../../lib'; export const transformPutPayloadToElasticsearchRole = ( - rolePayload: PutPayloadSchemaType, + rolePayload: RolePayloadSchemaType, application: string, allExistingApplications: ElasticsearchRole['applications'] = [] ) => { @@ -255,98 +40,26 @@ export const transformPutPayloadToElasticsearchRole = ( } as Omit; }; -const transformPrivilegesToElasticsearchPrivileges = ( - application: string, - kibanaPrivileges: PutPayloadSchemaType['kibana'] = [] -) => { - return kibanaPrivileges.map(({ base, feature, spaces }) => { - if (spaces.length === 1 && spaces[0] === GLOBAL_RESOURCE) { - return { - privileges: [ - ...(base - ? base.map((privilege) => PrivilegeSerializer.serializeGlobalBasePrivilege(privilege)) - : []), - ...(feature - ? Object.entries(feature) - .map(([featureName, featurePrivileges]) => - featurePrivileges.map((privilege) => - PrivilegeSerializer.serializeFeaturePrivilege(featureName, privilege) - ) - ) - .flat() - : []), - ], - application, - resources: [GLOBAL_RESOURCE], - }; - } - - return { - privileges: [ - ...(base - ? base.map((privilege) => PrivilegeSerializer.serializeSpaceBasePrivilege(privilege)) - : []), - ...(feature - ? Object.entries(feature) - .map(([featureName, featurePrivileges]) => - featurePrivileges.map((privilege) => - PrivilegeSerializer.serializeFeaturePrivilege(featureName, privilege) - ) - ) - .flat() - : []), - ], - application, - resources: (spaces as string[]).map((resource) => - ResourceSerializer.serializeSpaceResource(resource) - ), - }; - }); -}; - -export const validateKibanaPrivileges = ( - kibanaFeatures: KibanaFeature[], - kibanaPrivileges: PutPayloadSchemaType['kibana'] -) => { - const validationErrors = (kibanaPrivileges ?? []).flatMap((priv) => { - const forAllSpaces = priv.spaces.includes(ALL_SPACES_ID); - - return Object.entries(priv.feature ?? {}).flatMap(([featureId, feature]) => { - const errors: string[] = []; - const kibanaFeature = kibanaFeatures.find((f) => f.id === featureId); - if (!kibanaFeature) return errors; - - if (feature.includes('all')) { - if (kibanaFeature.privileges?.all.disabled) { - errors.push(`Feature [${featureId}] does not support privilege [all].`); - } - - if (kibanaFeature.privileges?.all.requireAllSpaces && !forAllSpaces) { - errors.push( - `Feature privilege [${featureId}.all] requires all spaces to be selected but received [${priv.spaces.join( - ',' - )}]` - ); - } - } - - if (feature.includes('read')) { - if (kibanaFeature.privileges?.read.disabled) { - errors.push(`Feature [${featureId}] does not support privilege [read].`); - } +export function getPutPayloadSchema( + getBasePrivilegeNames: () => { global: string[]; space: string[] } +) { + return schema.object({ + /** + * An optional meta-data dictionary. Within the metadata, keys that begin with _ are reserved + * for system usage. + */ + metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())), - if (kibanaFeature.privileges?.read.requireAllSpaces && !forAllSpaces) { - errors.push( - `Feature privilege [${featureId}.read] requires all spaces to be selected but received [${priv.spaces.join( - ',' - )}]` - ); - } - } + /** + * Elasticsearch specific portion of the role definition. + */ + elasticsearch: elasticsearchRoleSchema, - return errors; - }); + /** + * Kibana specific portion of the role definition. + */ + kibana: schema.maybe(getKibanaRoleSchema(getBasePrivilegeNames)), }); +} - return { validationErrors }; -}; +export type RolePayloadSchemaType = TypeOf>; diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index 03fbdf30dc767..a6c9ae8a15fd9 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -5,23 +5,17 @@ * 2.0. */ -import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; import type { KibanaFeature } from '@kbn/features-plugin/common'; import type { RouteDefinitionParams } from '../..'; import { wrapIntoCustomErrorResponse } from '../../../errors'; +import { validateKibanaPrivileges } from '../../../lib'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import { - getPutPayloadSchema, - transformPutPayloadToElasticsearchRole, - validateKibanaPrivileges, -} from './model'; +import type { RolePayloadSchemaType } from './model'; +import { getPutPayloadSchema, transformPutPayloadToElasticsearchRole } from './model'; -const roleGrantsSubFeaturePrivileges = ( - features: KibanaFeature[], - role: TypeOf> -) => { +const roleGrantsSubFeaturePrivileges = (features: KibanaFeature[], role: RolePayloadSchemaType) => { if (!role.kibana) { return false; } diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts index 3395f56359a2f..e39950699cb4a 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts @@ -5,7 +5,7 @@ * 2.0. */ import { schema } from '@kbn/config-schema'; -import { SavedObject } from '@kbn/core/server'; +import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core/server'; import { ConfigKey, MonitorFields, @@ -35,14 +35,28 @@ export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ return response.badRequest({ body: { message, attributes: { details, ...payload } } }); } - const newMonitor: SavedObject = - await savedObjectsClient.create( + let newMonitor: SavedObject | null = null; + + try { + newMonitor = await savedObjectsClient.create( syntheticsMonitorType, formatSecrets({ ...monitor, revision: 1, }) ); + } catch (getErr) { + if (SavedObjectsErrorHelpers.isForbiddenError(getErr)) { + return response.forbidden({ body: getErr }); + } + } + + if (!newMonitor) { + return response.customError({ + body: { message: 'Unable to create monitor' }, + statusCode: 500, + }); + } const { syntheticsService } = server; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts index 68ebda333e6e6..d072cbaff2bcd 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts @@ -22,7 +22,7 @@ import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; export const serviceApiKeyPrivileges = { cluster: ['monitor', 'read_ilm', 'read_pipeline'] as SecurityClusterPrivilege[], - index: [ + indices: [ { names: ['synthetics-*'], privileges: [ @@ -32,6 +32,7 @@ export const serviceApiKeyPrivileges = { ] as SecurityIndexPrivilege[], }, ], + run_as: [], }; export const getAPIKeyForSyntheticsService = async ({ @@ -137,7 +138,7 @@ export const getSyntheticsEnablement = async ({ 'manage_own_api_key', ...serviceApiKeyPrivileges.cluster, ], - index: serviceApiKeyPrivileges.index, + index: serviceApiKeyPrivileges.indices, }, }), security.authc.apiKeys.areAPIKeysEnabled(), diff --git a/x-pack/test/api_integration/apis/security/api_keys.ts b/x-pack/test/api_integration/apis/security/api_keys.ts index 98ef83c437863..28d9be63c2db0 100644 --- a/x-pack/test/api_integration/apis/security/api_keys.ts +++ b/x-pack/test/api_integration/apis/security/api_keys.ts @@ -6,6 +6,8 @@ */ import expect from '@kbn/expect'; +import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants'; +import { serviceApiKeyPrivileges } from '@kbn/synthetics-plugin/server/synthetics_service/get_api_key'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { @@ -64,5 +66,38 @@ export default function ({ getService }: FtrProviderContext) { }); }); }); + + describe('with kibana privileges', () => { + describe('POST /internal/security/api_key', () => { + it('should allow an API Key to be created', async () => { + await supertest + .post('/internal/security/api_key') + .set('kbn-xsrf', 'xxx') + .send({ + name: 'test_api_key', + expiration: '12d', + kibana_role_descriptors: { + uptime_save: { + elasticsearch: serviceApiKeyPrivileges, + kibana: [ + { + base: [], + spaces: [ALL_SPACES_ID], + feature: { + uptime: ['all'], + }, + }, + ], + }, + }, + }) + .expect(200) + .then((response: Record) => { + const { name } = response.body; + expect(name).to.eql('test_api_key'); + }); + }); + }); + }); }); } diff --git a/x-pack/test/api_integration/apis/uptime/rest/add_monitor.ts b/x-pack/test/api_integration/apis/uptime/rest/add_monitor.ts index 14fce8f974e92..ed90086b1bb0a 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/add_monitor.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/add_monitor.ts @@ -9,12 +9,16 @@ import expect from '@kbn/expect'; import { secretKeys } from '@kbn/synthetics-plugin/common/constants/monitor_management'; import { HTTPFields } from '@kbn/synthetics-plugin/common/runtime_types'; import { API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants'; +import { format as formatUrl } from 'url'; +import supertest from 'supertest'; +import { serviceApiKeyPrivileges } from '@kbn/synthetics-plugin/server/synthetics_service/get_api_key'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { getFixtureJson } from './helper/get_fixture_json'; export default function ({ getService }: FtrProviderContext) { describe('[POST] /internal/uptime/service/monitors', () => { - const supertest = getService('supertest'); + const supertestAPI = getService('supertest'); let _httpMonitorJson: HTTPFields; let httpMonitorJson: HTTPFields; @@ -30,7 +34,7 @@ export default function ({ getService }: FtrProviderContext) { it('returns the newly added monitor', async () => { const newMonitor = httpMonitorJson; - const apiResponse = await supertest + const apiResponse = await supertestAPI .post(API_URLS.SYNTHETICS_MONITORS) .set('kbn-xsrf', 'true') .send(newMonitor); @@ -42,7 +46,7 @@ export default function ({ getService }: FtrProviderContext) { // Delete a required property to make payload invalid const newMonitor = { ...httpMonitorJson, 'check.request.headers': undefined }; - const apiResponse = await supertest + const apiResponse = await supertestAPI .post(API_URLS.SYNTHETICS_MONITORS) .set('kbn-xsrf', 'true') .send(newMonitor); @@ -53,7 +57,7 @@ export default function ({ getService }: FtrProviderContext) { it('returns bad request if monitor type is invalid', async () => { const newMonitor = { ...httpMonitorJson, type: 'invalid-data-steam' }; - const apiResponse = await supertest + const apiResponse = await supertestAPI .post(API_URLS.SYNTHETICS_MONITORS) .set('kbn-xsrf', 'true') .send(newMonitor); @@ -61,5 +65,94 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.status).eql(400); expect(apiResponse.body.message).eql('Monitor type is invalid'); }); + + it('can create monitor with API key with proper permissions', async () => { + await supertestAPI + .post('/internal/security/api_key') + .set('kbn-xsrf', 'xxx') + .send({ + name: 'test_api_key', + expiration: '12d', + kibana_role_descriptors: { + uptime_save: { + elasticsearch: serviceApiKeyPrivileges, + kibana: [ + { + base: [], + spaces: [ALL_SPACES_ID], + feature: { + uptime: ['all'], + }, + }, + ], + }, + }, + }) + .expect(200) + .then(async (response: Record) => { + const { name, encoded: apiKey } = response.body; + expect(name).to.eql('test_api_key'); + + const config = getService('config'); + + const { hostname, protocol, port } = config.get('servers.kibana'); + const kibanaServerUrl = formatUrl({ hostname, protocol, port }); + const supertestNoAuth = supertest(kibanaServerUrl); + + const apiResponse = await supertestNoAuth + .post(API_URLS.SYNTHETICS_MONITORS) + .auth(name, apiKey) + .set('kbn-xsrf', 'true') + .set('Authorization', `ApiKey ${apiKey}`) + .send(httpMonitorJson); + + expect(apiResponse.status).eql(200); + }); + }); + + it('can not create monitor with API key without proper permissions', async () => { + await supertestAPI + .post('/internal/security/api_key') + .set('kbn-xsrf', 'xxx') + .send({ + name: 'test_api_key', + expiration: '12d', + kibana_role_descriptors: { + uptime_save: { + elasticsearch: serviceApiKeyPrivileges, + kibana: [ + { + base: [], + spaces: [ALL_SPACES_ID], + feature: { + uptime: ['read'], + }, + }, + ], + }, + }, + }) + .expect(200) + .then(async (response: Record) => { + const { name, encoded: apiKey } = response.body; + expect(name).to.eql('test_api_key'); + + const config = getService('config'); + + const { hostname, protocol, port } = config.get('servers.kibana'); + const kibanaServerUrl = formatUrl({ hostname, protocol, port }); + const supertestNoAuth = supertest(kibanaServerUrl); + + const apiResponse = await supertestNoAuth + .post(API_URLS.SYNTHETICS_MONITORS) + .auth(name, apiKey) + .set('kbn-xsrf', 'true') + .set('Authorization', `ApiKey ${apiKey}`) + .send(httpMonitorJson); + + expect(apiResponse.status).eql(403); + expect(apiResponse.body.message).eql('Unable to create synthetics-monitor'); + }); + }); }); } diff --git a/x-pack/test/api_integration/apis/uptime/rest/synthetics_enablement.ts b/x-pack/test/api_integration/apis/uptime/rest/synthetics_enablement.ts index c5e2c1d339ad5..9ac40532538e7 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/synthetics_enablement.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/synthetics_enablement.ts @@ -39,7 +39,7 @@ export default function ({ getService }: FtrProviderContext) { ], elasticsearch: { cluster: [privilege, ...serviceApiKeyPrivileges.cluster], - indices: serviceApiKeyPrivileges.index, + indices: serviceApiKeyPrivileges.indices, }, }); @@ -125,7 +125,7 @@ export default function ({ getService }: FtrProviderContext) { ], elasticsearch: { cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], - indices: serviceApiKeyPrivileges.index, + indices: serviceApiKeyPrivileges.indices, }, }); @@ -224,7 +224,7 @@ export default function ({ getService }: FtrProviderContext) { ], elasticsearch: { cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], - indices: serviceApiKeyPrivileges.index, + indices: serviceApiKeyPrivileges.indices, }, }); @@ -332,7 +332,7 @@ export default function ({ getService }: FtrProviderContext) { ], elasticsearch: { cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], - indices: serviceApiKeyPrivileges.index, + indices: serviceApiKeyPrivileges.indices, }, }); From a487d7c99484f23eb55cd5b7716fc2a550e39d0d Mon Sep 17 00:00:00 2001 From: Katerina Patticha Date: Mon, 23 May 2022 16:51:04 +0200 Subject: [PATCH 093/120] [APM] Add an internal endpoint for debugging telemetry (#132511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [APM] Add telemetry to service groups quries * Add service groups in telemetry schema * Add an internal route to test apm telemetry * Update endpoint to run telemetry jobs and display data * Update telemetry README * Move service_groups task work to another PR * Clean up * Use versioned link in x-pack/plugins/apm/dev_docs/telemetry.md Co-authored-by: Søren Louv-Jansen * Update x-pack/plugins/apm/server/routes/debug_telemetry/route.ts Co-authored-by: Søren Louv-Jansen * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' Co-authored-by: Søren Louv-Jansen Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/dev_docs/telemetry.md | 22 +-- .../generate_sample_documents.ts | 126 -------------- .../scripts/upload_telemetry_data/index.ts | 156 ------------------ .../collect_data_telemetry/index.ts | 7 +- .../collect_data_telemetry/tasks.ts | 1 - .../apm/server/lib/apm_telemetry/index.ts | 4 +- .../get_global_apm_server_route_repository.ts | 3 +- .../server/routes/debug_telemetry/route.ts | 35 ++++ 8 files changed, 54 insertions(+), 300 deletions(-) delete mode 100644 x-pack/plugins/apm/scripts/upload_telemetry_data/generate_sample_documents.ts delete mode 100644 x-pack/plugins/apm/scripts/upload_telemetry_data/index.ts create mode 100644 x-pack/plugins/apm/server/routes/debug_telemetry/route.ts diff --git a/x-pack/plugins/apm/dev_docs/telemetry.md b/x-pack/plugins/apm/dev_docs/telemetry.md index 27b9e57447467..0085f64cdec18 100644 --- a/x-pack/plugins/apm/dev_docs/telemetry.md +++ b/x-pack/plugins/apm/dev_docs/telemetry.md @@ -19,7 +19,7 @@ to the telemetry cluster using the During the APM server-side plugin's setup phase a [Saved Object](https://www.elastic.co/guide/en/kibana/master/managing-saved-objects.html) for APM telemetry is registered and a -[task manager](../../task_manager/server/README.md) task is registered and started. +[task manager](../../task_manager/README.md) task is registered and started. The task periodically queries the APM indices and saves the results in the Saved Object, and the usage collector periodically gets the data from the saved object and uploads it to the telemetry cluster. @@ -27,23 +27,19 @@ and uploads it to the telemetry cluster. Once uploaded to the telemetry cluster, the data telemetry is stored in `stack_stats.kibana.plugins.apm` in the xpack-phone-home index. -### Generating sample data +### Collect a new telemetry field -The script in `scripts/upload_telemetry_data` can generate sample telemetry data and upload it to a cluster of your choosing. +In order to collect a new telemetry field you need to add a task which performs the query that collects the data from the cluster. -You'll need to set the `GITHUB_TOKEN` environment variable to a token that has `repo` scope so it can read from the -[elastic/telemetry](https://github.com/elastic/telemetry) repository. (You probably have a token that works for this in -~/.backport/config.json.) +All the available tasks are [here](https://github.com/elastic/kibana/blob/ba84602455671f0f6175bbc0fd2e8f302c60bbe6/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts) -The script will run as the `elastic` user using the elasticsearch hosts and password settings from the config/kibana.yml -and/or config/kibana.dev.yml files. +### Debug telemetry -Running the script with `--clear` will delete the index first. +The following endpoint will run the `apm-telemetry-task` which is responsible for collecting the telemetry data and once it's completed it will return the telemetry attributes. -If you're using an Elasticsearch instance without TLS verification (if you have `elasticsearch.ssl.verificationMode: none` set in your kibana.yml) -you can run the script with `env NODE_TLS_REJECT_UNAUTHORIZED=0` to avoid TLS connection errors. - -After running the script you should see sample telemetry data in the "xpack-phone-home" index. +``` +GET /internal/apm/debug-telemetry +``` ### Updating Data Telemetry Mappings diff --git a/x-pack/plugins/apm/scripts/upload_telemetry_data/generate_sample_documents.ts b/x-pack/plugins/apm/scripts/upload_telemetry_data/generate_sample_documents.ts deleted file mode 100644 index fe4fe9de5d22a..0000000000000 --- a/x-pack/plugins/apm/scripts/upload_telemetry_data/generate_sample_documents.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DeepPartial } from 'utility-types'; -import { - merge, - omit, - defaultsDeep, - range, - mapValues, - isPlainObject, - flatten, -} from 'lodash'; -import uuid from 'uuid'; -import { - CollectTelemetryParams, - collectDataTelemetry, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../server/lib/apm_telemetry/collect_data_telemetry'; - -interface GenerateOptions { - days: number; - instances: number; - variation: { - min: number; - max: number; - }; -} - -const randomize = ( - value: unknown, - instanceVariation: number, - dailyGrowth: number -) => { - if (typeof value === 'boolean') { - return Math.random() > 0.5; - } - if (typeof value === 'number') { - return Math.round(instanceVariation * dailyGrowth * value); - } - return value; -}; - -const mapValuesDeep = ( - obj: Record, - iterator: (value: unknown, key: string, obj: Record) => unknown -): Record => - mapValues(obj, (val, key) => - isPlainObject(val) ? mapValuesDeep(val, iterator) : iterator(val, key!, obj) - ); - -export async function generateSampleDocuments( - options: DeepPartial & { - collectTelemetryParams: CollectTelemetryParams; - } -) { - const { collectTelemetryParams, ...preferredOptions } = options; - - const opts: GenerateOptions = defaultsDeep( - { - days: 100, - instances: 50, - variation: { - min: 0.1, - max: 4, - }, - }, - preferredOptions - ); - - const sample = await collectDataTelemetry(collectTelemetryParams); - - console.log('Collected telemetry'); // eslint-disable-line no-console - console.log('\n' + JSON.stringify(sample, null, 2)); // eslint-disable-line no-console - - const dateOfScriptExecution = new Date(); - - return flatten( - range(0, opts.instances).map(() => { - const instanceId = uuid.v4(); - const defaults = { - cluster_uuid: instanceId, - stack_stats: { - kibana: { - versions: { - version: '8.0.0', - }, - }, - }, - }; - - const instanceVariation = - Math.random() * (opts.variation.max - opts.variation.min) + - opts.variation.min; - - return range(0, opts.days).map((dayNo) => { - const dailyGrowth = Math.pow(1.005, opts.days - 1 - dayNo); - - const timestamp = Date.UTC( - dateOfScriptExecution.getFullYear(), - dateOfScriptExecution.getMonth(), - -dayNo - ); - - const generated = mapValuesDeep(omit(sample, 'versions'), (value) => - randomize(value, instanceVariation, dailyGrowth) - ); - - return merge({}, defaults, { - timestamp, - stack_stats: { - kibana: { - plugins: { - apm: merge({}, sample, generated), - }, - }, - }, - }); - }); - }) - ); -} diff --git a/x-pack/plugins/apm/scripts/upload_telemetry_data/index.ts b/x-pack/plugins/apm/scripts/upload_telemetry_data/index.ts deleted file mode 100644 index 0f8a6f874d72f..0000000000000 --- a/x-pack/plugins/apm/scripts/upload_telemetry_data/index.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// This script downloads the telemetry mapping, runs the APM telemetry tasks, -// generates a bunch of randomized data based on the downloaded sample, -// and uploads it to a cluster of your choosing in the same format as it is -// stored in the telemetry cluster. Its purpose is twofold: -// - Easier testing of the telemetry tasks -// - Validate whether we can run the queries we want to on the telemetry data - -import { merge, chunk, flatten, omit } from 'lodash'; -import { argv } from 'yargs'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Logger } from '@kbn/core/server'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { CollectTelemetryParams } from '../../server/lib/apm_telemetry/collect_data_telemetry'; -import { downloadTelemetryTemplate } from '../shared/download_telemetry_template'; -import { mergeApmTelemetryMapping } from '../../common/apm_telemetry'; -import { generateSampleDocuments } from './generate_sample_documents'; -import { readKibanaConfig } from '../shared/read_kibana_config'; -import { getHttpAuth } from '../shared/get_http_auth'; -import { createOrUpdateIndex } from '../shared/create_or_update_index'; -import { getEsClient } from '../shared/get_es_client'; - -async function uploadData() { - const githubToken = process.env.GITHUB_TOKEN; - - if (!githubToken) { - throw new Error('GITHUB_TOKEN was not provided.'); - } - - const xpackTelemetryIndexName = 'xpack-phone-home'; - const telemetryTemplate = await downloadTelemetryTemplate({ - githubToken, - }); - - const config = readKibanaConfig(); - - const httpAuth = getHttpAuth(config); - - const client = getEsClient({ - node: config['elasticsearch.hosts'], - ...(httpAuth - ? { - auth: { ...httpAuth, username: 'elastic' }, - } - : {}), - }); - - // The new template is the template downloaded from the telemetry repo, with - // our current telemetry mapping merged in, with the "index_patterns" key - // (which cannot be used when creating an index) removed. - const newTemplate = omit( - mergeApmTelemetryMapping( - merge(telemetryTemplate, { - index_patterns: undefined, - settings: { - index: { mapping: { total_fields: { limit: 10000 } } }, - }, - }) - ), - 'index_patterns' - ); - - await createOrUpdateIndex({ - indexName: xpackTelemetryIndexName, - client, - template: newTemplate, - clear: !!argv.clear, - }); - - const sampleDocuments = await generateSampleDocuments({ - collectTelemetryParams: { - logger: console as unknown as Logger, - indices: { - transaction: config['xpack.apm.indices.transaction'], - metric: config['xpack.apm.indices.metric'], - error: config['xpack.apm.indices.error'], - span: config['xpack.apm.indices.span'], - onboarding: config['xpack.apm.indices.onboarding'], - sourcemap: config['xpack.apm.indices.sourcemap'], - apmCustomLinkIndex: '.apm-custom-links', - apmAgentConfigurationIndex: '.apm-agent-configuration', - }, - search: (body) => { - return client.search(body) as Promise; - }, - indicesStats: (body) => { - return client.indices.stats(body); - }, - transportRequest: ((params) => { - return; - }) as CollectTelemetryParams['transportRequest'], - }, - }); - - const chunks = chunk(sampleDocuments, 250); - - await chunks.reduce>((prev, documents) => { - return prev.then(async () => { - const body = flatten( - documents.map((doc) => [ - { index: { _index: xpackTelemetryIndexName } }, - doc, - ]) - ); - - return client - .bulk({ - body, - refresh: 'wait_for', - }) - .then((response: any) => { - if (response.errors) { - const firstError = response.items.filter( - (item: any) => item.index.status >= 400 - )[0].index.error; - throw new Error( - `Failed to upload documents: ${firstError.reason} ` - ); - } - }); - }); - }, Promise.resolve()); -} - -uploadData() - .catch((e) => { - if ('response' in e) { - if (typeof e.response === 'string') { - // eslint-disable-next-line no-console - console.log(e.response); - } else { - // eslint-disable-next-line no-console - console.log( - JSON.stringify( - e.response, - ['status', 'statusText', 'headers', 'data'], - 2 - ) - ); - } - } else { - // eslint-disable-next-line no-console - console.log(e); - } - process.exit(1); - }) - .then(() => { - // eslint-disable-next-line no-console - console.log('Finished uploading generated telemetry data'); - }); diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts index 2a196c3e04638..c870ddc070f59 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts @@ -6,7 +6,7 @@ */ import { merge } from 'lodash'; -import { Logger } from '@kbn/core/server'; +import { Logger, SavedObjectsClient } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ESSearchRequest, @@ -16,6 +16,8 @@ import { ApmIndicesConfig } from '../../../routes/settings/apm_indices/get_apm_i import { tasks } from './tasks'; import { APMDataTelemetry } from '../types'; +type ISavedObjectsClient = Pick; + type TelemetryTaskExecutor = (params: { indices: ApmIndicesConfig; search( @@ -37,6 +39,7 @@ type TelemetryTaskExecutor = (params: { path: string; method: 'get'; }) => Promise; + savedObjectsClient: ISavedObjectsClient; }) => Promise; export interface TelemetryTask { @@ -54,6 +57,7 @@ export function collectDataTelemetry({ logger, indicesStats, transportRequest, + savedObjectsClient, }: CollectTelemetryParams) { return tasks.reduce((prev, task) => { return prev.then(async (data) => { @@ -65,6 +69,7 @@ export function collectDataTelemetry({ indices, indicesStats, transportRequest, + savedObjectsClient, }); const took = process.hrtime(time); diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index f06226c864a98..54157296da270 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -44,7 +44,6 @@ import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { Span } from '../../../../typings/es_schemas/ui/span'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { APMTelemetry } from '../types'; - const TIME_RANGES = ['1d', 'all'] as const; type TimeRange = typeof TIME_RANGES[number]; diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts index d8e1638ad4a57..c45ed100402b9 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { Observable, firstValueFrom } from 'rxjs'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { CoreSetup, Logger, SavedObjectsErrorHelpers } from '@kbn/core/server'; @@ -27,7 +26,7 @@ import { import { APMUsage } from './types'; import { apmSchema } from './schema'; -const APM_TELEMETRY_TASK_NAME = 'apm-telemetry-task'; +export const APM_TELEMETRY_TASK_NAME = 'apm-telemetry-task'; export async function createApmTelemetry({ core, @@ -93,6 +92,7 @@ export async function createApmTelemetry({ logger, indicesStats, transportRequest, + savedObjectsClient, }); await savedObjectsClient.create( diff --git a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts index 7224a58fda982..521e5887091ac 100644 --- a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts @@ -38,7 +38,7 @@ import { eventMetadataRouteRepository } from '../event_metadata/route'; import { suggestionsRouteRepository } from '../suggestions/route'; import { agentKeysRouteRepository } from '../agent_keys/route'; import { spanLinksRouteRepository } from '../span_links/route'; - +import { debugTelemetryRoute } from '../debug_telemetry/route'; function getTypedGlobalApmServerRouteRepository() { const repository = { ...dataViewRouteRepository, @@ -69,6 +69,7 @@ function getTypedGlobalApmServerRouteRepository() { ...eventMetadataRouteRepository, ...agentKeysRouteRepository, ...spanLinksRouteRepository, + ...debugTelemetryRoute, }; return repository; diff --git a/x-pack/plugins/apm/server/routes/debug_telemetry/route.ts b/x-pack/plugins/apm/server/routes/debug_telemetry/route.ts new file mode 100644 index 0000000000000..b3c8a9347708e --- /dev/null +++ b/x-pack/plugins/apm/server/routes/debug_telemetry/route.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; +import { APM_TELEMETRY_TASK_NAME } from '../../lib/apm_telemetry'; +import { APMTelemetry } from '../../lib/apm_telemetry/types'; +import { + APM_TELEMETRY_SAVED_OBJECT_ID, + APM_TELEMETRY_SAVED_OBJECT_TYPE, +} from '../../../common/apm_saved_object_constants'; +export const debugTelemetryRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/debug-telemetry', + options: { + tags: ['access:apm', 'access:apm_write'], + }, + handler: async (resources): Promise => { + const { plugins, context } = resources; + const coreContext = await context.core; + const taskManagerStart = await plugins.taskManager?.start(); + const savedObjectsClient = coreContext.savedObjects.client; + + await taskManagerStart?.runNow?.(APM_TELEMETRY_TASK_NAME); + + const apmTelemetryObject = await savedObjectsClient.get( + APM_TELEMETRY_SAVED_OBJECT_TYPE, + APM_TELEMETRY_SAVED_OBJECT_ID + ); + + return apmTelemetryObject.attributes; + }, +}); From dedbeecc0654771b264b34df1fcdb86abc17bb6f Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 23 May 2022 11:46:54 -0400 Subject: [PATCH 094/120] [Fleet] Allow to schedule upgrade (#132653) * [Fleet] Allow to schedule upgrade for agents * Add scheduled upgrade in current upgrade callout * Fix i18n * Fix tests * Fix start_time validation * Make selected datetime format more user friendly * Address code review comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: criamico --- .../fleet/common/types/models/agent.ts | 1 + .../fleet/common/types/rest_spec/agent.ts | 1 + .../components/bulk_actions.tsx | 24 ++++++-- .../current_bulk_upgrade_callout.tsx | 54 ++++++++++++---- .../components/agent_upgrade_modal/index.tsx | 61 ++++++++++++++++--- .../server/routes/agent/upgrade_handler.ts | 2 + .../fleet/server/services/agents/upgrade.ts | 13 +++- .../fleet/server/types/rest_spec/agent.ts | 10 +++ .../apis/agents/current_upgrades.ts | 5 ++ 9 files changed, 144 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index a26f63eba755b..fee6f4c2ae4c4 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -99,6 +99,7 @@ export interface CurrentUpgrade { nbAgents: number; nbAgentsAck: number; version: string; + startTime: string; } // Generated from FleetServer schema.json diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index 886730d38f831..77416f5e1db5d 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -90,6 +90,7 @@ export interface PostBulkAgentUpgradeRequest { source_uri?: string; version: string; rollout_duration_seconds?: number; + start_time?: string; }; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx index e27c647e25f70..7df96dad06a88 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx @@ -55,7 +55,7 @@ export const AgentBulkActions: React.FunctionComponent = ({ // Actions states const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(false); const [isUnenrollModalOpen, setIsUnenrollModalOpen] = useState(false); - const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false); + const [updateModalState, setUpgradeModalState] = useState({ isOpen: false, isScheduled: false }); // Check if user is working with only inactive agents const atLeastOneActiveAgentSelected = @@ -109,7 +109,22 @@ export const AgentBulkActions: React.FunctionComponent = ({ disabled: !atLeastOneActiveAgentSelected, onClick: () => { closeMenu(); - setIsUpgradeModalOpen(true); + setUpgradeModalState({ isOpen: true, isScheduled: false }); + }, + }, + { + name: ( + + ), + icon: , + disabled: !atLeastOneActiveAgentSelected, + onClick: () => { + closeMenu(); + setUpgradeModalState({ isOpen: true, isScheduled: true }); }, }, ], @@ -145,13 +160,14 @@ export const AgentBulkActions: React.FunctionComponent = ({ /> )} - {isUpgradeModalOpen && ( + {updateModalState.isOpen && ( { - setIsUpgradeModalOpen(false); + setUpgradeModalState({ isOpen: false, isScheduled: false }); refreshAgents(); }} /> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/current_bulk_upgrade_callout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/current_bulk_upgrade_callout.tsx index a77c26f8fef2f..a4931cbd6f362 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/current_bulk_upgrade_callout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/current_bulk_upgrade_callout.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { useCallback, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useCallback, useState, useMemo } from 'react'; +import { FormattedMessage, FormattedDate, FormattedTime } from '@kbn/i18n-react'; import { EuiCallOut, EuiLink, @@ -14,6 +14,7 @@ import { EuiFlexItem, EuiButton, EuiLoadingSpinner, + EuiIcon, } from '@elastic/eui'; import { useStartServices } from '../../../../hooks'; @@ -39,6 +40,44 @@ export const CurrentBulkUpgradeCallout: React.FunctionComponent { + const now = Date.now(); + const startDate = new Date(currentUpgrade.startTime).getTime(); + + return startDate > now; + }, [currentUpgrade]); + + const calloutTitle = isScheduled ? ( + + +   + + + ), + }} + /> + ) : ( + + ); return (
- + {isScheduled ? : }    - + {calloutTitle}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx index 2122abb5e2785..6708c12cf8b97 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; +import moment from 'moment'; import { EuiConfirmModal, EuiComboBox, @@ -17,6 +18,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiCallOut, + EuiDatePicker, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -36,6 +38,7 @@ interface Props { onClose: () => void; agents: Agent[] | string; agentCount: number; + isScheduled?: boolean; } const getVersion = (version: Array>) => version[0].value as string; @@ -44,6 +47,7 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ onClose, agents, agentCount, + isScheduled = false, }) => { const { notifications } = useStartServices(); const kibanaVersion = useKibanaVersion(); @@ -61,7 +65,8 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ value: option, }) ); - const maintainanceWindows = isSmallBatch ? [0].concat(MAINTAINANCE_VALUES) : MAINTAINANCE_VALUES; + const maintainanceWindows = + isSmallBatch && !isScheduled ? [0].concat(MAINTAINANCE_VALUES) : MAINTAINANCE_VALUES; const maintainanceOptions: Array> = maintainanceWindows.map( (option) => ({ label: @@ -81,14 +86,18 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ maintainanceOptions[0], ]); + const initialDatetime = useMemo(() => moment(), []); + const [startDatetime, setStartDatetime] = useState(initialDatetime); + async function onSubmit() { const version = getVersion(selectedVersion); - const rolloutOptions = - selectedMantainanceWindow.length > 0 && (selectedMantainanceWindow[0]?.value as number) > 0 - ? { - rollout_duration_seconds: selectedMantainanceWindow[0].value, - } - : {}; + const rolloutOptions = { + rollout_duration_seconds: + selectedMantainanceWindow.length > 0 && (selectedMantainanceWindow[0]?.value as number) > 0 + ? selectedMantainanceWindow[0].value + : undefined, + start_time: startDatetime.toISOString(), + }; try { setIsSubmitting(true); @@ -177,12 +186,18 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ {isSingleAgent ? ( + ) : isScheduled ? ( + ) : ( )} @@ -203,6 +218,11 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ id="xpack.fleet.upgradeAgents.confirmSingleButtonLabel" defaultMessage="Upgrade agent" /> + ) : isScheduled ? ( + ) : ( = ({ }} /> + {isScheduled && ( + <> + + + setStartDatetime(date as moment.Moment)} + /> + + + )} {!isSingleAgent ? ( ((acc, so) => { diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index d7f2735e2d284..87007b9ce880a 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -84,6 +84,7 @@ export async function sendUpgradeAgentsActions( sourceUri?: string | undefined; force?: boolean; upgradeDurationSeconds?: number; + startTime?: string; } ) { // Full set of agents @@ -166,9 +167,11 @@ export async function sendUpgradeAgentsActions( const rollingUpgradeOptions = options?.upgradeDurationSeconds ? { - start_time: now, + start_time: options.startTime ?? now, minimum_execution_duration: MINIMUM_EXECUTION_DURATION_SECONDS, - expiration: moment().add(options?.upgradeDurationSeconds, 'seconds').toISOString(), + expiration: moment(options.startTime ?? now) + .add(options?.upgradeDurationSeconds, 'seconds') + .toISOString(), } : {}; @@ -311,6 +314,11 @@ async function _getUpgradeActions(esClient: ElasticsearchClient, now = new Date( field: 'agents', }, }, + { + exists: { + field: 'start_time', + }, + }, { range: { expiration: { gte: now }, @@ -334,6 +342,7 @@ async function _getUpgradeActions(esClient: ElasticsearchClient, now = new Date( complete: false, nbAgentsAck: 0, version: hit._source.data?.version as string, + startTime: hit._source.start_time as string, }; } diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index e080fe66f7e2c..7c1078ba8dbd8 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import moment from 'moment'; import { NewAgentActionSchema } from '../models'; @@ -78,6 +79,15 @@ export const PostBulkAgentUpgradeRequestSchema = { version: schema.string(), force: schema.maybe(schema.boolean()), rollout_duration_seconds: schema.maybe(schema.number({ min: 600 })), + start_time: schema.maybe( + schema.string({ + validate: (v: string) => { + if (!moment(v).isValid()) { + return 'not a valid date'; + } + }, + }) + ), }), }; diff --git a/x-pack/test/fleet_api_integration/apis/agents/current_upgrades.ts b/x-pack/test/fleet_api_integration/apis/agents/current_upgrades.ts index f1a5666875e5a..8d060666cd1ee 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/current_upgrades.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/current_upgrades.ts @@ -45,6 +45,7 @@ export default function (providerContext: FtrProviderContext) { type: 'UPGRADE', action_id: 'action1', agents: ['agent1', 'agent2', 'agent3'], + start_time: moment().toISOString(), expiration: moment().add(1, 'day').toISOString(), }, }); @@ -57,6 +58,7 @@ export default function (providerContext: FtrProviderContext) { type: 'UPGRADE', action_id: 'action2', agents: ['agent1', 'agent2', 'agent3'], + start_time: moment().toISOString(), expiration: moment().add(1, 'day').toISOString(), }, }); @@ -68,6 +70,7 @@ export default function (providerContext: FtrProviderContext) { type: 'UPGRADE', action_id: 'action2', agents: ['agent4', 'agent5'], + start_time: moment().toISOString(), expiration: moment().add(1, 'day').toISOString(), }, }); @@ -79,6 +82,7 @@ export default function (providerContext: FtrProviderContext) { type: 'UPGRADE', action_id: 'action3', agents: ['agent1', 'agent2'], + start_time: moment().toISOString(), expiration: moment().add(1, 'day').toISOString(), }, }); @@ -129,6 +133,7 @@ export default function (providerContext: FtrProviderContext) { type: 'UPGRADE', action_id: 'action5', agents: ['agent1', 'agent2', 'agent3'], + start_time: moment().toISOString(), expiration: moment().add(1, 'day').toISOString(), }, }); From 3c648df094741caa09b5c3715a4193cec5f595e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Mon, 23 May 2022 17:58:51 +0200 Subject: [PATCH 095/120] [Unified Observability] Add Page load distribution chart to overview page (#132258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add serviceName to breakdown select * add exploratory view to ux section * fix chart height * fix types * Use datepicker values for page load distribution * use translations * memoize exploratory embeddable * fix tests * fix types * remove memoization * Update chart height Co-authored-by: Casper Hübertz * remove actions * minor improvements * Undo AppDataType enum Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Casper Hübertz --- .../public/application/index.tsx | 2 + .../components/app/section/apm/index.test.tsx | 4 ++ .../components/app/section/ux/index.tsx | 43 +++++++++++++++++++ .../rum/data_distribution_config.ts | 8 +++- .../public/context/plugin_context.tsx | 5 ++- .../pages/overview/overview.stories.tsx | 3 ++ .../public/pages/rules/index.test.tsx | 4 ++ .../public/utils/test_helper.tsx | 11 +++++ 8 files changed, 78 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index c48a663fefe5b..ff23f5c103d2d 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -92,6 +92,8 @@ export const renderApp = ({ value={{ appMountParameters, config, + core, + plugins, observabilityRuleTypeRegistry, ObservabilityPageTemplate, kibanaFeatures, diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index 179e8ef70deb1..138c0008df400 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -8,6 +8,8 @@ import React from 'react'; import * as fetcherHook from '../../../../hooks/use_fetcher'; import { render, data as dataMock } from '../../../../utils/test_helper'; +import { CoreStart } from '@kbn/core/public'; +import { ObservabilityPublicPluginsStart } from '../../../../plugin'; import { APMSection } from '.'; import { response } from './mock_data/apm.mock'; import * as hasDataHook from '../../../../hooks/use_has_data'; @@ -51,6 +53,8 @@ describe('APMSection', () => { rules: { enabled: true }, }, }, + core: {} as CoreStart, + plugins: {} as ObservabilityPublicPluginsStart, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), ObservabilityPageTemplate: KibanaPageTemplate, kibanaFeatures: [], diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx index 430285a0941cf..da5bffd6cb186 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx @@ -7,13 +7,21 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; +import type { AppDataType } from '../../../shared/exploratory_view/types'; import { SectionContainer } from '..'; import { getDataHandler } from '../../../../data_handler'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { useHasData } from '../../../../hooks/use_has_data'; import { useDatePickerContext } from '../../../../hooks/use_date_picker_context'; +import { usePluginContext } from '../../../../hooks/use_plugin_context'; import CoreVitals from '../../../shared/core_web_vitals'; import { BucketSize } from '../../../../pages/overview'; +import { getExploratoryViewEmbeddable } from '../../../shared/exploratory_view/embeddable'; +import { AllSeries } from '../../../shared/exploratory_view/hooks/use_series_storage'; +import { + SERVICE_NAME, + TRANSACTION_DURATION, +} from '../../../shared/exploratory_view/configurations/constants/elasticsearch_fieldnames'; interface Props { bucketSize: BucketSize; @@ -21,11 +29,30 @@ interface Props { export function UXSection({ bucketSize }: Props) { const { forceUpdate, hasDataMap } = useHasData(); + const { core, plugins } = usePluginContext(); const { relativeStart, relativeEnd, absoluteStart, absoluteEnd, lastUpdated } = useDatePickerContext(); const uxHasDataResponse = hasDataMap.ux; const serviceName = uxHasDataResponse?.serviceName as string; + const ExploratoryViewEmbeddable = getExploratoryViewEmbeddable(core, plugins); + + const seriesList: AllSeries = [ + { + name: PAGE_LOAD_DISTRIBUTION_TITLE, + time: { + from: relativeStart, + to: relativeEnd, + }, + reportDefinitions: { + [SERVICE_NAME]: ['ALL_VALUES'], + }, + breakdown: SERVICE_NAME, + dataType: 'ux' as AppDataType, + selectedMetricField: TRANSACTION_DURATION, + }, + ]; + const { data, status } = useFetcher( () => { if (serviceName && bucketSize && absoluteStart && absoluteEnd) { @@ -72,6 +99,15 @@ export function UXSection({ bucketSize }: Props) { }} hasError={status === FETCH_STATUS.FAILURE} > +
+ +
+ ); } + +const PAGE_LOAD_DISTRIBUTION_TITLE = i18n.translate( + 'xpack.observability.overview.ux.pageLoadDistribution.title', + { + defaultMessage: 'Page load distribution', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts index 97b6d2ddf7199..ff2939213bbc1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts @@ -74,7 +74,13 @@ export function getRumDistributionConfig({ dataView }: ConfigProps): SeriesConfi }, LABEL_FIELDS_FILTER, ], - breakdownFields: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], + breakdownFields: [ + USER_AGENT_NAME, + USER_AGENT_OS, + CLIENT_GEO_COUNTRY_NAME, + USER_AGENT_DEVICE, + SERVICE_NAME, + ], definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT], metricOptions: [ { label: PAGE_LOAD_TIME_LABEL, id: TRANSACTION_DURATION, field: TRANSACTION_DURATION }, diff --git a/x-pack/plugins/observability/public/context/plugin_context.tsx b/x-pack/plugins/observability/public/context/plugin_context.tsx index 37dffe4daa178..d77edb7d9de73 100644 --- a/x-pack/plugins/observability/public/context/plugin_context.tsx +++ b/x-pack/plugins/observability/public/context/plugin_context.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import { AppMountParameters } from '@kbn/core/public'; +import { AppMountParameters, CoreStart } from '@kbn/core/public'; import { createContext } from 'react'; import { KibanaFeature } from '@kbn/features-plugin/common'; +import { ObservabilityPublicPluginsStart } from '../plugin'; import { ConfigSchema } from '..'; import { ObservabilityRuleTypeRegistry } from '../rules/create_observability_rule_type_registry'; import type { LazyObservabilityPageTemplateProps } from '../components/shared/page_template/lazy_page_template'; @@ -15,6 +16,8 @@ import type { LazyObservabilityPageTemplateProps } from '../components/shared/pa export interface PluginContextValue { appMountParameters: AppMountParameters; config: ConfigSchema; + core: CoreStart; + plugins: ObservabilityPublicPluginsStart; observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; ObservabilityPageTemplate: React.ComponentType; kibanaFeatures: KibanaFeature[]; diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index 097d0d0845dca..d775e0102a5d7 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -14,6 +14,7 @@ import { UI_SETTINGS } from '@kbn/data-plugin/public'; import { createKibanaReactContext, KibanaPageTemplate } from '@kbn/kibana-react-plugin/public'; import { HasDataContextProvider } from '../../context/has_data_context'; import { PluginContext } from '../../context/plugin_context'; +import { ObservabilityPublicPluginsStart } from '../../plugin'; import { registerDataHandler, unregisterDataHandler } from '../../data_handler'; import { OverviewPage } from '.'; import { alertsFetchData } from './mock/alerts.mock'; @@ -88,6 +89,8 @@ const withCore = makeDecorator({ rules: { enabled: true }, }, }, + core: {} as CoreStart, + plugins: {} as ObservabilityPublicPluginsStart, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), ObservabilityPageTemplate: KibanaPageTemplate, kibanaFeatures: [], diff --git a/x-pack/plugins/observability/public/pages/rules/index.test.tsx b/x-pack/plugins/observability/public/pages/rules/index.test.tsx index 6987026b3b9bd..e932154526155 100644 --- a/x-pack/plugins/observability/public/pages/rules/index.test.tsx +++ b/x-pack/plugins/observability/public/pages/rules/index.test.tsx @@ -9,6 +9,8 @@ import React from 'react'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; import { ReactWrapper } from 'enzyme'; +import { CoreStart } from '@kbn/core/public'; +import { ObservabilityPublicPluginsStart } from '../../plugin'; import { RulesPage } from '.'; import { RulesTable } from './components/rules_table'; import { kibanaStartMock } from '../../utils/kibana_react.mock'; @@ -52,6 +54,8 @@ jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), ObservabilityPageTemplate: KibanaPageTemplate, kibanaFeatures: [], + core: {} as CoreStart, + plugins: {} as ObservabilityPublicPluginsStart, })); const { useFetchRules } = jest.requireMock('../../hooks/use_fetch_rules'); diff --git a/x-pack/plugins/observability/public/utils/test_helper.tsx b/x-pack/plugins/observability/public/utils/test_helper.tsx index bdbb9dd71164a..c4071070b73ce 100644 --- a/x-pack/plugins/observability/public/utils/test_helper.tsx +++ b/x-pack/plugins/observability/public/utils/test_helper.tsx @@ -7,6 +7,7 @@ import { render as testLibRender } from '@testing-library/react'; import { AppMountParameters } from '@kbn/core/public'; + import { coreMock } from '@kbn/core/public/mocks'; import React from 'react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; @@ -14,6 +15,7 @@ import { KibanaContextProvider, KibanaPageTemplate } from '@kbn/kibana-react-plu import translations from '@kbn/translations-plugin/translations/ja-JP.json'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { ObservabilityPublicPluginsStart } from '../plugin'; import { PluginContext } from '../context/plugin_context'; import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock'; @@ -21,6 +23,13 @@ const appMountParameters = { setHeaderActionMenu: () => {} } as unknown as AppMo export const core = coreMock.createStart(); export const data = dataPluginMock.createStartContract(); +const dataViewsMock = () => { + return {}; +}; + +const plugins = { + dataViews: dataViewsMock, +} as unknown as ObservabilityPublicPluginsStart; const config = { unsafe: { @@ -40,6 +49,8 @@ export const render = (component: React.ReactNode) => { value={{ appMountParameters, config, + core, + plugins, observabilityRuleTypeRegistry, ObservabilityPageTemplate: KibanaPageTemplate, kibanaFeatures: [], From a6370f3bd5438776e90ead157ea34374dc083aa1 Mon Sep 17 00:00:00 2001 From: Desmond Davis <99669693+dejadavi-el@users.noreply.github.com> Date: Mon, 23 May 2022 11:46:10 -0500 Subject: [PATCH 096/120] Update Prebuilt Telemetry Alert Filterlist (#132643) * first commit * Update x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts Updating nested fields to top level keys Co-authored-by: Pete Hampton Co-authored-by: Pete Hampton --- .../lib/telemetry/filterlists/prebuilt_rules_alerts.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts index fa0547fbf0125..15d4c8a608fd9 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts @@ -54,6 +54,12 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { 'kibana.alert.workflow_status': true, 'kibana.space_ids': true, 'kibana.version': true, + job_id: true, + causes: true, + typical: true, + multi_bucket_impact: true, + partition_field_name: true, + partition_field_value: true, // Alert specific filter entries agent: { id: true, From 23a345e78ddf94a4f05df32a1509ccbec153ced8 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 23 May 2022 18:52:58 +0200 Subject: [PATCH 097/120] [Synthetics] Getting started page (#132013) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 1 + .../common/constants/data_test_subjects.ts | 10 ++ .../plugins/synthetics/common/constants/ui.ts | 1 + .../plugins/synthetics/e2e/journeys/index.ts | 1 + .../e2e/journeys/monitor_name.journey.ts | 7 - .../synthetics/getting_started.journey.ts | 52 +++++++ .../e2e/journeys/synthetics/index.ts | 8 ++ .../e2e/page_objects/synthetics_app.tsx | 87 ++++++++++++ .../common/pages/synthetics_page_template.tsx | 13 +- .../form_fields/service_locations.tsx | 68 +++++++++ .../getting_started/getting_started_page.tsx | 97 +++++++++++++ .../simple_monitor_form.test.tsx | 84 +++++++++++ .../getting_started/simple_monitor_form.tsx | 134 ++++++++++++++++++ .../getting_started/use_simple_monitor.ts | 62 ++++++++ .../monitor_management_page.tsx | 21 ++- .../monitor_management/show_sync_errors.tsx | 12 +- .../monitor_management/use_breadcrumbs.ts | 9 +- .../components/overview/overview_page.tsx | 19 ++- .../public/apps/synthetics/hooks/index.ts | 2 - .../synthetics/hooks/use_no_data_config.ts | 47 ------ .../apps/synthetics/hooks/use_telemetry.ts | 46 ------ .../public/apps/synthetics/routes.tsx | 47 +++--- .../public/apps/synthetics/state/index.ts | 2 +- .../state/index_status/selectors.ts | 4 +- .../state/monitor_management/api.ts | 57 ++++++++ .../state/monitor_management/effects.ts | 34 +++++ .../state/monitor_management/monitor_list.ts | 37 +++++ .../state/monitor_management/selectors.ts | 12 ++ .../monitor_management/service_locations.ts | 45 ++++++ .../apps/synthetics/state/root_effect.ts | 7 +- .../apps/synthetics/state/root_reducer.ts | 6 +- .../apps/synthetics/state/ui/selectors.ts | 4 +- .../apps/synthetics/state/utils/actions.ts | 19 +++ .../synthetics/state/utils/fetch_effect.ts | 8 +- .../public/apps/synthetics/synthetics_app.tsx | 25 ++-- .../__mocks__/syncthetics_store.mock.ts | 31 +++- .../public/hooks/use_form_wrapped.tsx | 32 +++++ .../action_bar/action_bar.tsx | 4 +- .../legacy_uptime/hooks/use_telemetry.ts | 2 +- x-pack/plugins/synthetics/public/plugin.ts | 2 +- .../routes/monitor_cruds/add_monitor.ts | 6 +- yarn.lock | 5 + 42 files changed, 987 insertions(+), 183 deletions(-) create mode 100644 x-pack/plugins/synthetics/common/constants/data_test_subjects.ts create mode 100644 x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts create mode 100644 x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts create mode 100644 x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/form_fields/service_locations.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/simple_monitor_form.test.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/simple_monitor_form.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/use_simple_monitor.ts rename x-pack/plugins/synthetics/public/{legacy_uptime => apps/synthetics}/components/monitor_management/show_sync_errors.tsx (90%) delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_no_data_config.ts delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/effects.ts create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/monitor_list.ts create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/selectors.ts create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/service_locations.ts create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/utils/actions.ts create mode 100644 x-pack/plugins/synthetics/public/hooks/use_form_wrapped.tsx diff --git a/package.json b/package.json index e5fffb5b3a394..7cc2500e8dd6e 100644 --- a/package.json +++ b/package.json @@ -367,6 +367,7 @@ "react-dropzone": "^4.2.9", "react-fast-compare": "^2.0.4", "react-grid-layout": "^0.16.2", + "react-hook-form": "^7.30.0", "react-intl": "^2.8.0", "react-is": "^16.13.1", "react-markdown": "^4.3.1", diff --git a/x-pack/plugins/synthetics/common/constants/data_test_subjects.ts b/x-pack/plugins/synthetics/common/constants/data_test_subjects.ts new file mode 100644 index 0000000000000..f7e124c5a340e --- /dev/null +++ b/x-pack/plugins/synthetics/common/constants/data_test_subjects.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const syntheticsTestSubjects = { + urlsInput: 'urls-input', +}; diff --git a/x-pack/plugins/synthetics/common/constants/ui.ts b/x-pack/plugins/synthetics/common/constants/ui.ts index a736e3296161e..994cc20536723 100644 --- a/x-pack/plugins/synthetics/common/constants/ui.ts +++ b/x-pack/plugins/synthetics/common/constants/ui.ts @@ -14,6 +14,7 @@ export const MONITOR_EDIT_ROUTE = '/edit-monitor/:monitorId'; export const MONITOR_MANAGEMENT_ROUTE = '/manage-monitors'; export const OVERVIEW_ROUTE = '/'; +export const GETTING_STARTED_ROUTE = '/manage-monitors/getting-started'; export const SETTINGS_ROUTE = '/settings'; diff --git a/x-pack/plugins/synthetics/e2e/journeys/index.ts b/x-pack/plugins/synthetics/e2e/journeys/index.ts index 1fe02882fcd89..6f1d733e10537 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/index.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +export * from './synthetics'; export * from './data_view_permissions'; export * from './uptime.journey'; export * from './step_duration.journey'; diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts index a9dd2c4633402..4957df75aba13 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts @@ -1,10 +1,3 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts new file mode 100644 index 0000000000000..acef9e96e7f2d --- /dev/null +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, expect, before, Page } from '@elastic/synthetics'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; +import { byTestId } from '../utils'; + +journey(`Getting Started Page`, async ({ page, params }: { page: Page; params: any }) => { + const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl }); + + const createBasicMonitor = async () => { + await syntheticsApp.fillFirstMonitorDetails({ + url: 'https://www.elastic.co', + locations: ['us_central'], + apmServiceName: 'synthetics', + }); + }; + + before(async () => { + await syntheticsApp.waitForLoadingToFinish(); + }); + + step('Go to monitor-management', async () => { + await syntheticsApp.navigateToMonitorManagement(); + }); + + step('login to Kibana', async () => { + await syntheticsApp.loginToKibana(); + const invalid = await page.locator(`text=Username or password is incorrect. Please try again.`); + expect(await invalid.isVisible()).toBeFalsy(); + }); + + step('shows validation error on touch', async () => { + await page.click(byTestId('urls-input')); + await page.click(byTestId('comboBoxInput')); + expect(await page.isVisible('text=URL is required')).toBeTruthy(); + }); + + step('create basic monitor', async () => { + await createBasicMonitor(); + await syntheticsApp.confirmAndSave(); + }); + + step('it navigates to details page after saving', async () => { + await page.click('text=Dismiss'); + expect(await page.isVisible('text=My first monitor')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts new file mode 100644 index 0000000000000..1783ced950ca1 --- /dev/null +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './getting_started.journey'; diff --git a/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx b/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx new file mode 100644 index 0000000000000..1444e4282012f --- /dev/null +++ b/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Page } from '@elastic/synthetics'; +import { loginPageProvider } from './login'; +import { utilsPageProvider } from './utils'; + +export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kibanaUrl: string }) { + const remoteKibanaUrl = process.env.SYNTHETICS_REMOTE_KIBANA_URL; + const remoteUsername = process.env.SYNTHETICS_REMOTE_KIBANA_USERNAME; + const remotePassword = process.env.SYNTHETICS_REMOTE_KIBANA_PASSWORD; + const isRemote = Boolean(process.env.SYNTHETICS_REMOTE_ENABLED); + const basePath = isRemote ? remoteKibanaUrl : kibanaUrl; + const monitorManagement = `${basePath}/app/synthetics/manage-monitors`; + const addMonitor = `${basePath}/app/uptime/add-monitor`; + return { + ...loginPageProvider({ + page, + isRemote, + username: isRemote ? remoteUsername : 'elastic', + password: isRemote ? remotePassword : 'changeme', + }), + ...utilsPageProvider({ page }), + + async navigateToMonitorManagement() { + await page.goto(monitorManagement, { + waitUntil: 'networkidle', + }); + await this.waitForMonitorManagementLoadingToFinish(); + }, + + async waitForMonitorManagementLoadingToFinish() { + while (true) { + if ((await page.$(this.byTestId('uptimeLoader'))) === null) break; + await page.waitForTimeout(5 * 1000); + } + }, + + async getAddMonitorButton() { + return await this.findByTestSubj('syntheticsAddMonitorBtn'); + }, + + async navigateToAddMonitor() { + await page.goto(addMonitor, { + waitUntil: 'networkidle', + }); + }, + + async ensureIsOnMonitorConfigPage() { + await page.isVisible('[data-test-subj=monitorSettingsSection]'); + }, + + async confirmAndSave(isEditPage?: boolean) { + await this.ensureIsOnMonitorConfigPage(); + if (isEditPage) { + await page.click('text=Update monitor'); + } else { + await page.click('text=Create monitor'); + } + return await this.findByText('Monitor added successfully.'); + }, + + async selectLocations({ locations }: { locations: string[] }) { + for (let i = 0; i < locations.length; i++) { + await page.click(this.byTestId(`syntheticsServiceLocation--${locations[i]}`)); + } + }, + + async fillFirstMonitorDetails({ + url, + apmServiceName, + locations, + }: { + url: string; + apmServiceName: string; + locations: string[]; + }) { + await this.fillByTestSubj('urls-input', url); + await page.click(this.byTestId('comboBoxInput')); + await this.selectLocations({ locations }); + await page.click(this.byTestId('urls-input')); + }, + }; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx index 44b38236fc2a2..50497c4c9214c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx @@ -11,7 +11,6 @@ import { EuiPageHeaderProps, EuiPageTemplateProps } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useInspectorContext } from '@kbn/observability-plugin/public'; import { ClientPluginsStart } from '../../../../../plugin'; -import { useNoDataConfig } from '../../../hooks/use_no_data_config'; import { EmptyStateLoading } from '../../overview/empty_state/empty_state_loading'; import { EmptyStateError } from '../../overview/empty_state/empty_state_error'; import { useHasData } from '../../overview/empty_state/use_has_data'; @@ -51,8 +50,6 @@ export const SyntheticsPageTemplateComponent: React.FC {showLoading && } -
- {children} -
+
{children}
); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/form_fields/service_locations.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/form_fields/service_locations.tsx new file mode 100644 index 0000000000000..252b650cc7058 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/form_fields/service_locations.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiComboBox, EuiFormRow } from '@elastic/eui'; +import { Controller, FieldErrors, Control } from 'react-hook-form'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { useSelector } from 'react-redux'; +import { serviceLocationsSelector } from '../../../state/monitor_management/selectors'; +import { SimpleFormData } from '../simple_monitor_form'; +import { ConfigKey } from '../../../../../../common/constants/monitor_management'; + +export const ServiceLocationsField = ({ + errors, + control, +}: { + errors: FieldErrors; + control: Control; +}) => { + const locations = useSelector(serviceLocationsSelector); + + return ( + + ( + ({ + ...location, + 'data-test-subj': `syntheticsServiceLocation--${location.id}`, + }))} + selectedOptions={field.value} + isClearable={true} + data-test-subj="syntheticsServiceLocations" + {...field} + isInvalid={!!errors?.[ConfigKey.LOCATIONS]} + /> + )} + /> + + ); +}; + +const SELECT_ONE_OR_MORE_LOCATIONS = i18n.translate( + 'xpack.synthetics.monitorManagement.selectOneOrMoreLocations', + { + defaultMessage: 'Select one or more locations', + } +); + +const LOCATIONS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.locationsLabel', { + defaultMessage: 'Locations', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx new file mode 100644 index 0000000000000..767a48f22dae9 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { EuiEmptyPrompt, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { useDispatch } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { useBreadcrumbs } from '../../hooks'; +import { fetchServiceLocationsAction } from '../../state/monitor_management/service_locations'; +import { SimpleMonitorForm } from './simple_monitor_form'; +import { MONITORING_OVERVIEW_LABEL } from '../../../../legacy_uptime/routes'; + +export const GettingStartedPage = () => { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchServiceLocationsAction.get()); + }, [dispatch]); + + useBreadcrumbs([{ text: MONITORING_OVERVIEW_LABEL }]); // No extra breadcrumbs on overview + + return ( + + {CREATE_SINGLE_PAGE_LABEL}} + layout="horizontal" + color="plain" + body={ + <> + + {OR_LABEL}{' '} + {SELECT_DIFFERENT_MONITOR} + {i18n.translate('xpack.synthetics.gettingStarted.createSingle.description', { + defaultMessage: ' to get started with Elastic Synthetics Monitoring', + })} + + + + + } + footer={ + <> + + {FOR_MORE_INFO_LABEL} + {' '} + + {GETTING_STARTED_LABEL} + + + } + /> + + ); +}; + +const Wrapper = styled.div` + &&& { + .euiEmptyPrompt__content { + max-width: 40em; + padding: 0; + } + } +`; + +const FOR_MORE_INFO_LABEL = i18n.translate('xpack.synthetics.gettingStarted.forMoreInfo', { + defaultMessage: 'For more information, read our', +}); + +const CREATE_SINGLE_PAGE_LABEL = i18n.translate( + 'xpack.synthetics.gettingStarted.createSinglePageLabel', + { + defaultMessage: 'Create a single page browser monitor', + } +); + +const GETTING_STARTED_LABEL = i18n.translate( + 'xpack.synthetics.gettingStarted.gettingStartedLabel', + { + defaultMessage: 'Getting Started Guide', + } +); + +const SELECT_DIFFERENT_MONITOR = i18n.translate( + 'xpack.synthetics.gettingStarted.gettingStartedLabel.selectDifferentMonitor', + { + defaultMessage: 'select a different monitor type', + } +); + +const OR_LABEL = i18n.translate('xpack.synthetics.gettingStarted.orLabel', { + defaultMessage: 'Or', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/simple_monitor_form.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/simple_monitor_form.test.tsx new file mode 100644 index 0000000000000..0ab9834f190bd --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/simple_monitor_form.test.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CREATE_MONITOR_LABEL, + SimpleMonitorForm, + URL_REQUIRED_LABEL, + WEBSITE_URL_HELP_TEXT, + WEBSITE_URL_LABEL, +} from './simple_monitor_form'; +import { screen } from '@testing-library/react'; +import { render } from '../../utils/testing'; +import React from 'react'; +import { act, fireEvent, waitFor } from '@testing-library/react'; +import { syntheticsTestSubjects } from '../../../../../common/constants/data_test_subjects'; +import { apiService } from '../../../../utils/api_service'; + +describe('SimpleMonitorForm', () => { + const apiSpy = jest.spyOn(apiService, 'post'); + it('renders', async () => { + render(); + expect(screen.getByText(WEBSITE_URL_LABEL)).toBeInTheDocument(); + expect(screen.getByText(WEBSITE_URL_HELP_TEXT)).toBeInTheDocument(); + }); + + it('do not show validation error on touch', async () => { + render(); + await act(async () => { + fireEvent.click(screen.getByTestId(syntheticsTestSubjects.urlsInput)); + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('comboBoxInput')); + }); + + expect(await screen.queryByText(URL_REQUIRED_LABEL)).not.toBeInTheDocument(); + }); + + it('shows the validation errors on submit', async () => { + render(); + + await act(async () => { + fireEvent.click(screen.getByTestId(syntheticsTestSubjects.urlsInput)); + }); + + await act(async () => { + fireEvent.click(screen.getByText(CREATE_MONITOR_LABEL)); + }); + + expect(screen.getByText('Please address the highlighted errors.')).toBeInTheDocument(); + }); + + it('submits valid monitor', async () => { + render(); + + await act(async () => { + fireEvent.input(screen.getByTestId(syntheticsTestSubjects.urlsInput), { + target: { value: 'https://www.elastic.co' }, + }); + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('comboBoxInput')); + }); + + expect(screen.getByText('US Central')).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(screen.getByTestId(`syntheticsServiceLocation--us_central`)); + }); + + await act(async () => { + fireEvent.click(screen.getByText(CREATE_MONITOR_LABEL)); + }); + + await waitFor(async () => { + expect(apiSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/simple_monitor_form.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/simple_monitor_form.tsx new file mode 100644 index 0000000000000..77dc0e35effb6 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/simple_monitor_form.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFieldText, + EuiFormRow, + EuiForm, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useSimpleMonitor } from './use_simple_monitor'; +import { ServiceLocationsField } from './form_fields/service_locations'; +import { ConfigKey, ServiceLocations } from '../../../../../common/runtime_types'; +import { useFormWrapped } from '../../../../hooks/use_form_wrapped'; + +export interface SimpleFormData { + urls: string; + locations: ServiceLocations; +} + +export const SimpleMonitorForm = () => { + const { + control, + register, + handleSubmit, + formState: { errors, isValid, isSubmitted }, + } = useFormWrapped({ + mode: 'onSubmit', + reValidateMode: 'onChange', + shouldFocusError: true, + defaultValues: { urls: '', locations: [] as ServiceLocations }, + }); + + const [monitorData, setMonitorData] = useState(); + + const onSubmit = (data: SimpleFormData) => { + setMonitorData(data); + }; + + const { loading } = useSimpleMonitor({ monitorData }); + + const hasURLError = !!errors?.[ConfigKey.URLS]; + + return ( + + + + + + + + + + + {CREATE_MONITOR_LABEL} + + + + + ); +}; + +export const MY_FIRST_MONITOR = i18n.translate( + 'xpack.synthetics.monitorManagement.myFirstMonitor', + { + defaultMessage: 'My first monitor', + } +); + +export const WEBSITE_URL_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.websiteUrlLabel', + { + defaultMessage: 'Website URL', + } +); + +export const WEBSITE_URL_PLACEHOLDER = i18n.translate( + 'xpack.synthetics.monitorManagement.websiteUrlPlaceholder', + { + defaultMessage: 'Enter a website URL', + } +); + +export const WEBSITE_URL_HELP_TEXT = i18n.translate( + 'xpack.synthetics.monitorManagement.websiteUrlHelpText', + { + defaultMessage: `For example, your company's homepage or https://elastic.co`, + } +); + +export const CREATE_MONITOR_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.createMonitorLabel', + { defaultMessage: 'Create monitor' } +); + +export const MONITOR_SUCCESS_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorAddedSuccessMessage', + { + defaultMessage: 'Monitor added successfully.', + } +); + +export const URL_REQUIRED_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.urlRequiredLabel', + { + defaultMessage: 'URL is required', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/use_simple_monitor.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/use_simple_monitor.ts new file mode 100644 index 0000000000000..81585a9f26a99 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/use_simple_monitor.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useFetcher } from '@kbn/observability-plugin/public'; +import { useEffect } from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useSelector } from 'react-redux'; +import { serviceLocationsSelector } from '../../state/monitor_management/selectors'; +import { showSyncErrors } from '../monitor_management/show_sync_errors'; +import { createMonitorAPI } from '../../state/monitor_management/api'; +import { DEFAULT_FIELDS } from '../../../../../common/constants/monitor_defaults'; +import { ConfigKey } from '../../../../../common/constants/monitor_management'; +import { DataStream, SyntheticsMonitorWithId } from '../../../../../common/runtime_types'; +import { MONITOR_SUCCESS_LABEL, MY_FIRST_MONITOR, SimpleFormData } from './simple_monitor_form'; +import { kibanaService } from '../../../../utils/kibana_service'; + +export const useSimpleMonitor = ({ monitorData }: { monitorData?: SimpleFormData }) => { + const { application } = useKibana().services; + const locationsList = useSelector(serviceLocationsSelector); + + const { data, loading } = useFetcher(() => { + if (!monitorData) { + return new Promise((resolve) => resolve(undefined)); + } + const { urls, locations } = monitorData; + + return createMonitorAPI({ + monitor: { + ...DEFAULT_FIELDS.browser, + 'source.inline.script': `step('Go to ${urls}', async () => { + await page.goto('${urls}'); +});`, + [ConfigKey.MONITOR_TYPE]: DataStream.BROWSER, + [ConfigKey.NAME]: MY_FIRST_MONITOR, + [ConfigKey.LOCATIONS]: locations, + [ConfigKey.URLS]: urls, + }, + }); + }, [monitorData]); + + useEffect(() => { + const newMonitor = data as SyntheticsMonitorWithId; + const hasErrors = data && 'attributes' in data && data.attributes.errors?.length > 0; + if (hasErrors && !loading) { + showSyncErrors(data.attributes.errors, locationsList, kibanaService.toasts); + } + + if (!loading && newMonitor?.id) { + kibanaService.toasts.addSuccess({ + title: MONITOR_SUCCESS_LABEL, + toastLifeTimeMs: 3000, + }); + application?.navigateToApp('uptime', { path: `/monitor/${btoa(newMonitor.id)}` }); + } + }, [application, data, loading, locationsList]); + + return { data, loading }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/monitor_management_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/monitor_management_page.tsx index c4ca665563f0d..7c20fcfe1c143 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/monitor_management_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/monitor_management_page.tsx @@ -5,8 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { useTrackPageview } from '@kbn/observability-plugin/public'; +import { useDispatch, useSelector } from 'react-redux'; +import { Redirect } from 'react-router-dom'; +import { monitorListSelector } from '../../state/monitor_management/selectors'; +import { fetchMonitorListAction } from '../../state/monitor_management/monitor_list'; +import { GETTING_STARTED_ROUTE } from '../../../../../common/constants'; import { useMonitorManagementBreadcrumbs } from './use_breadcrumbs'; export const MonitorManagementPage: React.FC = () => { @@ -14,9 +19,21 @@ export const MonitorManagementPage: React.FC = () => { useTrackPageview({ app: 'synthetics', path: 'manage-monitors', delay: 15000 }); useMonitorManagementBreadcrumbs(); + const dispatch = useDispatch(); + + const { total } = useSelector(monitorListSelector); + + useEffect(() => { + dispatch(fetchMonitorListAction.get()); + }, [dispatch]); + + if (total === 0) { + return ; + } + return ( <> -

Monitor Management List page (Monitor Management Page)

+

This page is under construction and will be updated in a future release

); }; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/show_sync_errors.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/show_sync_errors.tsx similarity index 90% rename from x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/show_sync_errors.tsx rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/show_sync_errors.tsx index 0fde06c764c08..2d4412b71f230 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/show_sync_errors.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/show_sync_errors.tsx @@ -8,13 +8,17 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { kibanaService } from '../../state/kibana_service'; -import { ServiceLocationErrors, ServiceLocations } from '../../../../common/runtime_types'; +import { IToasts } from '@kbn/core/public'; +import { ServiceLocationErrors, ServiceLocations } from '../../../../../common/runtime_types'; -export const showSyncErrors = (errors: ServiceLocationErrors, locations: ServiceLocations) => { +export const showSyncErrors = ( + errors: ServiceLocationErrors, + locations: ServiceLocations, + toasts: IToasts +) => { Object.values(errors).forEach((location) => { const { status: responseStatus, reason } = location.error || {}; - kibanaService.toasts.addWarning({ + toasts.addWarning({ title: i18n.translate('xpack.synthetics.monitorManagement.service.error.title', { defaultMessage: `Unable to sync monitor config`, }), diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts index be94df18ef7d2..30d23128d1e82 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts @@ -22,9 +22,6 @@ export const useMonitorManagementBreadcrumbs = () => { ]); }; -const MONITOR_MANAGEMENT_CRUMB = i18n.translate( - 'xpack.synthetics.monitorManagementPage.monitorManagementCrumb', - { - defaultMessage: 'Monitor Management', - } -); +const MONITOR_MANAGEMENT_CRUMB = i18n.translate('xpack.synthetics.monitorsPage.monitorCrumb', { + defaultMessage: 'Monitors', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/overview_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/overview_page.tsx index 4a370fc024423..9e229308a402e 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/overview_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/overview_page.tsx @@ -6,8 +6,13 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; -import React from 'react'; +import React, { useEffect } from 'react'; import { useTrackPageview } from '@kbn/observability-plugin/public'; +import { useDispatch, useSelector } from 'react-redux'; +import { Redirect } from 'react-router-dom'; +import { monitorListSelector } from '../../state/monitor_management/selectors'; +import { GETTING_STARTED_ROUTE } from '../../../../../common/constants'; +import { fetchMonitorListAction } from '../../state/monitor_management/monitor_list'; import { useSyntheticsSettingsContext } from '../../contexts'; import { useOverviewBreadcrumbs } from './use_breadcrumbs'; @@ -17,6 +22,18 @@ export const OverviewPage: React.FC = () => { useOverviewBreadcrumbs(); const { basePath } = useSyntheticsSettingsContext(); + const dispatch = useDispatch(); + + const { total } = useSelector(monitorListSelector); + + useEffect(() => { + dispatch(fetchMonitorListAction.get()); + }, [dispatch]); + + if (total === 0) { + return ; + } + return ( diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts index 15079dc68823b..c3cde2eaffec5 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts @@ -7,7 +7,5 @@ export * from './use_url_params'; export * from './use_breadcrumbs'; -export * from './use_telemetry'; export * from '../../../hooks/use_breakpoints'; export * from './use_service_allowed'; -export * from './use_no_data_config'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_no_data_config.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_no_data_config.ts deleted file mode 100644 index 6243d2f5d8c8a..0000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_no_data_config.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { useContext } from 'react'; -import { useSelector } from 'react-redux'; -import { KibanaPageTemplateProps, useKibana } from '@kbn/kibana-react-plugin/public'; -import { SyntheticsSettingsContext } from '../contexts'; -import { ClientPluginsStart } from '../../../plugin'; -import { selectIndexState } from '../state'; - -export function useNoDataConfig(): KibanaPageTemplateProps['noDataConfig'] { - const { basePath } = useContext(SyntheticsSettingsContext); - - const { - services: { docLinks }, - } = useKibana(); - - const { data } = useSelector(selectIndexState); - - // Returns no data config when there is no historical data - if (data && !data.indexExists) { - return { - solution: i18n.translate('xpack.synthetics.noDataConfig.solutionName', { - defaultMessage: 'Observability', - }), - actions: { - beats: { - title: i18n.translate('xpack.synthetics.noDataConfig.beatsCard.title', { - defaultMessage: 'Add monitors with Heartbeat', - }), - description: i18n.translate('xpack.synthetics.noDataConfig.beatsCard.description', { - defaultMessage: - 'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users experience.', - }), - href: basePath + `/app/home#/tutorial/uptimeMonitors`, - }, - }, - docsLink: docLinks!.links.observability.guide, - }; - } -} -// TODO: Change no data config for Synthetics diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts deleted file mode 100644 index 64ecabaff5d5a..0000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect } from 'react'; -import { useGetUrlParams } from './use_url_params'; -import { apiService } from '../../../utils/api_service'; -// import { API_URLS } from '../../../common/constants'; - -export enum SyntheticsPage { - Overview = 'Overview', - Monitor = 'Monitor', - MonitorAdd = 'AddMonitor', - MonitorEdit = 'EditMonitor', - MonitorManagement = 'MonitorManagement', - Settings = 'Settings', - Certificates = 'Certificates', - StepDetail = 'StepDetail', - SyntheticCheckStepsPage = 'SyntheticCheckStepsPage', - NotFound = '__not-found__', -} - -export const useSyntheticsTelemetry = (page?: SyntheticsPage) => { - const { dateRangeStart, dateRangeEnd, autorefreshInterval, autorefreshIsPaused } = - useGetUrlParams(); - - useEffect(() => { - if (!apiService.http) throw new Error('Core http services are not defined'); - - /* - TODO: Add/Modify telemetry endpoint for synthetics - const params = { - page, - autorefreshInterval: autorefreshInterval / 1000, // divide by 1000 to keep it in secs - dateStart: dateRangeStart, - dateEnd: dateRangeEnd, - autoRefreshEnabled: !autorefreshIsPaused, - };*/ - setTimeout(() => { - // apiService.post(API_URLS.LOG_PAGE_VIEW, params); - }, 100); - }, [autorefreshInterval, autorefreshIsPaused, dateRangeEnd, dateRangeStart, page]); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index 7f04b3992885b..27f2599fbc102 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -12,27 +12,27 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { APP_WRAPPER_CLASS } from '@kbn/core/public'; import { useInspectorContext } from '@kbn/observability-plugin/public'; +import { GettingStartedPage } from './components/getting_started/getting_started_page'; import { MonitorAddEditPage } from './components/monitor_add_edit/monitor_add_edit_page'; import { OverviewPage } from './components/overview/overview_page'; import { SyntheticsPageTemplateComponent } from './components/common/pages/synthetics_page_template'; import { NotFoundPage } from './components/common/pages/not_found'; import { ServiceAllowedWrapper } from './components/common/wrappers/service_allowed_wrapper'; import { + GETTING_STARTED_ROUTE, MONITOR_ADD_ROUTE, MONITOR_MANAGEMENT_ROUTE, OVERVIEW_ROUTE, } from '../../../common/constants'; import { MonitorManagementPage } from './components/monitor_management/monitor_management_page'; import { apiService } from '../../utils/api_service'; -import { SyntheticsPage, useSyntheticsTelemetry } from './hooks/use_telemetry'; type RouteProps = { path: string; component: React.FC; dataTestSubj: string; title: string; - telemetryId: SyntheticsPage; - pageHeader: { + pageHeader?: { pageTitle: string | JSX.Element; children?: JSX.Element; rightSideItems?: JSX.Element[]; @@ -43,15 +43,22 @@ const baseTitle = i18n.translate('xpack.synthetics.routes.baseTitle', { defaultMessage: 'Synthetics - Kibana', }); -export const MONITOR_MANAGEMENT_LABEL = i18n.translate( - 'xpack.synthetics.monitorManagement.heading', - { - defaultMessage: 'Monitor Management', - } -); - const getRoutes = (): RouteProps[] => { return [ + { + title: i18n.translate('xpack.synthetics.gettingStartedRoute.title', { + defaultMessage: 'Synthetics Getting Started | {baseTitle}', + values: { baseTitle }, + }), + path: GETTING_STARTED_ROUTE, + component: () => , + dataTestSubj: 'syntheticsGettingStartedPage', + template: 'centeredBody', + pageContentProps: { + paddingSize: 'none', + hasShadow: false, + }, + }, { title: i18n.translate('xpack.synthetics.overviewRoute.title', { defaultMessage: 'Synthetics Overview | {baseTitle}', @@ -60,7 +67,6 @@ const getRoutes = (): RouteProps[] => { path: OVERVIEW_ROUTE, component: () => , dataTestSubj: 'syntheticsOverviewPage', - telemetryId: SyntheticsPage.Overview, pageHeader: { pageTitle: ( @@ -89,14 +95,13 @@ const getRoutes = (): RouteProps[] => { ), dataTestSubj: 'syntheticsMonitorManagementPage', - telemetryId: SyntheticsPage.MonitorManagement, pageHeader: { pageTitle: ( @@ -118,7 +123,6 @@ const getRoutes = (): RouteProps[] => { ), dataTestSubj: 'syntheticsMonitorAddPage', - telemetryId: SyntheticsPage.MonitorAdd, pageHeader: { pageTitle: ( { ]; }; -const RouteInit: React.FC> = ({ - path, - title, - telemetryId, -}) => { - useSyntheticsTelemetry(telemetryId); +const RouteInit: React.FC> = ({ path, title }) => { useEffect(() => { document.title = title; }, [path, title]); @@ -159,14 +158,12 @@ export const PageRouter: FC = () => { path, component: RouteComponent, dataTestSubj, - telemetryId, pageHeader, ...pageTemplateProps }) => ( - +
- {/* TODO: See if the callout is needed for Synthetics App as well */} - + appState.indexStatus; +const getState = (appState: SyntheticsAppState) => appState.indexStatus; export const selectIndexState = createSelector(getState, (state) => state); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts new file mode 100644 index 0000000000000..777e72069f6f2 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ServiceLocationsState } from './service_locations'; +import { apiService } from '../../../../utils/api_service'; +import { + EncryptedSyntheticsMonitor, + FetchMonitorManagementListQueryArgs, + MonitorManagementListResult, + MonitorManagementListResultCodec, + ServiceLocationErrors, + ServiceLocationsApiResponseCodec, + SyntheticsMonitor, + SyntheticsMonitorWithId, +} from '../../../../../common/runtime_types'; +import { API_URLS } from '../../../../../common/constants'; + +export const createMonitorAPI = async ({ + monitor, +}: { + monitor: SyntheticsMonitor | EncryptedSyntheticsMonitor; +}): Promise<{ attributes: { errors: ServiceLocationErrors } } | SyntheticsMonitor> => { + return await apiService.post(API_URLS.SYNTHETICS_MONITORS, monitor); +}; + +export const updateMonitorAPI = async ({ + monitor, + id, +}: { + monitor: SyntheticsMonitor | EncryptedSyntheticsMonitor; + id: string; +}): Promise<{ attributes: { errors: ServiceLocationErrors } } | SyntheticsMonitorWithId> => { + return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor); +}; + +export const fetchServiceLocations = async (): Promise => { + const { throttling, locations } = await apiService.get( + API_URLS.SERVICE_LOCATIONS, + undefined, + ServiceLocationsApiResponseCodec + ); + return { throttling, locations }; +}; + +export const fetchMonitorManagementList = async ( + params: FetchMonitorManagementListQueryArgs +): Promise => { + return await apiService.get( + API_URLS.SYNTHETICS_MONITORS, + params, + MonitorManagementListResultCodec + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/effects.ts new file mode 100644 index 0000000000000..924fb8baf1da0 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/effects.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { takeLeading } from 'redux-saga/effects'; +import { fetchMonitorListAction } from './monitor_list'; +import { fetchMonitorManagementList, fetchServiceLocations } from './api'; +import { fetchEffectFactory } from '../utils/fetch_effect'; +import { fetchServiceLocationsAction } from './service_locations'; + +export function* fetchServiceLocationsEffect() { + yield takeLeading( + String(fetchServiceLocationsAction.get), + fetchEffectFactory( + fetchServiceLocations, + fetchServiceLocationsAction.success, + fetchServiceLocationsAction.fail + ) + ); +} + +export function* fetchMonitorListEffect() { + yield takeLeading( + String(fetchMonitorListAction.get), + fetchEffectFactory( + fetchMonitorManagementList, + fetchMonitorListAction.success, + fetchMonitorListAction.fail + ) + ); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/monitor_list.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/monitor_list.ts new file mode 100644 index 0000000000000..2493f9eb173d8 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/monitor_list.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createReducer } from '@reduxjs/toolkit'; +import { IHttpFetchError } from '@kbn/core/public'; +import { createAsyncAction, Nullable } from '../utils/actions'; +import { MonitorManagementListResult } from '../../../../../common/runtime_types'; + +export const fetchMonitorListAction = createAsyncAction( + 'fetchMonitorListAction' +); + +export const monitorListReducer = createReducer( + { + data: {} as MonitorManagementListResult, + loading: false, + error: null as Nullable, + }, + (builder) => { + builder + .addCase(fetchMonitorListAction.get, (state, action) => { + state.loading = true; + }) + .addCase(fetchMonitorListAction.success, (state, action) => { + state.loading = false; + state.data = action.payload; + }) + .addCase(fetchMonitorListAction.fail, (state, action) => { + state.loading = false; + state.error = action.payload; + }); + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/selectors.ts new file mode 100644 index 0000000000000..5c7dc8360ec9f --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/selectors.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { SyntheticsAppState } from '../root_reducer'; + +export const monitorListSelector = (state: SyntheticsAppState) => state.monitorList.data; + +export const serviceLocationsSelector = (state: SyntheticsAppState) => + state.serviceLocations.locations; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/service_locations.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/service_locations.ts new file mode 100644 index 0000000000000..572d00ce3892f --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/service_locations.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createReducer, PayloadAction } from '@reduxjs/toolkit'; +import { IHttpFetchError } from '@kbn/core/public'; +import { createAsyncAction, Nullable } from '../utils/actions'; +import { ServiceLocations, ThrottlingOptions } from '../../../../../common/runtime_types'; + +export const fetchServiceLocationsAction = createAsyncAction( + 'fetchServiceLocationsAction' +); + +export interface ServiceLocationsState { + throttling: ThrottlingOptions | undefined; + locations: ServiceLocations; +} + +export const serviceLocationReducer = createReducer( + { + locations: [] as ServiceLocations, + loading: false, + error: null as Nullable, + }, + (builder) => { + builder + .addCase(fetchServiceLocationsAction.get, (state, action) => { + state.loading = true; + }) + .addCase( + fetchServiceLocationsAction.success, + (state, action: PayloadAction) => { + state.loading = false; + state.locations = action.payload.locations; + } + ) + .addCase(fetchServiceLocationsAction.fail, (state, action) => { + state.loading = false; + state.error = action.payload; + }); + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts index 7d1aaa60aa4f3..9a66b4e6b9e74 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts @@ -6,8 +6,13 @@ */ import { all, fork } from 'redux-saga/effects'; +import { fetchMonitorListEffect, fetchServiceLocationsEffect } from './monitor_management/effects'; import { fetchIndexStatusEffect } from './index_status'; export const rootEffect = function* root(): Generator { - yield all([fork(fetchIndexStatusEffect)]); + yield all([ + fork(fetchIndexStatusEffect), + fork(fetchServiceLocationsEffect), + fork(fetchMonitorListEffect), + ]); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts index a57c0af9e6fdb..1c8ed190fd80e 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts @@ -7,12 +7,16 @@ import { combineReducers } from '@reduxjs/toolkit'; +import { monitorListReducer } from './monitor_management/monitor_list'; +import { serviceLocationReducer } from './monitor_management/service_locations'; import { uiReducer } from './ui'; import { indexStatusReducer } from './index_status'; export const rootReducer = combineReducers({ ui: uiReducer, indexStatus: indexStatusReducer, + serviceLocations: serviceLocationReducer, + monitorList: monitorListReducer, }); -export type RooState = ReturnType; +export type SyntheticsAppState = ReturnType; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/selectors.ts index 52d9841cde961..8896e85f68290 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/selectors.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/selectors.ts @@ -6,9 +6,9 @@ */ import { createSelector } from 'reselect'; -import type { RooState } from '../root_reducer'; +import type { SyntheticsAppState } from '../root_reducer'; -const uiStateSelector = (appState: RooState) => appState.ui; +const uiStateSelector = (appState: SyntheticsAppState) => appState.ui; export const selectBasePath = createSelector(uiStateSelector, ({ basePath }) => basePath); export const selectIsIntegrationsPopupOpen = createSelector( diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/actions.ts new file mode 100644 index 0000000000000..d8354ee2887a0 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/actions.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IHttpFetchError } from '@kbn/core/public'; +import { createAction } from '@reduxjs/toolkit'; + +export function createAsyncAction(actionStr: string) { + return { + get: createAction(actionStr), + success: createAction(`${actionStr}_SUCCESS`), + fail: createAction(`${actionStr}_FAIL`), + }; +} + +export type Nullable = T | null; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/fetch_effect.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/fetch_effect.ts index 387ceeb56a514..a7b3dbc9c72b3 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/fetch_effect.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/fetch_effect.ts @@ -6,7 +6,7 @@ */ import { call, put } from 'redux-saga/effects'; -import { Action } from 'redux-actions'; +import { PayloadAction } from '@reduxjs/toolkit'; import { IHttpFetchError } from '@kbn/core/public'; /** @@ -22,10 +22,10 @@ import { IHttpFetchError } from '@kbn/core/public'; */ export function fetchEffectFactory( fetch: (request: T) => Promise, - success: (response: R) => Action, - fail: (error: IHttpFetchError) => Action + success: (response: R) => PayloadAction, + fail: (error: IHttpFetchError) => PayloadAction ) { - return function* (action: Action): Generator { + return function* (action: PayloadAction): Generator { try { const response = yield call(fetch, action.payload); if (response instanceof Error) { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx index 07fb3604abd42..808444c1f8ec8 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx @@ -24,7 +24,6 @@ import { SyntheticsSettingsContextProvider, SyntheticsThemeContextProvider, SyntheticsStartupPluginsContextProvider, - SyntheticsDataViewContextProvider, } from './contexts'; import { PageRouter } from './routes'; @@ -91,19 +90,17 @@ const Application = (props: SyntheticsAppProps) => { - -
- - - - - - -
-
+
+ + + + + + +
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts index adf2a15e70fa6..3b357cb18fa7c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { AppState } from '../../../state'; +import { SyntheticsAppState } from '../../../state/root_reducer'; /** * NOTE: This variable name MUST start with 'mock*' in order for * Jest to accept its use within a jest.mock() */ -export const mockState: AppState = { +export const mockState: SyntheticsAppState = { ui: { alertFlyoutVisible: false, basePath: 'yyz', @@ -25,6 +25,33 @@ export const mockState: AppState = { error: null, loading: false, }, + serviceLocations: { + locations: [ + { + id: 'us_central', + label: 'US Central', + geo: { + lat: 41.25, + lon: -95.86, + }, + url: 'https://test.elastic.dev', + isServiceManaged: true, + }, + ], + loading: false, + error: null, + }, + monitorList: { + data: { + total: 0, + monitors: [], + perPage: 0, + page: 0, + syncErrors: [], + }, + error: null, + loading: false, + }, }; // TODO: Complete mock state diff --git a/x-pack/plugins/synthetics/public/hooks/use_form_wrapped.tsx b/x-pack/plugins/synthetics/public/hooks/use_form_wrapped.tsx new file mode 100644 index 0000000000000..5e80eaee2c031 --- /dev/null +++ b/x-pack/plugins/synthetics/public/hooks/use_form_wrapped.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { FieldValues, useForm, UseFormProps } from 'react-hook-form'; + +export function useFormWrapped( + props?: UseFormProps +) { + const { register, ...restOfForm } = useForm(props); + + const euiRegister = useCallback( + (name, ...registerArgs) => { + const { ref, ...restOfRegister } = register(name, ...registerArgs); + + return { + inputRef: ref, + ...restOfRegister, + }; + }, + [register] + ); + + return { + register: euiRegister, + ...restOfForm, + }; +} diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/action_bar/action_bar.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/action_bar/action_bar.tsx index 80e614eb4d77f..0963500a168ba 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/action_bar/action_bar.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/action_bar/action_bar.tsx @@ -31,7 +31,7 @@ import { TestRun } from '../test_now_mode/test_now_mode'; import { monitorManagementListSelector } from '../../../state/selectors'; import { kibanaService } from '../../../state/kibana_service'; -import { showSyncErrors } from '../show_sync_errors'; +import { showSyncErrors } from '../../../../apps/synthetics/components/monitor_management/show_sync_errors'; export interface ActionBarProps { monitor: SyntheticsMonitor; @@ -103,7 +103,7 @@ export const ActionBar = ({ }); setIsSuccessful(true); } else if (hasErrors && !loading) { - showSyncErrors(data.attributes.errors, locations); + showSyncErrors(data.attributes.errors, locations, kibanaService.toasts); setIsSuccessful(true); } }, [data, status, isSaving, isValid, monitorId, hasErrors, locations, loading]); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/hooks/use_telemetry.ts b/x-pack/plugins/synthetics/public/legacy_uptime/hooks/use_telemetry.ts index 78062bb1ff7eb..fc221dedeeef2 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/hooks/use_telemetry.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/hooks/use_telemetry.ts @@ -11,7 +11,7 @@ import { apiService } from '../state/api/utils'; import { API_URLS } from '../../../common/constants'; export enum UptimePage { - Overview = 'Overview', + Overview = 'GettingStarted', MappingError = 'MappingError', Monitor = 'Monitor', MonitorAdd = 'AddMonitor', diff --git a/x-pack/plugins/synthetics/public/plugin.ts b/x-pack/plugins/synthetics/public/plugin.ts index 0daf2fbe74de2..127655f95e8a6 100644 --- a/x-pack/plugins/synthetics/public/plugin.ts +++ b/x-pack/plugins/synthetics/public/plugin.ts @@ -263,7 +263,7 @@ function registerSyntheticsRoutesWithNavigation( }), app: 'synthetics', path: '/manage-monitors', - matchFullPath: true, + matchFullPath: false, ignoreTrailingSlash: true, }, ], diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts index e39950699cb4a..2e3a76cdad964 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts @@ -82,7 +82,11 @@ export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ if (errors && errors.length > 0) { return response.ok({ - body: { message: 'error pushing monitor to the service', attributes: { errors } }, + body: { + message: 'error pushing monitor to the service', + attributes: { errors }, + id: newMonitor.id, + }, }); } diff --git a/yarn.lock b/yarn.lock index 35c60d9444f32..267029dfe8068 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23694,6 +23694,11 @@ react-helmet-async@^1.0.7: react-fast-compare "^3.2.0" shallowequal "^1.1.0" +react-hook-form@^7.30.0: + version "7.30.0" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.30.0.tgz#c9e2fd54d3627e43bd94bf38ef549df2e80c1371" + integrity sha512-DzjiM6o2vtDGNMB9I4yCqW8J21P314SboNG1O0obROkbg7KVS0I7bMtwSdKyapnCPjHgnxc3L7E5PEdISeEUcQ== + react-input-autosize@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2" From 7a5fef193eba1d39ef9914938c327245ba2afdda Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 23 May 2022 19:06:04 +0200 Subject: [PATCH 098/120] [ML] Context for recovered alerts (#132496) * recovered context for ad alerting rule * datafeed report for recovered alerts * mml report for recovered alerts * update executor for setting recovered context * update jest tests, fix mml check * update error messages check * update jest tests * update delayed data test * fix the mml check * enable doesSetRecoveryContext * add rule.name to the default message * fix datafeed check * recovered message * refactor, update anomaly explorer URL time range for recovered alerts * update message for recovered errorMessage alert * update delayedDataRecoveryMessage * fix time range * update message for recovered anomaly detection alert * update mml messages --- .../ml/public/alerting/register_ml_alerts.ts | 2 +- .../ml/server/lib/alerts/alerting_service.ts | 301 ++++++++++++------ .../lib/alerts/jobs_health_service.test.ts | 25 +- .../server/lib/alerts/jobs_health_service.ts | 227 ++++++++----- .../register_anomaly_detection_alert_type.ts | 32 +- .../register_jobs_monitoring_rule_type.ts | 19 +- 6 files changed, 399 insertions(+), 207 deletions(-) diff --git a/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts b/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts index b3a4c5420ca46..75aea773f662b 100644 --- a/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts +++ b/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts @@ -104,7 +104,7 @@ export function registerMlAlerts( defaultActionMessage: i18n.translate( 'xpack.ml.alertTypes.anomalyDetection.defaultActionMessage', { - defaultMessage: `Elastic Stack Machine Learning Alert: + defaultMessage: `[\\{\\{rule.name\\}\\}] Elastic Stack Machine Learning Alert: - Job IDs: \\{\\{context.jobIds\\}\\} - Time: \\{\\{context.timestampIso8601\\}\\} - Anomaly score: \\{\\{context.score\\}\\} diff --git a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts index bd0d8df872125..d354e0a4eca3c 100644 --- a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts @@ -6,10 +6,10 @@ */ import Boom from '@hapi/boom'; +import { i18n } from '@kbn/i18n'; import rison from 'rison-node'; import { Duration } from 'moment/moment'; import { memoize } from 'lodash'; -import type { MlDatafeed } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { FIELD_FORMAT_IDS, IFieldFormat, @@ -51,8 +51,101 @@ type AggResultsResponse = { key?: number } & { }; }; +interface AnomalyESQueryParams { + resultType: AnomalyResultType; + /** Appropriate score field for requested result type. */ + anomalyScoreField: string; + anomalyScoreThreshold: number; + jobIds: string[]; + topNBuckets: number; + maxBucketInSeconds: number; + lookBackTimeInterval: string; + includeInterimResults: boolean; + /** Source index from the datafeed. Required for retrieving field types for formatting results. */ + indexPattern: string; +} + const TIME_RANGE_PADDING = 10; +/** + * TODO Replace with URL generator when https://github.com/elastic/kibana/issues/59453 is resolved + */ +export function buildExplorerUrl( + jobIds: string[], + timeRange: { from: string; to: string; mode?: string }, + type: AnomalyResultType, + r?: AlertExecutionResult +): string { + const isInfluencerResult = type === ANOMALY_RESULT_TYPE.INFLUENCER; + + /** + * Disabled until Anomaly Explorer page is fixed and properly + * support single point time selection + */ + const highlightSwimLaneSelection = false; + + const globalState = { + ml: { + jobIds, + }, + time: { + from: timeRange.from, + to: timeRange.to, + mode: timeRange.mode ?? 'absolute', + }, + }; + + const appState = { + explorer: { + mlExplorerFilter: { + ...(r && isInfluencerResult + ? { + filterActive: true, + filteredFields: [ + r.topInfluencers![0].influencer_field_name, + r.topInfluencers![0].influencer_field_value, + ], + influencersFilterQuery: { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + [r.topInfluencers![0].influencer_field_name]: + r.topInfluencers![0].influencer_field_value, + }, + }, + ], + }, + }, + queryString: `${r.topInfluencers![0].influencer_field_name}:"${ + r.topInfluencers![0].influencer_field_value + }"`, + } + : {}), + }, + mlExplorerSwimlane: { + ...(r && highlightSwimLaneSelection + ? { + selectedLanes: [ + isInfluencerResult ? r.topInfluencers![0].influencer_field_value : 'Overall', + ], + selectedTimes: r.timestampEpoch, + selectedType: isInfluencerResult ? 'viewBy' : 'overall', + ...(isInfluencerResult + ? { viewByFieldName: r.topInfluencers![0].influencer_field_name } + : {}), + ...(isInfluencerResult ? {} : { showTopFieldValues: true }), + } + : {}), + }, + }, + }; + return `/app/ml/explorer/?_g=${encodeURIComponent( + rison.encode(globalState) + )}&_a=${encodeURIComponent(rison.encode(appState))}`; +} + /** * Mapping for result types and corresponding score fields. */ @@ -79,11 +172,11 @@ export function alertingServiceProvider( * Provides formatters based on the data view of the datafeed index pattern * and set of default formatters for fallback. */ - const getFormatters = memoize(async (datafeed: MlDatafeed) => { + const getFormatters = memoize(async (indexPattern: string) => { const fieldFormatsRegistry = await getFieldsFormatRegistry(); const numberFormatter = fieldFormatsRegistry.deserialize({ id: FIELD_FORMAT_IDS.NUMBER }); - const fieldFormatMap = await getFieldsFormatMap(datafeed.indices[0]); + const fieldFormatMap = await getFieldsFormatMap(indexPattern); const fieldFormatters = fieldFormatMap ? Object.entries(fieldFormatMap).reduce((acc, [fieldName, config]) => { @@ -313,8 +406,10 @@ export function alertingServiceProvider( return { count: aggTypeResults.doc_count, key: v.key, - message: - 'Alerts are raised based on real-time scores. Remember that scores may be adjusted over time as data continues to be analyzed.', + message: i18n.translate('xpack.ml.alertTypes.anomalyDetectionAlertingRule.alertMessage', { + defaultMessage: + 'Alerts are raised based on real-time scores. Remember that scores may be adjusted over time as data continues to be analyzed.', + }), alertInstanceKey, jobIds: [...new Set(requestedAnomalies.map((h) => h._source.job_id))], isInterim: requestedAnomalies.some((h) => h._source.is_interim), @@ -463,7 +558,7 @@ export function alertingServiceProvider( const resultsLabel = getAggResultsLabel(params.resultType); - const fieldsFormatters = await getFormatters(datafeeds![0]!); + const fieldsFormatters = await getFormatters(datafeeds![0]!.indices[0]); const formatter = getResultsFormatter( params.resultType, @@ -495,14 +590,14 @@ export function alertingServiceProvider( }; /** - * Fetches the most recent anomaly according the top N buckets within the lookback interval - * that satisfies a rule criteria. + * Gets ES query params for fetching anomalies. * - * @param params - Alert params + * @param params {MlAnomalyDetectionAlertParams} + * @return Params required for performing ES query for anomalies. */ - const fetchResult = async ( + const getQueryParams = async ( params: MlAnomalyDetectionAlertParams - ): Promise => { + ): Promise => { const jobAndGroupIds = [ ...(params.jobSelection.jobIds ?? []), ...(params.jobSelection.groupIds ?? []), @@ -534,6 +629,40 @@ export function alertingServiceProvider( const topNBuckets: number = params.topNBuckets ?? getTopNBuckets(jobsResponse[0]); + return { + jobIds, + topNBuckets, + maxBucketInSeconds, + lookBackTimeInterval, + anomalyScoreField: resultTypeScoreMapping[params.resultType], + includeInterimResults: params.includeInterim, + resultType: params.resultType, + indexPattern: datafeeds![0]!.indices[0], + anomalyScoreThreshold: params.severity, + }; + }; + + /** + * Fetches the most recent anomaly according the top N buckets within the lookback interval + * that satisfies a rule criteria. + * + * @param params - Alert params + */ + const fetchResult = async ( + params: AnomalyESQueryParams + ): Promise => { + const { + resultType, + jobIds, + maxBucketInSeconds, + topNBuckets, + lookBackTimeInterval, + anomalyScoreField, + includeInterimResults, + anomalyScoreThreshold, + indexPattern, + } = params; + const requestBody = { size: 0, query: { @@ -554,7 +683,7 @@ export function alertingServiceProvider( }, }, }, - ...(params.includeInterim + ...(includeInterimResults ? [] : [ { @@ -576,10 +705,10 @@ export function alertingServiceProvider( aggs: { max_score: { max: { - field: resultTypeScoreMapping[params.resultType], + field: anomalyScoreField, }, }, - ...getResultTypeAggRequest(params.resultType, params.severity), + ...getResultTypeAggRequest(resultType, anomalyScoreThreshold), truncate: { bucket_sort: { size: topNBuckets, @@ -622,113 +751,73 @@ export function alertingServiceProvider( prev.max_score.value > current.max_score.value ? prev : current ); - const formatters = await getFormatters(datafeeds![0]); + const formatters = await getFormatters(indexPattern); return getResultsFormatter(params.resultType, false, formatters)(topResult); }; - /** - * TODO Replace with URL generator when https://github.com/elastic/kibana/issues/59453 is resolved - * @param r - * @param type - */ - const buildExplorerUrl = (r: AlertExecutionResult, type: AnomalyResultType): string => { - const isInfluencerResult = type === ANOMALY_RESULT_TYPE.INFLUENCER; - - /** - * Disabled until Anomaly Explorer page is fixed and properly - * support single point time selection - */ - const highlightSwimLaneSelection = false; - - const globalState = { - ml: { - jobIds: r.jobIds, - }, - time: { - from: r.bucketRange.start, - to: r.bucketRange.end, - mode: 'absolute', - }, - }; - - const appState = { - explorer: { - mlExplorerFilter: { - ...(isInfluencerResult - ? { - filterActive: true, - filteredFields: [ - r.topInfluencers![0].influencer_field_name, - r.topInfluencers![0].influencer_field_value, - ], - influencersFilterQuery: { - bool: { - minimum_should_match: 1, - should: [ - { - match_phrase: { - [r.topInfluencers![0].influencer_field_name]: - r.topInfluencers![0].influencer_field_value, - }, - }, - ], - }, - }, - queryString: `${r.topInfluencers![0].influencer_field_name}:"${ - r.topInfluencers![0].influencer_field_value - }"`, - } - : {}), - }, - mlExplorerSwimlane: { - ...(highlightSwimLaneSelection - ? { - selectedLanes: [ - isInfluencerResult ? r.topInfluencers![0].influencer_field_value : 'Overall', - ], - selectedTimes: r.timestampEpoch, - selectedType: isInfluencerResult ? 'viewBy' : 'overall', - ...(isInfluencerResult - ? { viewByFieldName: r.topInfluencers![0].influencer_field_name } - : {}), - ...(isInfluencerResult ? {} : { showTopFieldValues: true }), - } - : {}), - }, - }, - }; - return `/app/ml/explorer/?_g=${encodeURIComponent( - rison.encode(globalState) - )}&_a=${encodeURIComponent(rison.encode(appState))}`; - }; - return { /** * Return the result of an alert condition execution. * * @param params - Alert params - * @param startedAt - * @param previousStartedAt */ execute: async ( - params: MlAnomalyDetectionAlertParams, - startedAt: Date, - previousStartedAt: Date | null - ): Promise => { - const result = await fetchResult(params); + params: MlAnomalyDetectionAlertParams + ): Promise< + { context: AnomalyDetectionAlertContext; name: string; isHealthy: boolean } | undefined + > => { + const queryParams = await getQueryParams(params); - if (!result) return; + if (!queryParams) { + return; + } - const anomalyExplorerUrl = buildExplorerUrl(result, params.resultType); + const result = await fetchResult(queryParams); - const executionResult = { - ...result, - name: result.alertInstanceKey, - anomalyExplorerUrl, - }; + if (result) { + const anomalyExplorerUrl = buildExplorerUrl( + result.jobIds, + { from: result.bucketRange.start, to: result.bucketRange.end }, + params.resultType, + result + ); - return executionResult; + const executionResult = { + ...result, + anomalyExplorerUrl, + }; + + return { context: executionResult, name: result.alertInstanceKey, isHealthy: false }; + } + + return { + name: '', + isHealthy: true, + context: { + anomalyExplorerUrl: buildExplorerUrl( + queryParams.jobIds, + { + from: `now-${queryParams.lookBackTimeInterval}`, + to: 'now', + mode: 'relative', + }, + queryParams.resultType + ), + jobIds: queryParams.jobIds, + message: i18n.translate( + 'xpack.ml.alertTypes.anomalyDetectionAlertingRule.recoveredMessage', + { + defaultMessage: + 'No anomalies have been found in the past {lookbackInterval} that exceed the severity threshold of {severity}.', + values: { + severity: queryParams.anomalyScoreThreshold, + lookbackInterval: queryParams.lookBackTimeInterval, + }, + } + ), + } as AnomalyDetectionAlertContext, + }; }, /** * Checks how often the alert condition will fire an alert instance diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts index f1ca545da947b..bfb9279e62d48 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts @@ -248,7 +248,16 @@ describe('JobsHealthService', () => { expect(logger.warn).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledWith(`Performing health checks for job IDs: test_job_01`); expect(datafeedsService.getDatafeedByJobId).not.toHaveBeenCalled(); - expect(executionResult).toEqual([]); + expect(executionResult).toEqual([ + { + context: { + message: 'No errors in the jobs messages.', + results: [], + }, + isHealthy: true, + name: 'Errors in job messages', + }, + ]); }); test('takes into account delayed data params', async () => { @@ -287,6 +296,7 @@ describe('JobsHealthService', () => { expect(executionResult).toEqual([ { + isHealthy: false, name: 'Data delay has occurred', context: { results: [ @@ -342,6 +352,7 @@ describe('JobsHealthService', () => { expect(executionResult).toEqual([ { + isHealthy: false, name: 'Datafeed is not started', context: { results: [ @@ -356,6 +367,7 @@ describe('JobsHealthService', () => { }, }, { + isHealthy: false, name: 'Model memory limit reached', context: { results: [ @@ -370,10 +382,11 @@ describe('JobsHealthService', () => { }, ], message: - 'Job test_job_01 reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.', + 'Job test_job_01 reached the hard model memory limit. Assign more memory to the job and restore it from a snapshot taken prior to reaching the hard limit.', }, }, { + isHealthy: false, name: 'Data delay has occurred', context: { results: [ @@ -395,6 +408,14 @@ describe('JobsHealthService', () => { message: 'Jobs test_job_01, test_job_02 are suffering from delayed data.', }, }, + { + isHealthy: true, + name: 'Errors in job messages', + context: { + message: 'No errors in the jobs messages.', + results: [], + }, + }, ]); }); }); diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts index bd2503c183c6c..4c19846794de1 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { groupBy, keyBy, memoize } from 'lodash'; +import { groupBy, keyBy, memoize, partition } from 'lodash'; import { KibanaRequest, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; import { MlJob } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; @@ -37,9 +37,13 @@ import { } from '../../models/job_audit_messages/job_audit_messages'; import type { FieldFormatsRegistryProvider } from '../../../common/types/kibana'; -interface TestResult { +export interface TestResult { name: string; context: AnomalyDetectionJobsHealthAlertContext; + /** + * Indicates if the health check is successful. + */ + isHealthy: boolean; } type TestsResults = TestResult[]; @@ -152,7 +156,7 @@ export function jobsHealthServiceProvider( * Gets not started datafeeds for opened jobs. * @param jobIds */ - async getNotStartedDatafeeds(jobIds: string[]): Promise { + async getDatafeedsReport(jobIds: string[]): Promise { const datafeeds = await getDatafeeds(jobIds); if (datafeeds) { @@ -176,13 +180,13 @@ export function jobsHealthServiceProvider( }; }) .filter((datafeedStat) => { - // Find opened jobs with not started datafeeds - return datafeedStat.job_state === 'opened' && datafeedStat.datafeed_state !== 'started'; + // Find opened jobs + return datafeedStat.job_state === 'opened'; }); } }, /** - * Gets jobs that reached soft or hard model memory limits. + * Gets the model memory report for opened jobs. * @param jobIds */ async getMmlReport(jobIds: string[]): Promise { @@ -191,7 +195,7 @@ export function jobsHealthServiceProvider( const { dateFormatter, bytesFormatter } = await getFormatters(); return jobsStats - .filter((j) => j.state === 'opened' && j.model_size_stats.memory_status !== 'ok') + .filter((j) => j.state === 'opened') .map(({ job_id: jobId, model_size_stats: modelSizeStats }) => { return { job_id: jobId, @@ -210,12 +214,14 @@ export function jobsHealthServiceProvider( * @param jobs * @param timeInterval - Custom time interval provided by the user. * @param docsCount - The threshold for a number of missing documents to alert upon. + * + * @return {Promise<[DelayedDataResponse[], DelayedDataResponse[]]>} - Collections of annotations exceeded and not exceeded the docs threshold. */ async getDelayedDataReport( jobs: MlJob[], timeInterval: string | null, docsCount: number | null - ): Promise { + ): Promise<[DelayedDataResponse[], DelayedDataResponse[]]> { const jobIds = getJobIds(jobs); const datafeeds = await getDatafeeds(jobIds); @@ -231,13 +237,14 @@ export function jobsHealthServiceProvider( const { dateFormatter } = await getFormatters(); - return ( + const annotationsData = ( await annotationService.getDelayedDataAnnotations({ jobIds: resultJobIds, earliestMs, }) ) .map((v) => { + // TODO Update when https://github.com/elastic/elasticsearch/issues/76088 is resolved. const match = v.annotation.match(/Datafeed has missed (\d+)\s/); const missedDocsCount = match ? parseInt(match[1], 10) : 0; return { @@ -255,14 +262,12 @@ export function jobsHealthServiceProvider( const job = jobsMap[v.job_id]; const datafeed = datafeedsMap[v.job_id]; - const isDocCountExceededThreshold = docsCount ? v.missed_docs_count >= docsCount : true; - const jobLookbackInterval = resolveLookbackInterval([job], [datafeed]); const isEndTimestampWithinRange = v.end_timestamp > getDelayedDataLookbackTimestamp(timeInterval, jobLookbackInterval); - return isDocCountExceededThreshold && isEndTimestampWithinRange; + return isEndTimestampWithinRange; }) .map((v) => { return { @@ -270,6 +275,11 @@ export function jobsHealthServiceProvider( end_timestamp: dateFormatter(v.end_timestamp), }; }); + + return partition(annotationsData, (v) => { + const isDocCountExceededThreshold = docsCount ? v.missed_docs_count >= docsCount : true; + return isDocCountExceededThreshold; + }); }, /** * Retrieves a list of the latest errors per jobs. @@ -322,21 +332,39 @@ export function jobsHealthServiceProvider( logger.debug(`Performing health checks for job IDs: ${jobIds.join(', ')}`); if (config.datafeed.enabled) { - const response = await this.getNotStartedDatafeeds(jobIds); + const response = await this.getDatafeedsReport(jobIds); if (response && response.length > 0) { - const { count, jobsString } = getJobsAlertingMessageValues(response); + const [startedDatafeeds, notStartedDatafeeds] = partition( + response, + (datafeedStat) => datafeedStat.datafeed_state === 'started' + ); + + const isHealthy = notStartedDatafeeds.length === 0; + const datafeedResults = isHealthy ? startedDatafeeds : notStartedDatafeeds; + const { count, jobsString } = getJobsAlertingMessageValues(datafeedResults); + results.push({ + isHealthy, name: HEALTH_CHECK_NAMES.datafeed.name, context: { - results: response, - message: i18n.translate( - 'xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedStateMessage', - { - defaultMessage: - 'Datafeed is not started for {count, plural, one {job} other {jobs}} {jobsString}', - values: { count, jobsString }, - } - ), + results: datafeedResults, + message: isHealthy + ? i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedRecoveryMessage', + { + defaultMessage: + 'Datafeed is started for {count, plural, one {job} other {jobs}} {jobsString}', + values: { count, jobsString }, + } + ) + : i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedStateMessage', + { + defaultMessage: + 'Datafeed is not started for {count, plural, one {job} other {jobs}} {jobsString}', + values: { count, jobsString }, + } + ), }, }); } @@ -345,49 +373,62 @@ export function jobsHealthServiceProvider( if (config.mml.enabled) { const response = await this.getMmlReport(jobIds); if (response && response.length > 0) { - const { hard_limit: hardLimitJobs, soft_limit: softLimitJobs } = groupBy( - response, - 'memory_status' - ); + const { + hard_limit: hardLimitJobs, + soft_limit: softLimitJobs, + ok: okJobs, + } = groupBy(response, 'memory_status'); - const { count: hardLimitCount, jobsString: hardLimitJobsString } = - getJobsAlertingMessageValues(hardLimitJobs); - const { count: softLimitCount, jobsString: softLimitJobsString } = - getJobsAlertingMessageValues(softLimitJobs); + const isHealthy = !hardLimitJobs?.length && !softLimitJobs?.length; let message = ''; - if (hardLimitCount > 0) { - message = i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.mmlMessage', { - defaultMessage: `{count, plural, one {Job} other {Jobs}} {jobsString} reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.`, - values: { - count: hardLimitCount, - jobsString: hardLimitJobsString, - }, - }); - } - - if (softLimitCount > 0) { - if (message.length > 0) { - message += '\n'; - } - message += i18n.translate( - 'xpack.ml.alertTypes.jobsHealthAlertingRule.mmlSoftLimitMessage', + if (isHealthy) { + message = i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.mmlRecoveredMessage', { - defaultMessage: - '{count, plural, one {Job} other {Jobs}} {jobsString} reached the soft model memory limit. Assign the job more memory or edit the datafeed filter to limit scope of analysis.', + defaultMessage: `All jobs are running within configured model memory limits.`, + } + ); + } else { + const { count: hardLimitCount, jobsString: hardLimitJobsString } = + getJobsAlertingMessageValues(hardLimitJobs); + const { count: softLimitCount, jobsString: softLimitJobsString } = + getJobsAlertingMessageValues(softLimitJobs); + + if (hardLimitCount > 0) { + message = i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.mmlMessage', { + defaultMessage: `{count, plural, one {Job} other {Jobs}} {jobsString} reached the hard model memory limit. Assign more memory to the job and restore it from a snapshot taken prior to reaching the hard limit.`, values: { - count: softLimitCount, - jobsString: softLimitJobsString, + count: hardLimitCount, + jobsString: hardLimitJobsString, }, + }); + } + + if (softLimitCount > 0) { + if (message.length > 0) { + message += '\n'; } - ); + message += i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.mmlSoftLimitMessage', + { + defaultMessage: + '{count, plural, one {Job} other {Jobs}} {jobsString} reached the soft model memory limit. Assign more memory to the job or edit the datafeed filter to limit the scope of analysis.', + values: { + count: softLimitCount, + jobsString: softLimitJobsString, + }, + } + ); + } } results.push({ + isHealthy, name: HEALTH_CHECK_NAMES.mml.name, context: { - results: response, + results: isHealthy ? okJobs : [...(hardLimitJobs ?? []), ...(softLimitJobs ?? [])], message, }, }); @@ -395,51 +436,63 @@ export function jobsHealthServiceProvider( } if (config.delayedData.enabled) { - const response = await this.getDelayedDataReport( - jobs, - config.delayedData.timeInterval, - config.delayedData.docsCount - ); - - const { count, jobsString } = getJobsAlertingMessageValues(response); + const [exceededThresholdAnnotations, withinThresholdAnnotations] = + await this.getDelayedDataReport( + jobs, + config.delayedData.timeInterval, + config.delayedData.docsCount + ); - if (response.length > 0) { - results.push({ - name: HEALTH_CHECK_NAMES.delayedData.name, - context: { - results: response, - message: i18n.translate( - 'xpack.ml.alertTypes.jobsHealthAlertingRule.delayedDataMessage', - { + const isHealthy = exceededThresholdAnnotations.length === 0; + const { count, jobsString } = getJobsAlertingMessageValues(exceededThresholdAnnotations); + + results.push({ + isHealthy, + name: HEALTH_CHECK_NAMES.delayedData.name, + context: { + results: isHealthy ? withinThresholdAnnotations : exceededThresholdAnnotations, + message: isHealthy + ? i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.delayedDataRecoveryMessage', + { + defaultMessage: 'No data delay has occurred.', + } + ) + : i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.delayedDataMessage', { defaultMessage: '{count, plural, one {Job} other {Jobs}} {jobsString} {count, plural, one {is} other {are}} suffering from delayed data.', values: { count, jobsString }, - } - ), - }, - }); - } + }), + }, + }); } if (config.errorMessages.enabled && previousStartedAt) { const response = await this.getErrorsReport(jobIds, previousStartedAt); - if (response.length > 0) { - const { count, jobsString } = getJobsAlertingMessageValues(response); - results.push({ - name: HEALTH_CHECK_NAMES.errorMessages.name, - context: { - results: response, - message: i18n.translate( - 'xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesMessage', - { + const { count, jobsString } = getJobsAlertingMessageValues(response); + const isHealthy = response.length === 0; + + results.push({ + isHealthy, + name: HEALTH_CHECK_NAMES.errorMessages.name, + context: { + results: response, + message: isHealthy + ? i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesRecoveredMessage', + { + defaultMessage: + 'No errors in the {count, plural, one {job} other {jobs}} messages.', + values: { count }, + } + ) + : i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesMessage', { defaultMessage: '{count, plural, one {Job} other {Jobs}} {jobsString} {count, plural, one {contains} other {contain}} errors in the messages.', values: { count, jobsString }, - } - ), - }, - }); - } + }), + }, + }); } return results; diff --git a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts index e34f32d603b2a..7aa81e7668c0f 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts @@ -23,17 +23,24 @@ import { import { RegisterAlertParams } from './register_ml_alerts'; import { InfluencerAnomalyAlertDoc, RecordAnomalyAlertDoc } from '../../../common/types/alerts'; -export type AnomalyDetectionAlertContext = { - name: string; +/** + * Base Anomaly detection alerting rule context. + * Relevant for both active and recovered alerts. + */ +export type AnomalyDetectionAlertBaseContext = AlertInstanceContext & { jobIds: string[]; + anomalyExplorerUrl: string; + message: string; +}; + +export type AnomalyDetectionAlertContext = AnomalyDetectionAlertBaseContext & { timestampIso8601: string; timestamp: number; score: number; isInterim: boolean; topRecords: RecordAnomalyAlertDoc[]; topInfluencers?: InfluencerAnomalyAlertDoc[]; - anomalyExplorerUrl: string; -} & AlertInstanceContext; +}; export const ANOMALY_SCORE_MATCH_GROUP_ID = 'anomaly_score_match'; @@ -129,18 +136,27 @@ export function registerAnomalyDetectionAlertType({ producer: PLUGIN_ID, minimumLicenseRequired: MINIMUM_FULL_LICENSE, isExportable: true, - async executor({ services, params, alertId, state, previousStartedAt, startedAt }) { + doesSetRecoveryContext: true, + async executor({ services, params, alertId, state, previousStartedAt, startedAt, name }) { const fakeRequest = {} as KibanaRequest; const { execute } = mlSharedServices.alertingServiceProvider( services.savedObjectsClient, fakeRequest ); - const executionResult = await execute(params, startedAt, previousStartedAt); + const executionResult = await execute(params); - if (executionResult) { + if (executionResult && !executionResult.isHealthy) { const alertInstanceName = executionResult.name; const alertInstance = services.alertFactory.create(alertInstanceName); - alertInstance.scheduleActions(ANOMALY_SCORE_MATCH_GROUP_ID, executionResult); + alertInstance.scheduleActions(ANOMALY_SCORE_MATCH_GROUP_ID, executionResult.context); + } + + // Set context for recovered alerts + const { getRecoveredAlerts } = services.alertFactory.done(); + for (const recoveredAlert of getRecoveredAlerts()) { + if (!!executionResult?.isHealthy) { + recoveredAlert.setContext(executionResult.context); + } } }, }); diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts index 1414262969153..7ea087e1239a6 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts @@ -140,6 +140,7 @@ export function registerJobsMonitoringRuleType({ producer: PLUGIN_ID, minimumLicenseRequired: MINIMUM_FULL_LICENSE, isExportable: true, + doesSetRecoveryContext: true, async executor(options) { const { services, name } = options; @@ -151,18 +152,30 @@ export function registerJobsMonitoringRuleType({ ); const executionResult = await getTestsResults(options); - if (executionResult.length > 0) { + const unhealthyTests = executionResult.filter(({ isHealthy }) => !isHealthy); + + if (unhealthyTests.length > 0) { logger.debug( - `"${name}" rule is scheduling actions for tests: ${executionResult + `"${name}" rule is scheduling actions for tests: ${unhealthyTests .map((v) => v.name) .join(', ')}` ); - executionResult.forEach(({ name: alertInstanceName, context }) => { + unhealthyTests.forEach(({ name: alertInstanceName, context }) => { const alertInstance = services.alertFactory.create(alertInstanceName); alertInstance.scheduleActions(ANOMALY_DETECTION_JOB_REALTIME_ISSUE, context); }); } + + // Set context for recovered alerts + const { getRecoveredAlerts } = services.alertFactory.done(); + for (const recoveredAlert of getRecoveredAlerts()) { + const recoveredAlertId = recoveredAlert.getId(); + const testResult = executionResult.find((v) => v.name === recoveredAlertId); + if (testResult) { + recoveredAlert.setContext(testResult.context); + } + } }, }); } From f9b065e22858a17fefd67c0e4e8e99ed799607b1 Mon Sep 17 00:00:00 2001 From: Pius Date: Mon, 23 May 2022 10:07:56 -0700 Subject: [PATCH 099/120] Cross reference audit log settings (#132359) * Cross reference audit log settings * Update docs/user/security/audit-logging.asciidoc Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> --- docs/user/security/audit-logging.asciidoc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index caa6512955f67..8cd293654c29c 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -15,8 +15,10 @@ by cluster-wide privileges. For more information on enabling audit logging in [NOTE] ============================================================================ Audit logs are **disabled** by default. To enable this functionality, you must -set `xpack.security.audit.enabled` to `true` in `kibana.yml`, and optionally configure -an <> to write the audit log to a location of your choosing. +set `xpack.security.audit.enabled` to `true` in `kibana.yml`. + +You can optionally configure audit logs location, file/rolling file appenders and +ignore filters using <>. ============================================================================ [[xpack-security-ecs-audit-logging]] From 65054ae3b0d4fb713084d85df11e26c37034648c Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Mon, 23 May 2022 18:15:01 +0100 Subject: [PATCH 100/120] Endpoint Timeline telemetry (#132626) * Staging branch. Update. Fix up timeline task types + received logic. switch between internal / current user based on the context of the call. Send to telemetry endpoint. * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Add test for the entire task. * This code shouldn't have been deleted. * Return 100 every 3 hours. * Don't be explicit about types for test. Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../server/endpoint/routes/resolver.ts | 2 +- .../server/endpoint/routes/resolver/entity.ts | 136 - .../routes/resolver/entity/handler.ts | 49 + .../entity/utils/build_resolver_entity.ts | 37 + .../entity/utils/supported_schemas.ts | 75 + .../server/endpoint/routes/resolver/events.ts | 1 - .../resolver/tree/queries/descendants.ts | 11 +- .../routes/resolver/tree/queries/lifecycle.ts | 9 +- .../routes/resolver/tree/queries/stats.ts | 10 +- .../routes/resolver/tree/utils/fetch.ts | 27 +- .../server/lib/telemetry/__mocks__/index.ts | 43 +- .../lib/telemetry/__mocks__/timeline.ts | 2582 +++++++++++++++++ .../server/lib/telemetry/constants.ts | 2 + .../server/lib/telemetry/receiver.ts | 159 +- .../server/lib/telemetry/tasks/index.ts | 2 + .../lib/telemetry/tasks/timelines.test.ts | 40 + .../server/lib/telemetry/tasks/timelines.ts | 149 + .../server/lib/telemetry/types.ts | 18 + 18 files changed, 3195 insertions(+), 157 deletions(-) delete mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/handler.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/build_resolver_entity.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/supported_schemas.ts create mode 100644 x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/timeline.ts create mode 100644 x-pack/plugins/security_solution/server/lib/telemetry/tasks/timelines.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/telemetry/tasks/timelines.ts diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts index 2e50b54f9db70..76336e33cb522 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts @@ -13,7 +13,7 @@ import { } from '../../../common/endpoint/schema/resolver'; import { handleTree } from './resolver/tree/handler'; -import { handleEntities } from './resolver/entity'; +import { handleEntities } from './resolver/entity/handler'; import { handleEvents } from './resolver/events'; export function registerResolverRoutes(router: IRouter) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts deleted file mode 100644 index ef438667c3647..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import _ from 'lodash'; -import { RequestHandler } from '@kbn/core/server'; -import { TypeOf } from '@kbn/config-schema'; -import { validateEntities } from '../../../../common/endpoint/schema/resolver'; -import { ResolverEntityIndex, ResolverSchema } from '../../../../common/endpoint/types'; - -interface SupportedSchema { - /** - * A name for the schema being used - */ - name: string; - - /** - * A constraint to search for in the documented returned by Elasticsearch - */ - constraints: Array<{ field: string; value: string }>; - - /** - * Schema to return to the frontend so that it can be passed in to call to the /tree API - */ - schema: ResolverSchema; -} - -/** - * This structure defines the preset supported schemas for a resolver graph. We'll probably want convert this - * implementation to something similar to how row renderers is implemented. - */ -const supportedSchemas: SupportedSchema[] = [ - { - name: 'endpoint', - constraints: [ - { - field: 'agent.type', - value: 'endpoint', - }, - ], - schema: { - id: 'process.entity_id', - parent: 'process.parent.entity_id', - ancestry: 'process.Ext.ancestry', - name: 'process.name', - }, - }, - { - name: 'winlogbeat', - constraints: [ - { - field: 'agent.type', - value: 'winlogbeat', - }, - { - field: 'event.module', - value: 'sysmon', - }, - ], - schema: { - id: 'process.entity_id', - parent: 'process.parent.entity_id', - name: 'process.name', - }, - }, -]; - -function getFieldAsString(doc: unknown, field: string): string | undefined { - const value = _.get(doc, field); - if (value === undefined) { - return undefined; - } - - return String(value); -} - -/** - * This is used to get an 'entity_id' which is an internal-to-Resolver concept, from an `_id`, which - * is the artificial ID generated by ES for each document. - */ -export function handleEntities(): RequestHandler> { - return async (context, request, response) => { - const { - query: { _id, indices }, - } = request; - - const esClient = (await context.core).elasticsearch.client; - const queryResponse = await esClient.asCurrentUser.search({ - ignore_unavailable: true, - index: indices, - body: { - // only return 1 match at most - size: 1, - query: { - bool: { - filter: [ - { - // only return documents with the matching _id - ids: { - values: _id, - }, - }, - ], - }, - }, - }, - }); - - const responseBody: ResolverEntityIndex = []; - for (const hit of queryResponse.hits.hits) { - for (const supportedSchema of supportedSchemas) { - let foundSchema = true; - // check that the constraint and id fields are defined and that the id field is not an empty string - const id = getFieldAsString(hit._source, supportedSchema.schema.id); - for (const constraint of supportedSchema.constraints) { - const fieldValue = getFieldAsString(hit._source, constraint.field); - // track that all the constraints are true, if one of them is false then this schema is not valid so mark it - // that we did not find the schema - foundSchema = foundSchema && fieldValue?.toLowerCase() === constraint.value.toLowerCase(); - } - - if (foundSchema && id !== undefined && id !== '') { - responseBody.push({ - name: supportedSchema.name, - schema: supportedSchema.schema, - id, - }); - } - } - } - return response.ok({ body: responseBody }); - }; -} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/handler.ts new file mode 100644 index 0000000000000..2552cf4754c90 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/handler.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RequestHandler } from '@kbn/core/server'; +import { TypeOf } from '@kbn/config-schema'; +import { validateEntities } from '../../../../../common/endpoint/schema/resolver'; +import { ResolverEntityIndex } from '../../../../../common/endpoint/types'; +import { resolverEntity } from './utils/build_resolver_entity'; + +/** + * This is used to get an 'entity_id' which is an internal-to-Resolver concept, from an `_id`, which + * is the artificial ID generated by ES for each document. + */ +export function handleEntities(): RequestHandler> { + return async (context, request, response) => { + const { + query: { _id, indices }, + } = request; + + const esClient = (await context.core).elasticsearch.client; + const queryResponse = await esClient.asCurrentUser.search({ + ignore_unavailable: true, + index: indices, + body: { + // only return 1 match at most + size: 1, + query: { + bool: { + filter: [ + { + // only return documents with the matching _id + ids: { + values: _id, + }, + }, + ], + }, + }, + }, + }); + + const responseBody: ResolverEntityIndex = resolverEntity(queryResponse.hits.hits); + return response.ok({ body: responseBody }); + }; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/build_resolver_entity.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/build_resolver_entity.ts new file mode 100644 index 0000000000000..3c554f1602591 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/build_resolver_entity.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { getFieldAsString, supportedSchemas } from './supported_schemas'; +import { ResolverEntityIndex } from '../../../../../../common/endpoint/types'; + +export function resolverEntity(hits: Array>) { + const responseBody: ResolverEntityIndex = []; + for (const hit of hits) { + for (const supportedSchema of supportedSchemas) { + let foundSchema = true; + // check that the constraint and id fields are defined and that the id field is not an empty string + const id = getFieldAsString(hit._source, supportedSchema.schema.id); + for (const constraint of supportedSchema.constraints) { + const fieldValue = getFieldAsString(hit._source, constraint.field); + // track that all the constraints are true, if one of them is false then this schema is not valid so mark it + // that we did not find the schema + foundSchema = foundSchema && fieldValue?.toLowerCase() === constraint.value.toLowerCase(); + } + + if (foundSchema && id !== undefined && id !== '') { + responseBody.push({ + name: supportedSchema.name, + schema: supportedSchema.schema, + id, + }); + } + } + } + + return responseBody; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/supported_schemas.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/supported_schemas.ts new file mode 100644 index 0000000000000..9c40b2b5024e6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/supported_schemas.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import _ from 'lodash'; +import { ResolverSchema } from '../../../../../../common/endpoint/types'; + +interface SupportedSchema { + /** + * A name for the schema being used + */ + name: string; + + /** + * A constraint to search for in the documented returned by Elasticsearch + */ + constraints: Array<{ field: string; value: string }>; + + /** + * Schema to return to the frontend so that it can be passed in to call to the /tree API + */ + schema: ResolverSchema; +} + +/** + * This structure defines the preset supported schemas for a resolver graph. We'll probably want convert this + * implementation to something similar to how row renderers is implemented. + */ +export const supportedSchemas: SupportedSchema[] = [ + { + name: 'endpoint', + constraints: [ + { + field: 'agent.type', + value: 'endpoint', + }, + ], + schema: { + id: 'process.entity_id', + parent: 'process.parent.entity_id', + ancestry: 'process.Ext.ancestry', + name: 'process.name', + }, + }, + { + name: 'winlogbeat', + constraints: [ + { + field: 'agent.type', + value: 'winlogbeat', + }, + { + field: 'event.module', + value: 'sysmon', + }, + ], + schema: { + id: 'process.entity_id', + parent: 'process.parent.entity_id', + name: 'process.name', + }, + }, +]; + +export function getFieldAsString(doc: unknown, field: string): string | undefined { + const value = _.get(doc, field); + if (value === undefined) { + return undefined; + } + + return String(value); +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts index 2acc9e6aeb114..9734c89900960 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts @@ -46,7 +46,6 @@ export function handleEvents(): RequestHandler< timeRange: body.timeRange, }); const results = await query.search(client, body.filter); - return res.ok({ body: createEvents(results, PaginationBuilder.buildCursorRequestLimit(limit, results)), }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts index 09da7f87bf6c8..d4206dc853a3c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts @@ -15,6 +15,7 @@ interface DescendantsParams { schema: ResolverSchema; indexPatterns: string | string[]; timeRange: TimeRange; + isInternalRequest: boolean; } /** @@ -25,12 +26,14 @@ export class DescendantsQuery { private readonly indexPatterns: string | string[]; private readonly timeRange: TimeRange; private readonly docValueFields: JsonValue[]; + private readonly isInternalRequest: boolean; - constructor({ schema, indexPatterns, timeRange }: DescendantsParams) { + constructor({ schema, indexPatterns, timeRange, isInternalRequest }: DescendantsParams) { this.docValueFields = docValueFields(schema); this.schema = schema; this.indexPatterns = indexPatterns; this.timeRange = timeRange; + this.isInternalRequest = isInternalRequest; } private query(nodes: NodeID[], size: number): JsonObject { @@ -198,14 +201,16 @@ export class DescendantsQuery { return []; } + const esClient = this.isInternalRequest ? client.asInternalUser : client.asCurrentUser; + let response: estypes.SearchResponse; if (this.schema.ancestry) { - response = await client.asCurrentUser.search({ + response = await esClient.search({ body: this.queryWithAncestryArray(validNodes, this.schema.ancestry, limit), index: this.indexPatterns, }); } else { - response = await client.asCurrentUser.search({ + response = await esClient.search({ body: this.query(validNodes, limit), index: this.indexPatterns, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts index 88b2e842ce8cb..be5a34092e0c5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts @@ -14,6 +14,7 @@ interface LifecycleParams { schema: ResolverSchema; indexPatterns: string | string[]; timeRange: TimeRange; + isInternalRequest: boolean; } /** @@ -24,11 +25,13 @@ export class LifecycleQuery { private readonly indexPatterns: string | string[]; private readonly timeRange: TimeRange; private readonly docValueFields: JsonValue[]; - constructor({ schema, indexPatterns, timeRange }: LifecycleParams) { + private readonly isInternalRequest: boolean; + constructor({ schema, indexPatterns, timeRange, isInternalRequest }: LifecycleParams) { this.docValueFields = docValueFields(schema); this.schema = schema; this.indexPatterns = indexPatterns; this.timeRange = timeRange; + this.isInternalRequest = isInternalRequest; } private query(nodes: NodeID[]): JsonObject { @@ -91,7 +94,9 @@ export class LifecycleQuery { return []; } - const body = await client.asCurrentUser.search({ + const esClient = this.isInternalRequest ? client.asInternalUser : client.asCurrentUser; + + const body = await esClient.search({ body: this.query(validNodes), index: this.indexPatterns, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts index b87b121e5769f..b4143345d9db4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts @@ -29,6 +29,7 @@ interface StatsParams { schema: ResolverSchema; indexPatterns: string | string[]; timeRange: TimeRange; + isInternalRequest: boolean; } /** @@ -38,10 +39,13 @@ export class StatsQuery { private readonly schema: ResolverSchema; private readonly indexPatterns: string | string[]; private readonly timeRange: TimeRange; - constructor({ schema, indexPatterns, timeRange }: StatsParams) { + private readonly isInternalRequest: boolean; + + constructor({ schema, indexPatterns, timeRange, isInternalRequest }: StatsParams) { this.schema = schema; this.indexPatterns = indexPatterns; this.timeRange = timeRange; + this.isInternalRequest = isInternalRequest; } private query(nodes: NodeID[]): JsonObject { @@ -122,8 +126,10 @@ export class StatsQuery { return {}; } + const esClient = this.isInternalRequest ? client.asInternalUser : client.asCurrentUser; + // leaving unknown here because we don't actually need the hits part of the body - const body = await client.asCurrentUser.search({ + const body = await esClient.search({ body: this.query(nodes), index: this.indexPatterns, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.ts index f7d4b7d97efdb..add798f861c96 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.ts @@ -49,10 +49,13 @@ export class Fetcher { * * @param options the options for retrieving the structure of the tree. */ - public async tree(options: TreeOptions): Promise { + public async tree( + options: TreeOptions, + isInternalRequest: boolean = false + ): Promise { const treeParts = await Promise.all([ - this.retrieveAncestors(options), - this.retrieveDescendants(options), + this.retrieveAncestors(options, isInternalRequest), + this.retrieveDescendants(options, isInternalRequest), ]); const tree = treeParts.reduce((results, partArray) => { @@ -60,12 +63,13 @@ export class Fetcher { return results; }, []); - return this.formatResponse(tree, options); + return this.formatResponse(tree, options, isInternalRequest); } private async formatResponse( treeNodes: FieldsObject[], - options: TreeOptions + options: TreeOptions, + isInternalRequest: boolean ): Promise { const statsIDs: NodeID[] = []; for (const node of treeNodes) { @@ -79,6 +83,7 @@ export class Fetcher { indexPatterns: options.indexPatterns, schema: options.schema, timeRange: options.timeRange, + isInternalRequest, }); const eventStats = await query.search(this.client, statsIDs); @@ -133,12 +138,16 @@ export class Fetcher { return nodes; } - private async retrieveAncestors(options: TreeOptions): Promise { + private async retrieveAncestors( + options: TreeOptions, + isInternalRequest: boolean + ): Promise { const ancestors: FieldsObject[] = []; const query = new LifecycleQuery({ schema: options.schema, indexPatterns: options.indexPatterns, timeRange: options.timeRange, + isInternalRequest, }); let nodes = options.nodes; @@ -179,12 +188,16 @@ export class Fetcher { return ancestors; } - private async retrieveDescendants(options: TreeOptions): Promise { + private async retrieveDescendants( + options: TreeOptions, + isInternalRequest: boolean + ): Promise { const descendants: FieldsObject[] = []; const query = new DescendantsQuery({ schema: options.schema, indexPatterns: options.indexPatterns, timeRange: options.timeRange, + isInternalRequest, }); let nodes: NodeID[] = options.nodes; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts index b28a5213b4326..d91f08f15cedf 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts @@ -5,11 +5,13 @@ * 2.0. */ +import moment from 'moment'; import { ConcreteTaskInstance, TaskStatus } from '@kbn/task-manager-plugin/server'; import { TelemetryEventsSender } from '../sender'; import { TelemetryReceiver } from '../receiver'; import { SecurityTelemetryTaskConfig } from '../task'; import { PackagePolicy } from '@kbn/fleet-plugin/common/types/models/package_policy'; +import { stubEndpointAlertResponse, stubProcessTree, stubFetchTimelineEvents } from './timeline'; export const createMockTelemetryEventsSender = ( enableTelemetry?: boolean @@ -29,13 +31,45 @@ export const createMockTelemetryEventsSender = ( } as unknown as jest.Mocked; }; +const stubClusterInfo = { + name: 'Stub-MacBook-Pro.local', + cluster_name: 'elasticsearch', + cluster_uuid: '5Pr5PXRQQpGJUTn0czAvKQ', + version: { + number: '8.0.0-SNAPSHOT', + build_type: 'tar', + build_hash: '38537ab4a726b42ce8f034aad78d8fca4d4f3e51', + build_date: moment().toISOString(), + build_snapshot: true, + lucene_version: '9.2.0', + minimum_wire_compatibility_version: '7.17.0', + minimum_index_compatibility_version: '7.0.0', + }, + tagline: 'You Know, for Search', +}; + +const stubLicenseInfo = { + status: 'active', + uid: '4a7dde08-e5f8-4e50-80f8-bc85b72b4934', + type: 'trial', + issue_date: moment().toISOString(), + issue_date_in_millis: 1653299879146, + expiry_date: moment().toISOString(), + expiry_date_in_millis: 1655891879146, + max_nodes: 1000, + max_resource_units: null, + issued_to: 'elasticsearch', + issuer: 'elasticsearch', + start_date_in_millis: -1, +}; + export const createMockTelemetryReceiver = ( diagnosticsAlert?: unknown ): jest.Mocked => { return { start: jest.fn(), - fetchClusterInfo: jest.fn(), - fetchLicenseInfo: jest.fn(), + fetchClusterInfo: jest.fn().mockReturnValue(stubClusterInfo), + fetchLicenseInfo: jest.fn().mockReturnValue(stubLicenseInfo), copyLicenseFields: jest.fn(), fetchFleetAgents: jest.fn(), fetchDiagnosticAlerts: jest.fn().mockReturnValue(diagnosticsAlert ?? jest.fn()), @@ -45,6 +79,11 @@ export const createMockTelemetryReceiver = ( fetchEndpointList: jest.fn(), fetchDetectionRules: jest.fn().mockReturnValue({ body: null }), fetchEndpointMetadata: jest.fn(), + fetchTimelineEndpointAlerts: jest + .fn() + .mockReturnValue(Promise.resolve(stubEndpointAlertResponse())), + buildProcessTree: jest.fn().mockReturnValue(Promise.resolve(stubProcessTree())), + fetchTimelineEvents: jest.fn().mockReturnValue(Promise.resolve(stubFetchTimelineEvents())), } as unknown as jest.Mocked; }; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/timeline.ts b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/timeline.ts new file mode 100644 index 0000000000000..2cb1fddaa47cc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/timeline.ts @@ -0,0 +1,2582 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import type { ResolverNode } from '../../../../common/endpoint/types'; + +export const stubEndpointAlertResponse = () => { + return { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 47, + relation: 'eq', + }, + max_score: 0, + hits: [ + { + _index: '.internal.alerts-security.alerts-default-000001', + _id: '2f4c790211998ec3369f581b778e9761ae5647d041edd7b1245f7311fba06f37', + _score: 0, + _source: { + 'kibana.alert.rule.category': 'Custom Query Rule', + 'kibana.alert.rule.consumer': 'siem', + 'kibana.alert.rule.execution.uuid': 'c92c1a91-9981-4948-8dee-39b263d81f05', + 'kibana.alert.rule.name': 'Endpoint Security', + 'kibana.alert.rule.producer': 'siem', + 'kibana.alert.rule.rule_type_id': 'siem.queryRule', + 'kibana.alert.rule.uuid': 'b35e3af8-da87-11ec-ad90-353e53c6bd3e', + 'kibana.space_ids': ['default'], + 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], + '@timestamp': moment.now(), + registry: { + path: 'HKEY_USERS\\S-1-5-21-2460036010-3910878774-3458087990-1001\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\chrome', + data: { + strings: 'C:/fake_behavior/explorer.exe', + }, + value: 'explorer.exe', + }, + agent: { + id: 'd2529c31-5415-492a-9c9b-87a77e8874d5', + type: 'endpoint', + version: '7.0.1', + }, + process: { + Ext: { + ancestry: ['j0mdzksneq', 'up4f1f87wr'], + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + user: 'SYSTEM', + token: { + integrity_level_name: 'high', + elevation_level: 'full', + }, + }, + parent: { + pid: 1, + entity_id: 'j0mdzksneq', + }, + group_leader: { + name: 'fake leader', + pid: 112, + entity_id: '3po060bfqd', + }, + session_leader: { + name: 'fake session', + pid: 7, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft Windows', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 139, + entity_id: '3po060bfqd', + }, + name: 'explorer.exe', + pid: 2, + entity_id: 'p1dbx787xe', + executable: 'C:/fake_behavior/explorer.exe', + }, + dll: [ + { + Ext: { + compile_time: 1534424710, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + }, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + code_signature: { + trusted: true, + subject_name: 'Cybereason Inc', + }, + pe: { + architecture: 'x64', + }, + hash: { + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + md5: '1f2d082566b0fc5f2c238a5180db7451', + }, + }, + ], + destination: { + port: 443, + ip: '10.39.10.58', + }, + rule: { + description: 'Behavior rule description', + id: 'e2d719cc-7044-4a46-b2ee-0a2993202096', + }, + source: { + port: 59406, + ip: '10.199.40.10', + }, + network: { + transport: 'tcp', + type: 'ipv4', + direction: 'outgoing', + }, + file: { + path: 'C:/fake_behavior.exe', + name: 'fake_behavior.exe', + }, + ecs: { + version: '1.6.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.alerts', + }, + elastic: { + agent: { + id: 'd2529c31-5415-492a-9c9b-87a77e8874d5', + }, + }, + host: { + hostname: 'Host-uu8vmc2z8a', + os: { + Ext: { + variant: 'Windows Server', + }, + name: 'Windows', + family: 'windows', + version: '10.0', + platform: 'Windows', + full: 'Windows Server 2016', + }, + ip: ['10.23.178.108'], + name: 'Host-uu8vmc2z8a', + id: 'c1e90e16-0130-46d4-88de-ee338f13fed7', + mac: ['ee-83-79-cf-1a-13', 'a7-79-da-62-9e-78'], + architecture: 'a4rwx2t7yu', + }, + 'event.agent_id_status': 'auth_metadata_missing', + 'event.sequence': 15, + 'event.ingested': '2022-05-23T11:02:53Z', + 'event.code': 'behavior', + 'event.kind': 'signal', + 'event.module': 'endpoint', + 'event.action': 'rule_detection', + 'event.id': '962dba31-1306-4bb1-82c2-2a6d9ef8962d', + 'event.category': 'behavior', + 'event.type': 'info', + 'event.dataset': 'endpoint.diagnostic.collection', + 'kibana.alert.original_time': '2022-05-23T11:02:59.511Z', + 'kibana.alert.ancestors': [ + { + id: 'juKV8IABsphBWHn-nT4H', + type: 'event', + index: '.ds-logs-endpoint.alerts-default-2022.05.23-000001', + depth: 0, + }, + ], + 'kibana.alert.status': 'active', + 'kibana.alert.workflow_status': 'open', + 'kibana.alert.depth': 1, + 'kibana.alert.reason': + 'behavior event with process explorer.exe, file fake_behavior.exe,:59406,:443, on Host-uu8vmc2z8a created medium alert Endpoint Security.', + 'kibana.alert.severity': 'medium', + 'kibana.alert.risk_score': 47, + 'kibana.alert.rule.actions': [], + 'kibana.alert.rule.author': ['Elastic'], + 'kibana.alert.rule.created_at': '2022-05-23T11:01:34.044Z', + 'kibana.alert.rule.created_by': 'elastic', + 'kibana.alert.rule.description': + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.exceptions_list': [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ], + 'kibana.alert.rule.false_positives': [], + 'kibana.alert.rule.from': 'now-10m', + 'kibana.alert.rule.immutable': true, + 'kibana.alert.rule.interval': '5m', + 'kibana.alert.rule.license': 'Elastic License v2', + 'kibana.alert.rule.max_signals': 10000, + 'kibana.alert.rule.references': [], + 'kibana.alert.rule.risk_score_mapping': [ + { + field: 'event.risk_score', + operator: 'equals', + value: '', + }, + ], + 'kibana.alert.rule.rule_id': '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + 'kibana.alert.rule.rule_name_override': 'message', + 'kibana.alert.rule.severity_mapping': [ + { + field: 'event.severity', + operator: 'equals', + severity: 'low', + value: '21', + }, + { + field: 'event.severity', + operator: 'equals', + severity: 'medium', + value: '47', + }, + { + field: 'event.severity', + operator: 'equals', + severity: 'high', + value: '73', + }, + { + field: 'event.severity', + operator: 'equals', + severity: 'critical', + value: '99', + }, + ], + 'kibana.alert.rule.threat': [], + 'kibana.alert.rule.timestamp_override': 'event.ingested', + 'kibana.alert.rule.to': 'now', + 'kibana.alert.rule.type': 'query', + 'kibana.alert.rule.updated_at': '2022-05-23T11:01:34.044Z', + 'kibana.alert.rule.updated_by': 'elastic', + 'kibana.alert.rule.version': 3, + 'kibana.alert.rule.risk_score': 47, + 'kibana.alert.rule.severity': 'medium', + 'kibana.alert.original_event.agent_id_status': 'auth_metadata_missing', + 'kibana.alert.original_event.sequence': 15, + 'kibana.alert.original_event.ingested': '2022-05-23T11:02:53Z', + 'kibana.alert.original_event.code': 'behavior', + 'kibana.alert.original_event.kind': 'alert', + 'kibana.alert.original_event.module': 'endpoint', + 'kibana.alert.original_event.action': 'rule_detection', + 'kibana.alert.original_event.id': '962dba31-1306-4bb1-82c2-2a6d9ef8962d', + 'kibana.alert.original_event.category': 'behavior', + 'kibana.alert.original_event.type': 'info', + 'kibana.alert.original_event.dataset': 'endpoint.diagnostic.collection', + 'kibana.alert.uuid': '2f4c790211998ec3369f581b778e9761ae5647d041edd7b1245f7311fba06f37', + }, + }, + ], + }, + }; +}; + +export const stubProcessTree = (): ResolverNode[] => [ + { + id: 'p1dbx787xe', + parent: 'j0mdzksneq', + name: 'explorer.exe', + data: { + 'process.name': ['explorer.exe'], + '@timestamp': ['2022-05-23T11:02:57.511Z'], + 'process.parent.entity_id': ['j0mdzksneq'], + 'process.Ext.ancestry': ['j0mdzksneq', 'up4f1f87wr'], + 'process.entity_id': ['p1dbx787xe'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'up4f1f87wr', + parent: '3po060bfqd', + name: 'notepad.exe', + data: { + 'process.name': ['notepad.exe'], + '@timestamp': ['2022-05-23T11:02:55.511Z'], + 'process.parent.entity_id': ['3po060bfqd'], + 'process.Ext.ancestry': ['3po060bfqd'], + 'process.entity_id': ['up4f1f87wr'], + }, + stats: { + total: 5, + byCategory: { + registry: 2, + authentication: 1, + driver: 1, + network: 1, + session: 1, + }, + }, + }, + { + id: 'j0mdzksneq', + parent: 'up4f1f87wr', + name: 'notepad.exe', + data: { + 'process.name': ['notepad.exe'], + '@timestamp': ['2022-05-23T11:02:56.511Z'], + 'process.parent.entity_id': ['up4f1f87wr'], + 'process.Ext.ancestry': ['3po060bfqd', 'up4f1f87wr'], + 'process.entity_id': ['j0mdzksneq'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: '3po060bfqd', + name: 'mimikatz.exe', + data: { + 'process.name': ['mimikatz.exe'], + '@timestamp': ['2022-05-23T11:02:54.511Z'], + 'process.entity_id': ['3po060bfqd'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: '72221vhp1s', + parent: 'p1dbx787xe', + name: 'lsass.exe', + data: { + 'process.name': ['lsass.exe'], + '@timestamp': ['2022-05-23T11:03:00.511Z'], + 'process.parent.entity_id': ['p1dbx787xe'], + 'process.Ext.ancestry': ['j0mdzksneq', 'p1dbx787xe'], + 'process.entity_id': ['72221vhp1s'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'o055ylvrqg', + parent: 'p1dbx787xe', + name: 'powershell.exe', + data: { + 'process.name': ['powershell.exe'], + '@timestamp': ['2022-05-23T11:03:09.511Z'], + 'process.parent.entity_id': ['p1dbx787xe'], + 'process.Ext.ancestry': ['j0mdzksneq', 'p1dbx787xe'], + 'process.entity_id': ['o055ylvrqg'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'kemjvigx5w', + parent: 'p1dbx787xe', + name: 'notepad.exe', + data: { + 'process.name': ['notepad.exe'], + '@timestamp': ['2022-05-23T11:03:14.511Z'], + 'process.parent.entity_id': ['p1dbx787xe'], + 'process.Ext.ancestry': ['j0mdzksneq', 'p1dbx787xe'], + 'process.entity_id': ['kemjvigx5w'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: '9xtipos591', + parent: '72221vhp1s', + name: 'iexlorer.exe', + data: { + 'process.name': ['iexlorer.exe'], + '@timestamp': ['2022-05-23T11:03:01.511Z'], + 'process.parent.entity_id': ['72221vhp1s'], + 'process.Ext.ancestry': ['72221vhp1s', 'p1dbx787xe'], + 'process.entity_id': ['9xtipos591'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'spk93ihzue', + parent: '72221vhp1s', + name: 'explorer.exe', + data: { + 'process.name': ['explorer.exe'], + '@timestamp': ['2022-05-23T11:03:02.511Z'], + 'process.parent.entity_id': ['72221vhp1s'], + 'process.Ext.ancestry': ['72221vhp1s', 'p1dbx787xe'], + 'process.entity_id': ['spk93ihzue'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'eefk3v3tg0', + parent: '72221vhp1s', + name: 'iexlorer.exe', + data: { + 'process.name': ['iexlorer.exe'], + '@timestamp': ['2022-05-23T11:03:05.511Z'], + 'process.parent.entity_id': ['72221vhp1s'], + 'process.Ext.ancestry': ['72221vhp1s', 'p1dbx787xe'], + 'process.entity_id': ['eefk3v3tg0'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'z7t7ai4mcl', + parent: 'o055ylvrqg', + name: 'powershell.exe', + data: { + 'process.name': ['powershell.exe'], + '@timestamp': ['2022-05-23T11:03:10.511Z'], + 'process.parent.entity_id': ['o055ylvrqg'], + 'process.Ext.ancestry': ['o055ylvrqg', 'p1dbx787xe'], + 'process.entity_id': ['z7t7ai4mcl'], + }, + stats: { + total: 1, + byCategory: { + network: 1, + }, + }, + }, + { + id: '33k536gv9n', + parent: 'o055ylvrqg', + name: 'mimikatz.exe', + data: { + 'process.name': ['mimikatz.exe'], + '@timestamp': ['2022-05-23T11:03:11.511Z'], + 'process.parent.entity_id': ['o055ylvrqg'], + 'process.Ext.ancestry': ['o055ylvrqg', 'p1dbx787xe'], + 'process.entity_id': ['33k536gv9n'], + }, + stats: { + total: 5, + byCategory: { + file: 4, + network: 1, + }, + }, + }, + { + id: 'tbxjoicr50', + parent: 'kemjvigx5w', + name: 'iexlorer.exe', + data: { + 'process.name': ['iexlorer.exe'], + '@timestamp': ['2022-05-23T11:03:15.511Z'], + 'process.parent.entity_id': ['kemjvigx5w'], + 'process.Ext.ancestry': ['kemjvigx5w', 'p1dbx787xe'], + 'process.entity_id': ['tbxjoicr50'], + }, + stats: { + total: 5, + byCategory: { + authentication: 2, + driver: 2, + session: 2, + file: 1, + }, + }, + }, + { + id: '4kdvfoj2u9', + parent: 'kemjvigx5w', + name: 'notepad.exe', + data: { + 'process.name': ['notepad.exe'], + '@timestamp': ['2022-05-23T11:03:17.511Z'], + 'process.parent.entity_id': ['kemjvigx5w'], + 'process.Ext.ancestry': ['kemjvigx5w', 'p1dbx787xe'], + 'process.entity_id': ['4kdvfoj2u9'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'lfgmzmj99j', + parent: 'kemjvigx5w', + name: 'powershell.exe', + data: { + 'process.name': ['powershell.exe'], + '@timestamp': ['2022-05-23T11:03:19.511Z'], + 'process.parent.entity_id': ['kemjvigx5w'], + 'process.Ext.ancestry': ['kemjvigx5w', 'p1dbx787xe'], + 'process.entity_id': ['lfgmzmj99j'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: '9xtipos591', + parent: '72221vhp1s', + name: 'iexlorer.exe', + data: { + 'process.name': ['iexlorer.exe'], + '@timestamp': ['2022-05-23T11:03:01.511Z'], + 'process.parent.entity_id': ['72221vhp1s'], + 'process.Ext.ancestry': ['72221vhp1s', 'p1dbx787xe'], + 'process.entity_id': ['9xtipos591'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'spk93ihzue', + parent: '72221vhp1s', + name: 'explorer.exe', + data: { + 'process.name': ['explorer.exe'], + '@timestamp': ['2022-05-23T11:03:02.511Z'], + 'process.parent.entity_id': ['72221vhp1s'], + 'process.Ext.ancestry': ['72221vhp1s', 'p1dbx787xe'], + 'process.entity_id': ['spk93ihzue'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'eefk3v3tg0', + parent: '72221vhp1s', + name: 'iexlorer.exe', + data: { + 'process.name': ['iexlorer.exe'], + '@timestamp': ['2022-05-23T11:03:05.511Z'], + 'process.parent.entity_id': ['72221vhp1s'], + 'process.Ext.ancestry': ['72221vhp1s', 'p1dbx787xe'], + 'process.entity_id': ['eefk3v3tg0'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'z7t7ai4mcl', + parent: 'o055ylvrqg', + name: 'powershell.exe', + data: { + 'process.name': ['powershell.exe'], + '@timestamp': ['2022-05-23T11:03:10.511Z'], + 'process.parent.entity_id': ['o055ylvrqg'], + 'process.Ext.ancestry': ['o055ylvrqg', 'p1dbx787xe'], + 'process.entity_id': ['z7t7ai4mcl'], + }, + stats: { + total: 1, + byCategory: { + network: 1, + }, + }, + }, + { + id: '33k536gv9n', + parent: 'o055ylvrqg', + name: 'mimikatz.exe', + data: { + 'process.name': ['mimikatz.exe'], + '@timestamp': ['2022-05-23T11:03:11.511Z'], + 'process.parent.entity_id': ['o055ylvrqg'], + 'process.Ext.ancestry': ['o055ylvrqg', 'p1dbx787xe'], + 'process.entity_id': ['33k536gv9n'], + }, + stats: { + total: 5, + byCategory: { + file: 4, + network: 1, + }, + }, + }, + { + id: 'tbxjoicr50', + parent: 'kemjvigx5w', + name: 'iexlorer.exe', + data: { + 'process.name': ['iexlorer.exe'], + '@timestamp': ['2022-05-23T11:03:15.511Z'], + 'process.parent.entity_id': ['kemjvigx5w'], + 'process.Ext.ancestry': ['kemjvigx5w', 'p1dbx787xe'], + 'process.entity_id': ['tbxjoicr50'], + }, + stats: { + total: 5, + byCategory: { + authentication: 2, + driver: 2, + session: 2, + file: 1, + }, + }, + }, + { + id: '4kdvfoj2u9', + parent: 'kemjvigx5w', + name: 'notepad.exe', + data: { + 'process.name': ['notepad.exe'], + '@timestamp': ['2022-05-23T11:03:17.511Z'], + 'process.parent.entity_id': ['kemjvigx5w'], + 'process.Ext.ancestry': ['kemjvigx5w', 'p1dbx787xe'], + 'process.entity_id': ['4kdvfoj2u9'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'lfgmzmj99j', + parent: 'kemjvigx5w', + name: 'powershell.exe', + data: { + 'process.name': ['powershell.exe'], + '@timestamp': ['2022-05-23T11:03:19.511Z'], + 'process.parent.entity_id': ['kemjvigx5w'], + 'process.Ext.ancestry': ['kemjvigx5w', 'p1dbx787xe'], + 'process.entity_id': ['lfgmzmj99j'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'yx3us23cnz', + parent: 'spk93ihzue', + name: 'notepad.exe', + data: { + 'process.name': ['notepad.exe'], + '@timestamp': ['2022-05-23T11:03:03.511Z'], + 'process.parent.entity_id': ['spk93ihzue'], + 'process.Ext.ancestry': ['72221vhp1s', 'spk93ihzue'], + 'process.entity_id': ['yx3us23cnz'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: '5n8xwwsm4y', + parent: 'spk93ihzue', + name: 'explorer.exe', + data: { + 'process.name': ['explorer.exe'], + '@timestamp': ['2022-05-23T11:03:04.511Z'], + 'process.parent.entity_id': ['spk93ihzue'], + 'process.Ext.ancestry': ['72221vhp1s', 'spk93ihzue'], + 'process.entity_id': ['5n8xwwsm4y'], + }, + stats: { + total: 5, + byCategory: { + authentication: 2, + registry: 2, + session: 2, + driver: 1, + }, + }, + }, + { + id: 'yx2sbsktcr', + parent: 'eefk3v3tg0', + name: 'explorer.exe', + data: { + 'process.name': ['explorer.exe'], + '@timestamp': ['2022-05-23T11:03:06.511Z'], + 'process.parent.entity_id': ['eefk3v3tg0'], + 'process.Ext.ancestry': ['72221vhp1s', 'eefk3v3tg0'], + 'process.entity_id': ['yx2sbsktcr'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'm6681zvabo', + parent: 'eefk3v3tg0', + name: 'iexlorer.exe', + data: { + 'process.name': ['iexlorer.exe'], + '@timestamp': ['2022-05-23T11:03:07.511Z'], + 'process.parent.entity_id': ['eefk3v3tg0'], + 'process.Ext.ancestry': ['72221vhp1s', 'eefk3v3tg0'], + 'process.entity_id': ['m6681zvabo'], + }, + stats: { + total: 5, + byCategory: { + authentication: 3, + session: 3, + network: 1, + registry: 1, + }, + }, + }, + { + id: '57ega9sp2m', + parent: 'eefk3v3tg0', + name: 'explorer.exe', + data: { + 'process.name': ['explorer.exe'], + '@timestamp': ['2022-05-23T11:03:08.511Z'], + 'process.parent.entity_id': ['eefk3v3tg0'], + 'process.Ext.ancestry': ['72221vhp1s', 'eefk3v3tg0'], + 'process.entity_id': ['57ega9sp2m'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: '2q9pvz4liy', + parent: '33k536gv9n', + name: 'iexlorer.exe', + data: { + 'process.name': ['iexlorer.exe'], + '@timestamp': ['2022-05-23T11:03:12.511Z'], + 'process.parent.entity_id': ['33k536gv9n'], + 'process.Ext.ancestry': ['33k536gv9n', 'o055ylvrqg'], + 'process.entity_id': ['2q9pvz4liy'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'hpzss8vcwd', + parent: '33k536gv9n', + name: 'explorer.exe', + data: { + 'process.name': ['explorer.exe'], + '@timestamp': ['2022-05-23T11:03:13.511Z'], + 'process.parent.entity_id': ['33k536gv9n'], + 'process.Ext.ancestry': ['33k536gv9n', 'o055ylvrqg'], + 'process.entity_id': ['hpzss8vcwd'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: 'lar8v50hvc', + parent: 'tbxjoicr50', + name: 'notepad.exe', + data: { + 'process.name': ['notepad.exe'], + '@timestamp': ['2022-05-23T11:03:16.511Z'], + 'process.parent.entity_id': ['tbxjoicr50'], + 'process.Ext.ancestry': ['kemjvigx5w', 'tbxjoicr50'], + 'process.entity_id': ['lar8v50hvc'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, + { + id: '22olnc3pqr', + parent: '4kdvfoj2u9', + name: 'iexlorer.exe', + data: { + 'process.name': ['iexlorer.exe'], + '@timestamp': ['2022-05-23T11:03:18.511Z'], + 'process.parent.entity_id': ['4kdvfoj2u9'], + 'process.Ext.ancestry': ['4kdvfoj2u9', 'kemjvigx5w'], + 'process.entity_id': ['22olnc3pqr'], + }, + stats: { + total: 0, + byCategory: {}, + }, + }, +]; + +export const stubFetchTimelineEvents = () => { + return { + took: 2, + timed_out: false, + _shards: { + total: 2, + successful: 2, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 32, + relation: 'eq', + }, + max_score: 0, + hits: [ + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'f-KV8IABsphBWHn-nT4H', + _score: 0, + _source: { + process: { + args: ['"C:\\mimikatz.exe"', '--fd0'], + Ext: { + ancestry: [], + }, + group_leader: { + name: 'fake leader', + pid: 780, + entity_id: '3po060bfqd', + }, + session_leader: { + name: 'fake session', + pid: 124, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 12, + entity_id: '3po060bfqd', + }, + name: 'mimikatz.exe', + pid: 644, + working_directory: '/home/h351qq3jzg/', + entity_id: '3po060bfqd', + executable: 'C:\\mimikatz.exe', + hash: { + md5: '848277f3-026f-4e55-8447-51d2e2c3d16a', + }, + }, + '@timestamp': 1653303774511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 0, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'bffdfe87-d26a-4314-9c76-06ab1f8638e8', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'gOKV8IABsphBWHn-nT4H', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['3po060bfqd'], + }, + parent: { + pid: 644, + entity_id: '3po060bfqd', + }, + group_leader: { + name: 'fake leader', + pid: 997, + entity_id: '3po060bfqd', + }, + pid: 974, + working_directory: '/home/73xgdrsqsl/', + entity_id: 'up4f1f87wr', + executable: 'C:\\notepad.exe', + args: ['"C:\\notepad.exe"', '--u2n'], + session_leader: { + name: 'fake session', + pid: 753, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 199, + entity_id: '3po060bfqd', + }, + name: 'notepad.exe', + hash: { + md5: 'd5d6d125-d2a6-459c-be47-64d226a9fe86', + }, + }, + '@timestamp': 1653303775511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 1, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '157d0d30-aa65-427b-9ddc-66096328d172', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'i-KV8IABsphBWHn-nT4H', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['up4f1f87wr', '3po060bfqd'], + }, + parent: { + pid: 974, + entity_id: 'up4f1f87wr', + }, + group_leader: { + name: 'fake leader', + pid: 253, + entity_id: '3po060bfqd', + }, + pid: 2241, + working_directory: '/home/x6tczq6ibn/', + entity_id: 'j0mdzksneq', + executable: 'C:\\notepad.exe', + args: ['"C:\\notepad.exe"', '--dl7'], + session_leader: { + name: 'fake session', + pid: 539, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 820, + entity_id: '3po060bfqd', + }, + name: 'notepad.exe', + hash: { + md5: '065eb1f0-d255-41d3-a126-701c266a6ef1', + }, + }, + '@timestamp': 1653303776511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 12, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '2f348502-f7fe-4688-8d13-46c1fbfe5c16', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'jOKV8IABsphBWHn-nT4H', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['up4f1f87wr', '3po060bfqd'], + }, + parent: { + pid: 1591, + entity_id: 'up4f1f87wr', + }, + group_leader: { + name: 'fake leader', + pid: 852, + entity_id: '3po060bfqd', + }, + pid: 3696, + working_directory: '/home/s4c3y8wrzj/', + entity_id: 'j0mdzksneq', + executable: 'C:\\notepad.exe', + args: ['"C:\\notepad.exe"', '--0my'], + session_leader: { + name: 'fake session', + pid: 948, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 570, + entity_id: '3po060bfqd', + }, + name: 'notepad.exe', + hash: { + md5: '56c70b52-e25e-46f1-9d1c-407c355daeca', + }, + }, + '@timestamp': 1654124641511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 13, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'b87ef54d-283c-4029-a0f4-d0b1a7d3179c', + category: ['process'], + type: ['end'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'jeKV8IABsphBWHn-nT4H', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['j0mdzksneq', 'up4f1f87wr'], + }, + parent: { + pid: 2241, + entity_id: 'j0mdzksneq', + }, + group_leader: { + name: 'fake leader', + pid: 304, + entity_id: '3po060bfqd', + }, + pid: 3678, + working_directory: '/home/943lu62rw5/', + entity_id: 'p1dbx787xe', + executable: 'C:\\explorer.exe', + args: ['"C:\\explorer.exe"', '--4p9'], + session_leader: { + name: 'fake session', + pid: 429, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 894, + entity_id: '3po060bfqd', + }, + name: 'explorer.exe', + hash: { + md5: 'd2e5bdc0-cb03-4a16-b89d-3cc47cdc4463', + }, + }, + '@timestamp': 1653303777511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 14, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'aaa5cae6-a00c-4081-ae14-b10393e455c1', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'j-KV8IABsphBWHn-nT4H', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['p1dbx787xe', 'j0mdzksneq'], + }, + parent: { + pid: 4555, + entity_id: 'p1dbx787xe', + }, + group_leader: { + name: 'fake leader', + pid: 111, + entity_id: '3po060bfqd', + }, + pid: 1618, + working_directory: '/home/mwler9rdyj/', + entity_id: '72221vhp1s', + executable: 'C:\\lsass.exe', + args: ['"C:\\lsass.exe"', '--448'], + session_leader: { + name: 'fake session', + pid: 259, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 301, + entity_id: '3po060bfqd', + }, + name: 'lsass.exe', + hash: { + md5: '9bdc057b-5b79-4a1f-b96d-5027d1089977', + }, + }, + '@timestamp': 1653303780511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 16, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'e5915b76-0f6b-461b-945c-d9cf9291fa1d', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'kOKV8IABsphBWHn-nT4H', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['72221vhp1s', 'p1dbx787xe'], + }, + parent: { + pid: 4708, + entity_id: '72221vhp1s', + }, + group_leader: { + name: 'fake leader', + pid: 315, + entity_id: '3po060bfqd', + }, + pid: 1156, + working_directory: '/home/ohyrjwqpx7/', + entity_id: '9xtipos591', + executable: 'C:\\iexlorer.exe', + args: ['"C:\\iexlorer.exe"', '--f1s'], + session_leader: { + name: 'fake session', + pid: 390, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 268, + entity_id: '3po060bfqd', + }, + name: 'iexlorer.exe', + hash: { + md5: 'b4211e8c-9e0c-4957-9dac-3e2086f53a36', + }, + }, + '@timestamp': 1653303781511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 17, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'aa384768-c473-472e-94bb-d839100e6918', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'keKV8IABsphBWHn-nT4H', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['72221vhp1s', 'p1dbx787xe'], + }, + parent: { + pid: 3608, + entity_id: '72221vhp1s', + }, + group_leader: { + name: 'fake leader', + pid: 477, + entity_id: '3po060bfqd', + }, + pid: 2464, + working_directory: '/home/bpq5j3fgzq/', + entity_id: 'spk93ihzue', + executable: 'C:\\explorer.exe', + args: ['"C:\\explorer.exe"', '--9ii'], + session_leader: { + name: 'fake session', + pid: 247, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 493, + entity_id: '3po060bfqd', + }, + name: 'explorer.exe', + hash: { + md5: 'b25a9cc0-bf7f-4880-8b16-7da5f545bf22', + }, + }, + '@timestamp': 1653303782511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 18, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'bb7d3758-2274-439d-8fd4-64f7cd3d5411', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'kuKV8IABsphBWHn-nT4H', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['spk93ihzue', '72221vhp1s'], + }, + parent: { + pid: 4087, + entity_id: 'spk93ihzue', + }, + group_leader: { + name: 'fake leader', + pid: 788, + entity_id: '3po060bfqd', + }, + pid: 632, + working_directory: '/home/0qiwrpn3zj/', + entity_id: 'yx3us23cnz', + executable: 'C:\\notepad.exe', + args: ['"C:\\notepad.exe"', '--1et'], + session_leader: { + name: 'fake session', + pid: 374, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 353, + entity_id: '3po060bfqd', + }, + name: 'notepad.exe', + hash: { + md5: '134dbb94-ee50-47bb-8b5b-73b9e21670cb', + }, + }, + '@timestamp': 1653303783511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 19, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'c4962257-1f6c-4098-b13b-9089ea1d0af5', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'k-KV8IABsphBWHn-nT4H', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['spk93ihzue', '72221vhp1s'], + }, + parent: { + pid: 429, + entity_id: 'spk93ihzue', + }, + group_leader: { + name: 'fake leader', + pid: 675, + entity_id: '3po060bfqd', + }, + pid: 2390, + working_directory: '/home/4vo90awsyg/', + entity_id: '5n8xwwsm4y', + executable: 'C:\\explorer.exe', + args: ['"C:\\explorer.exe"', '--3e2'], + session_leader: { + name: 'fake session', + pid: 543, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 738, + entity_id: '3po060bfqd', + }, + name: 'explorer.exe', + hash: { + md5: 'bef94719-099d-4afa-b601-36d0c6c638a0', + }, + }, + '@timestamp': 1653303784511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 20, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '403f5fa4-4a04-4a5d-a2cf-d325d1bdde06', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'nuKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['72221vhp1s', 'p1dbx787xe'], + }, + parent: { + pid: 2493, + entity_id: '72221vhp1s', + }, + group_leader: { + name: 'fake leader', + pid: 130, + entity_id: '3po060bfqd', + }, + pid: 214, + working_directory: '/home/ou0t92en75/', + entity_id: 'eefk3v3tg0', + executable: 'C:\\iexlorer.exe', + args: ['"C:\\iexlorer.exe"', '--vqg'], + session_leader: { + name: 'fake session', + pid: 428, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 564, + entity_id: '3po060bfqd', + }, + name: 'iexlorer.exe', + hash: { + md5: 'a7382338-84c3-47e5-9f47-a737922b2ffa', + }, + }, + '@timestamp': 1653303785511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 31, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '183adae5-e6c7-456c-aca4-bb0675b106c9', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'n-KV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['72221vhp1s', 'p1dbx787xe'], + }, + parent: { + pid: 797, + entity_id: '72221vhp1s', + }, + group_leader: { + name: 'fake leader', + pid: 946, + entity_id: '3po060bfqd', + }, + pid: 9, + working_directory: '/home/6bfscwho95/', + entity_id: 'eefk3v3tg0', + executable: 'C:\\explorer.exe', + args: ['"C:\\explorer.exe"', '--ai0'], + session_leader: { + name: 'fake session', + pid: 158, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 453, + entity_id: '3po060bfqd', + }, + name: 'explorer.exe', + hash: { + md5: 'ae236075-a0d8-4735-897a-94e2b6db13f5', + }, + }, + '@timestamp': 1653579812511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 32, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '48d65fd5-9fc2-4250-8d7a-0719f8c38a93', + category: ['process'], + type: ['end'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'oOKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['eefk3v3tg0', '72221vhp1s'], + }, + parent: { + pid: 458, + entity_id: 'eefk3v3tg0', + }, + group_leader: { + name: 'fake leader', + pid: 728, + entity_id: '3po060bfqd', + }, + pid: 4069, + working_directory: '/home/tqoq4hemri/', + entity_id: 'yx2sbsktcr', + executable: 'C:\\explorer.exe', + args: ['"C:\\explorer.exe"', '--tru'], + session_leader: { + name: 'fake session', + pid: 903, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 407, + entity_id: '3po060bfqd', + }, + name: 'explorer.exe', + hash: { + md5: '67057556-8c28-47ef-856a-54d1fee52bab', + }, + }, + '@timestamp': 1653303786511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 33, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'de276c4b-42b9-4e68-869c-ae39afc27976', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'oeKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['eefk3v3tg0', '72221vhp1s'], + }, + parent: { + pid: 4334, + entity_id: 'eefk3v3tg0', + }, + group_leader: { + name: 'fake leader', + pid: 134, + entity_id: '3po060bfqd', + }, + pid: 82, + working_directory: '/home/40f18m4zzo/', + entity_id: 'm6681zvabo', + executable: 'C:\\iexlorer.exe', + args: ['"C:\\iexlorer.exe"', '--kym'], + session_leader: { + name: 'fake session', + pid: 290, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 141, + entity_id: '3po060bfqd', + }, + name: 'iexlorer.exe', + hash: { + md5: '51281a5c-7eba-42f9-b563-61f0eac87e33', + }, + }, + '@timestamp': 1653303787511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 34, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'd4c6c047-5c70-47bc-9f50-7b143e580caa', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'rOKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['eefk3v3tg0', '72221vhp1s'], + }, + parent: { + pid: 1876, + entity_id: 'eefk3v3tg0', + }, + group_leader: { + name: 'fake leader', + pid: 43, + entity_id: '3po060bfqd', + }, + pid: 1168, + working_directory: '/home/rdk4jzxof4/', + entity_id: '57ega9sp2m', + executable: 'C:\\explorer.exe', + args: ['"C:\\explorer.exe"', '--ham'], + session_leader: { + name: 'fake session', + pid: 497, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 280, + entity_id: '3po060bfqd', + }, + name: 'explorer.exe', + hash: { + md5: '1b12825c-5b27-4172-8a96-c518df53ab26', + }, + }, + '@timestamp': 1653303788511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 45, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'eae70e75-e955-436e-94c9-034a485a4f61', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'reKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['p1dbx787xe', 'j0mdzksneq'], + }, + parent: { + pid: 255, + entity_id: 'p1dbx787xe', + }, + group_leader: { + name: 'fake leader', + pid: 81, + entity_id: '3po060bfqd', + }, + pid: 3316, + working_directory: '/home/5zyydjd6ns/', + entity_id: 'o055ylvrqg', + executable: 'C:\\powershell.exe', + args: ['"C:\\powershell.exe"', '--622'], + session_leader: { + name: 'fake session', + pid: 239, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 805, + entity_id: '3po060bfqd', + }, + name: 'powershell.exe', + hash: { + md5: 'cd25ea58-396f-48f7-a1c3-4d3bafd8348c', + }, + }, + '@timestamp': 1653303789511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 46, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '7b41a46d-5d21-4f40-bd5b-dba8465a4c6c', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'ruKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['o055ylvrqg', 'p1dbx787xe'], + }, + parent: { + pid: 4612, + entity_id: 'o055ylvrqg', + }, + group_leader: { + name: 'fake leader', + pid: 155, + entity_id: '3po060bfqd', + }, + pid: 2147, + working_directory: '/home/4nsogy8ycy/', + entity_id: 'z7t7ai4mcl', + executable: 'C:\\powershell.exe', + args: ['"C:\\powershell.exe"', '--p7b'], + session_leader: { + name: 'fake session', + pid: 159, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 887, + entity_id: '3po060bfqd', + }, + name: 'powershell.exe', + hash: { + md5: '113f90f8-895c-4497-b69e-1ca043011b95', + }, + }, + '@timestamp': 1653303790511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 47, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'c0717b62-387a-4802-b164-45918c790902', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'r-KV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['o055ylvrqg', 'p1dbx787xe'], + }, + parent: { + pid: 4431, + entity_id: 'o055ylvrqg', + }, + group_leader: { + name: 'fake leader', + pid: 11, + entity_id: '3po060bfqd', + }, + pid: 3288, + working_directory: '/home/ji0q863pka/', + entity_id: 'z7t7ai4mcl', + executable: 'C:\\explorer.exe', + args: ['"C:\\explorer.exe"', '--1j6'], + session_leader: { + name: 'fake session', + pid: 419, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 209, + entity_id: '3po060bfqd', + }, + name: 'explorer.exe', + hash: { + md5: 'f1e77430-6074-4bfb-986a-97725bf589c2', + }, + }, + '@timestamp': 1653897359511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 48, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'f48f37fc-a0b0-4c01-b3d8-a9664e46c13a', + category: ['process'], + type: ['end'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'uuKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['o055ylvrqg', 'p1dbx787xe'], + }, + parent: { + pid: 3885, + entity_id: 'o055ylvrqg', + }, + group_leader: { + name: 'fake leader', + pid: 409, + entity_id: '3po060bfqd', + }, + pid: 5, + working_directory: '/home/5q6hfguqxr/', + entity_id: '33k536gv9n', + executable: 'C:\\mimikatz.exe', + args: ['"C:\\mimikatz.exe"', '--3nf'], + session_leader: { + name: 'fake session', + pid: 821, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 597, + entity_id: '3po060bfqd', + }, + name: 'mimikatz.exe', + hash: { + md5: 'a756c0dc-d018-4720-a55d-23080f19afeb', + }, + }, + '@timestamp': 1653303791511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 59, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '95024994-bbf0-4076-8ee0-8d9349d2e2b5', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'xeKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['33k536gv9n', 'o055ylvrqg'], + }, + parent: { + pid: 794, + entity_id: '33k536gv9n', + }, + group_leader: { + name: 'fake leader', + pid: 49, + entity_id: '3po060bfqd', + }, + pid: 1583, + working_directory: '/home/pwkog8tum3/', + entity_id: '2q9pvz4liy', + executable: 'C:\\iexlorer.exe', + args: ['"C:\\iexlorer.exe"', '--8jk'], + session_leader: { + name: 'fake session', + pid: 131, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 707, + entity_id: '3po060bfqd', + }, + name: 'iexlorer.exe', + hash: { + md5: 'ecac2bc0-69d9-4fc0-9af3-8a3256b73f0c', + }, + }, + '@timestamp': 1653303792511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 70, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'c514d95e-aeda-4ac6-8fb0-60e0baec183f', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'xuKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['33k536gv9n', 'o055ylvrqg'], + }, + parent: { + pid: 3084, + entity_id: '33k536gv9n', + }, + group_leader: { + name: 'fake leader', + pid: 47, + entity_id: '3po060bfqd', + }, + pid: 2788, + working_directory: '/home/tw5d4v2dhd/', + entity_id: 'hpzss8vcwd', + executable: 'C:\\explorer.exe', + args: ['"C:\\explorer.exe"', '--wyx'], + session_leader: { + name: 'fake session', + pid: 583, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 713, + entity_id: '3po060bfqd', + }, + name: 'explorer.exe', + hash: { + md5: '2fe80980-3b06-4a51-9cdd-e011a73b7480', + }, + }, + '@timestamp': 1653303793511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 71, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '4b4e7a1d-4b19-47bc-a98c-660ce632d91f', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'x-KV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['33k536gv9n', 'o055ylvrqg'], + }, + parent: { + pid: 2693, + entity_id: '33k536gv9n', + }, + group_leader: { + name: 'fake leader', + pid: 553, + entity_id: '3po060bfqd', + }, + pid: 4777, + working_directory: '/home/t59qkz7ecu/', + entity_id: 'hpzss8vcwd', + executable: 'C:\\mimikatz.exe', + args: ['"C:\\mimikatz.exe"', '--mol'], + session_leader: { + name: 'fake session', + pid: 255, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 354, + entity_id: '3po060bfqd', + }, + name: 'mimikatz.exe', + hash: { + md5: '8fc89958-89e5-43f6-959f-97f9115771d0', + }, + }, + '@timestamp': 1654235413511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 72, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'd3fa03e3-201b-4d65-8faa-0008736d92b6', + category: ['process'], + type: ['end'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'yOKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['p1dbx787xe', 'j0mdzksneq'], + }, + parent: { + pid: 2555, + entity_id: 'p1dbx787xe', + }, + group_leader: { + name: 'fake leader', + pid: 838, + entity_id: '3po060bfqd', + }, + pid: 288, + working_directory: '/home/l6y0ju3us6/', + entity_id: 'kemjvigx5w', + executable: 'C:\\notepad.exe', + args: ['"C:\\notepad.exe"', '--xci'], + session_leader: { + name: 'fake session', + pid: 350, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 366, + entity_id: '3po060bfqd', + }, + name: 'notepad.exe', + hash: { + md5: 'db204c43-247a-4a61-8af5-7f332434173e', + }, + }, + '@timestamp': 1653303794511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 73, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '3b7d327a-a560-4d8a-9b47-749ae83366e8', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'yeKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['p1dbx787xe', 'j0mdzksneq'], + }, + parent: { + pid: 3103, + entity_id: 'p1dbx787xe', + }, + group_leader: { + name: 'fake leader', + pid: 210, + entity_id: '3po060bfqd', + }, + pid: 4492, + working_directory: '/home/5s499lqduj/', + entity_id: 'kemjvigx5w', + executable: 'C:\\mimikatz.exe', + args: ['"C:\\mimikatz.exe"', '--hxr'], + session_leader: { + name: 'fake session', + pid: 71, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 505, + entity_id: '3po060bfqd', + }, + name: 'mimikatz.exe', + hash: { + md5: '2d270895-8718-48c6-acfb-5d6ddfb10bb7', + }, + }, + '@timestamp': 1653571764511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 74, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'b363ec64-a5d7-4589-84e6-9055fd39d122', + category: ['process'], + type: ['end'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: 'yuKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['kemjvigx5w', 'p1dbx787xe'], + }, + parent: { + pid: 3678, + entity_id: 'kemjvigx5w', + }, + group_leader: { + name: 'fake leader', + pid: 451, + entity_id: '3po060bfqd', + }, + pid: 2275, + working_directory: '/home/capo07rwi3/', + entity_id: 'tbxjoicr50', + executable: 'C:\\iexlorer.exe', + args: ['"C:\\iexlorer.exe"', '--lc1'], + session_leader: { + name: 'fake session', + pid: 891, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 595, + entity_id: '3po060bfqd', + }, + name: 'iexlorer.exe', + hash: { + md5: '5dd468cb-85d0-47be-b84a-6bc3dca6767c', + }, + }, + '@timestamp': 1653303795511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 75, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'cafd1046-57fe-4ac1-aadf-0810977d50d6', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: '1eKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['tbxjoicr50', 'kemjvigx5w'], + }, + parent: { + pid: 3523, + entity_id: 'tbxjoicr50', + }, + group_leader: { + name: 'fake leader', + pid: 989, + entity_id: '3po060bfqd', + }, + pid: 507, + working_directory: '/home/wrlu2t6z99/', + entity_id: 'lar8v50hvc', + executable: 'C:\\notepad.exe', + args: ['"C:\\notepad.exe"', '--ceo'], + session_leader: { + name: 'fake session', + pid: 349, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 411, + entity_id: '3po060bfqd', + }, + name: 'notepad.exe', + hash: { + md5: '71ba9887-26ab-467a-900e-28178da95d1b', + }, + }, + '@timestamp': 1653303796511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 86, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'a56a3209-3146-4186-880a-51ef223cc5e5', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: '1uKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['tbxjoicr50', 'kemjvigx5w'], + }, + parent: { + pid: 4126, + entity_id: 'tbxjoicr50', + }, + group_leader: { + name: 'fake leader', + pid: 389, + entity_id: '3po060bfqd', + }, + pid: 1700, + working_directory: '/home/k5k0zkznd4/', + entity_id: 'lar8v50hvc', + executable: 'C:\\notepad.exe', + args: ['"C:\\notepad.exe"', '--24g'], + session_leader: { + name: 'fake session', + pid: 751, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 693, + entity_id: '3po060bfqd', + }, + name: 'notepad.exe', + hash: { + md5: '3f9905cf-4b57-4a14-87d6-cdf24222a164', + }, + }, + '@timestamp': 1654251443511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 87, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '51d0ba93-78c5-4a60-b760-7b1f1c1a02e8', + category: ['process'], + type: ['end'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: '1-KV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['kemjvigx5w', 'p1dbx787xe'], + }, + parent: { + pid: 4339, + entity_id: 'kemjvigx5w', + }, + group_leader: { + name: 'fake leader', + pid: 592, + entity_id: '3po060bfqd', + }, + pid: 3149, + working_directory: '/home/gzb0sjtj79/', + entity_id: '4kdvfoj2u9', + executable: 'C:\\notepad.exe', + args: ['"C:\\notepad.exe"', '--wlr'], + session_leader: { + name: 'fake session', + pid: 411, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 514, + entity_id: '3po060bfqd', + }, + name: 'notepad.exe', + hash: { + md5: '9531cfdc-9634-4169-97b2-d6fcaedf946f', + }, + }, + '@timestamp': 1653303797511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 88, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'a2d38b84-adbe-4e40-9265-109cb0fb9374', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: '2OKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['4kdvfoj2u9', 'kemjvigx5w'], + }, + parent: { + pid: 4637, + entity_id: '4kdvfoj2u9', + }, + group_leader: { + name: 'fake leader', + pid: 312, + entity_id: '3po060bfqd', + }, + pid: 3741, + working_directory: '/home/xl5bdg92xa/', + entity_id: '22olnc3pqr', + executable: 'C:\\iexlorer.exe', + args: ['"C:\\iexlorer.exe"', '--7cj'], + session_leader: { + name: 'fake session', + pid: 549, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 325, + entity_id: '3po060bfqd', + }, + name: 'iexlorer.exe', + hash: { + md5: 'd8754665-d660-4188-bfb4-22f7837b9dd8', + }, + }, + '@timestamp': 1653303798511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 89, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '89782b1c-e288-443c-96fb-858447ae2b4f', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: '2eKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['4kdvfoj2u9', 'kemjvigx5w'], + }, + parent: { + pid: 210, + entity_id: '4kdvfoj2u9', + }, + group_leader: { + name: 'fake leader', + pid: 173, + entity_id: '3po060bfqd', + }, + pid: 1497, + working_directory: '/home/uhaptao1wl/', + entity_id: '22olnc3pqr', + executable: 'C:\\notepad.exe', + args: ['"C:\\notepad.exe"', '--232'], + session_leader: { + name: 'fake session', + pid: 694, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 671, + entity_id: '3po060bfqd', + }, + name: 'notepad.exe', + hash: { + md5: '98d7edc5-42be-4dc5-b1c5-3321c485a1db', + }, + }, + '@timestamp': 1654059315511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 90, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '47a5bf53-99a9-4982-af65-1fcd3abb1d40', + category: ['process'], + type: ['end'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: '2uKV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['kemjvigx5w', 'p1dbx787xe'], + }, + parent: { + pid: 2107, + entity_id: 'kemjvigx5w', + }, + group_leader: { + name: 'fake leader', + pid: 261, + entity_id: '3po060bfqd', + }, + pid: 3715, + working_directory: '/home/f5583b60xu/', + entity_id: 'lfgmzmj99j', + executable: 'C:\\powershell.exe', + args: ['"C:\\powershell.exe"', '--10x'], + session_leader: { + name: 'fake session', + pid: 57, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 430, + entity_id: '3po060bfqd', + }, + name: 'powershell.exe', + hash: { + md5: '91a99525-0bc3-42d8-89f7-1fe5cecb8334', + }, + }, + '@timestamp': 1653303799511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 91, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: 'be3621bb-efc9-4eee-b8de-6d07bace543c', + category: ['process'], + type: ['start'], + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2022.05.23-000001', + _id: '2-KV8IABsphBWHn-nT4I', + _score: 0, + _source: { + process: { + Ext: { + ancestry: ['kemjvigx5w', 'p1dbx787xe'], + }, + parent: { + pid: 3754, + entity_id: 'kemjvigx5w', + }, + group_leader: { + name: 'fake leader', + pid: 22, + entity_id: '3po060bfqd', + }, + pid: 4895, + working_directory: '/home/de1ijqnt0h/', + entity_id: 'lfgmzmj99j', + executable: 'C:\\lsass.exe', + args: ['"C:\\lsass.exe"', '--4mb'], + session_leader: { + name: 'fake session', + pid: 808, + entity_id: '3po060bfqd', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 982, + entity_id: '3po060bfqd', + }, + name: 'lsass.exe', + hash: { + md5: 'eb3ba411-4b8f-402a-bc70-5758b21ad32c', + }, + }, + '@timestamp': 1654060564511, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 92, + ingested: '2022-05-23T11:02:53Z', + kind: 'event', + id: '07dff76f-fb51-42ee-a0af-0f5e49105f8c', + category: ['process'], + type: ['end'], + }, + }, + }, + ], + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/constants.ts b/x-pack/plugins/security_solution/server/lib/telemetry/constants.ts index 3dc0fc4558fc5..a3e173098a19f 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/constants.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/constants.ts @@ -21,6 +21,8 @@ export const TELEMETRY_CHANNEL_ENDPOINT_META = 'endpoint-metadata'; export const TELEMETRY_CHANNEL_DETECTION_ALERTS = 'alerts-detections'; +export const TELEMETRY_CHANNEL_TIMELINE = 'alerts-timeline'; + export const LIST_DETECTION_RULE_EXCEPTION = 'detection_rule_exception'; export const LIST_ENDPOINT_EXCEPTION = 'endpoint_exception'; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index 2f1a5fbd5cc7d..8712a51b15069 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -8,6 +8,7 @@ import { Logger, CoreStart, + IScopedClusterClient, ElasticsearchClient, SavedObjectsClientContract, } from '@kbn/core/server'; @@ -37,8 +38,16 @@ import { trustedApplicationToTelemetryEntry, ruleExceptionListItemToTelemetryEvent, } from './helpers'; +import { Fetcher } from '../../endpoint/routes/resolver/tree/utils/fetch'; +import type { TreeOptions } from '../../endpoint/routes/resolver/tree/utils/fetch'; +import type { + ResolverNode, + SafeEndpointEvent, + ResolverSchema, +} from '../../../common/endpoint/types'; import type { TelemetryEvent, + EnhancedAlertEvent, ESLicense, ESClusterInfo, GetEndpointListResponse, @@ -134,6 +143,21 @@ export interface ITelemetryReceiver { }; fetchPrebuiltRuleAlerts(): Promise; + + fetchTimelineEndpointAlerts( + interval: number + ): Promise>>; + + buildProcessTree( + entityId: string, + resolverSchema: ResolverSchema, + startOfDay: string, + endOfDay: string + ): Promise; + + fetchTimelineEvents( + nodeIds: string[] + ): Promise>>; } export class TelemetryReceiver implements ITelemetryReceiver { @@ -146,6 +170,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { private kibanaIndex?: string; private alertsIndex?: string; private clusterInfo?: ESClusterInfo; + private processTreeFetcher?: Fetcher; private readonly maxRecords = 10_000 as const; constructor(logger: Logger) { @@ -168,6 +193,9 @@ export class TelemetryReceiver implements ITelemetryReceiver { this.soClient = core?.savedObjects.createInternalRepository() as unknown as SavedObjectsClientContract; this.clusterInfo = await this.fetchClusterInfo(); + + const elasticsearch = core?.elasticsearch.client as unknown as IScopedClusterClient; + this.processTreeFetcher = new Fetcher(elasticsearch); } public getClusterInfo(): ESClusterInfo | undefined { @@ -176,7 +204,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { public async fetchFleetAgents() { if (this.esClient === undefined || this.esClient === null) { - throw Error('elasticsearch client is unavailable: cannot retrieve fleet policy responses'); + throw Error('elasticsearch client is unavailable: cannot retrieve fleet agents'); } return this.agentClient?.listAgents({ @@ -430,7 +458,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { */ public async fetchDetectionRules() { if (this.esClient === undefined || this.esClient === null) { - throw Error('elasticsearch client is unavailable: cannot retrieve diagnostic alerts'); + throw Error('elasticsearch client is unavailable: cannot retrieve detection rules'); } const query: SearchRequest = { @@ -509,7 +537,7 @@ export class TelemetryReceiver implements ITelemetryReceiver { */ public async fetchPrebuiltRuleAlerts() { if (this.esClient === undefined || this.esClient === null) { - throw Error('elasticsearch client is unavailable: cannot retrieve detection rule alerts'); + throw Error('elasticsearch client is unavailable: cannot retrieve pre-built rule alerts'); } const query: SearchRequest = { @@ -630,6 +658,131 @@ export class TelemetryReceiver implements ITelemetryReceiver { return telemetryEvents; } + public async fetchTimelineEndpointAlerts(interval: number) { + if (this.esClient === undefined || this.esClient === null) { + throw Error('elasticsearch client is unavailable: cannot retrieve cluster infomation'); + } + + const query: SearchRequest = { + expand_wildcards: ['open' as const, 'hidden' as const], + index: `${this.alertsIndex}*`, + ignore_unavailable: true, + size: 100, + body: { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'event.module': 'endpoint', + }, + }, + ], + }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'kibana.alert.rule.parameters.immutable': 'true', + }, + }, + ], + }, + }, + { + range: { + '@timestamp': { + gte: `now-${interval}h`, + lte: 'now', + }, + }, + }, + ], + }, + }, + }, + }; + + return this.esClient.search(query); + } + + public async buildProcessTree( + entityId: string, + resolverSchema: ResolverSchema, + startOfDay: string, + endOfDay: string + ): Promise { + if (this.processTreeFetcher === undefined || this.processTreeFetcher === null) { + throw Error( + 'resolver tree builder is unavailable: cannot build encoded endpoint event graph' + ); + } + + const request: TreeOptions = { + ancestors: 200, + descendants: 500, + timeRange: { + from: startOfDay, + to: endOfDay, + }, + schema: resolverSchema, + nodes: [entityId], + indexPatterns: [`${this.alertsIndex}*`, 'logs-*'], + descendantLevels: 20, + }; + + return this.processTreeFetcher.tree(request, true); + } + + public async fetchTimelineEvents(nodeIds: string[]) { + if (this.esClient === undefined || this.esClient === null) { + throw Error('elasticsearch client is unavailable: cannot retrieve timeline endpoint events'); + } + + const query: SearchRequest = { + expand_wildcards: ['open' as const, 'hidden' as const], + index: [`${this.alertsIndex}*`, 'logs-*'], + ignore_unavailable: true, + size: 100, + body: { + _source: { + include: [ + '@timestamp', + 'process', + 'event', + 'file', + 'network', + 'dns', + 'kibana.rule.alert.uuid', + ], + }, + query: { + bool: { + filter: [ + { + terms: { + 'process.entity_id': nodeIds, + }, + }, + { + term: { + 'event.category': 'process', + }, + }, + ], + }, + }, + }, + }; + + return this.esClient.search(query); + } + public async fetchClusterInfo(): Promise { if (this.esClient === undefined || this.esClient === null) { throw Error('elasticsearch client is unavailable: cannot retrieve cluster infomation'); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/index.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/index.ts index 9cfb6883e9533..b0141ca7a5fb1 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/index.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/index.ts @@ -11,6 +11,7 @@ import { createTelemetryEndpointTaskConfig } from './endpoint'; import { createTelemetrySecurityListTaskConfig } from './security_lists'; import { createTelemetryDetectionRuleListsTaskConfig } from './detection_rule'; import { createTelemetryPrebuiltRuleAlertsTaskConfig } from './prebuilt_rule_alerts'; +import { createTelemetryTimelineTaskConfig } from './timelines'; import { MAX_SECURITY_LIST_TELEMETRY_BATCH, MAX_ENDPOINT_TELEMETRY_BATCH, @@ -25,5 +26,6 @@ export function createTelemetryTaskConfigs(): SecurityTelemetryTaskConfig[] { createTelemetrySecurityListTaskConfig(MAX_ENDPOINT_TELEMETRY_BATCH), createTelemetryDetectionRuleListsTaskConfig(MAX_DETECTION_RULE_TELEMETRY_BATCH), createTelemetryPrebuiltRuleAlertsTaskConfig(MAX_DETECTION_ALERTS_BATCH), + createTelemetryTimelineTaskConfig(), ]; } diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/timelines.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/timelines.test.ts new file mode 100644 index 0000000000000..36f6d3caa6d46 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/timelines.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { createTelemetryTimelineTaskConfig } from './timelines'; +import { createMockTelemetryEventsSender, createMockTelemetryReceiver } from '../__mocks__'; + +describe('timeline telemetry task test', () => { + let logger: ReturnType; + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + }); + + test('timeline telemetry task should be correctly set up', async () => { + const testTaskExecutionPeriod = { + last: undefined, + current: new Date().toISOString(), + }; + const mockTelemetryEventsSender = createMockTelemetryEventsSender(); + const mockTelemetryReceiver = createMockTelemetryReceiver(); + const telemetryTelemetryTaskConfig = createTelemetryTimelineTaskConfig(); + + await telemetryTelemetryTaskConfig.runTask( + 'test-timeline-task-id', + logger, + mockTelemetryReceiver, + mockTelemetryEventsSender, + testTaskExecutionPeriod + ); + + expect(mockTelemetryReceiver.buildProcessTree).toHaveBeenCalled(); + expect(mockTelemetryReceiver.fetchTimelineEvents).toHaveBeenCalled(); + expect(mockTelemetryReceiver.fetchTimelineEndpointAlerts).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/timelines.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/timelines.ts new file mode 100644 index 0000000000000..c85d933172786 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/timelines.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { Logger } from '@kbn/core/server'; +import { SafeEndpointEvent } from '../../../../common/endpoint/types'; +import { ITelemetryEventsSender } from '../sender'; +import { ITelemetryReceiver } from '../receiver'; +import type { TaskExecutionPeriod } from '../task'; +import type { + ESClusterInfo, + ESLicense, + TimelineTelemetryTemplate, + TimelineTelemetryEvent, +} from '../types'; +import { TELEMETRY_CHANNEL_TIMELINE } from '../constants'; +import { resolverEntity } from '../../../endpoint/routes/resolver/entity/utils/build_resolver_entity'; + +export function createTelemetryTimelineTaskConfig() { + return { + type: 'security:telemetry-timelines', + title: 'Security Solution Timeline telemetry', + interval: '3h', + timeout: '10m', + version: '1.0.0', + runTask: async ( + taskId: string, + logger: Logger, + receiver: ITelemetryReceiver, + sender: ITelemetryEventsSender, + taskExecutionPeriod: TaskExecutionPeriod + ) => { + let counter = 0; + + logger.debug(`Running task: ${taskId}`); + + const [clusterInfoPromise, licenseInfoPromise] = await Promise.allSettled([ + receiver.fetchClusterInfo(), + receiver.fetchLicenseInfo(), + ]); + + const clusterInfo = + clusterInfoPromise.status === 'fulfilled' + ? clusterInfoPromise.value + : ({} as ESClusterInfo); + + const licenseInfo = + licenseInfoPromise.status === 'fulfilled' + ? licenseInfoPromise.value + : ({} as ESLicense | undefined); + + const now = moment(); + const startOfDay = now.startOf('day').toISOString(); + const endOfDay = now.endOf('day').toISOString(); + + const baseDocument = { + version: clusterInfo.version?.number, + cluster_name: clusterInfo.cluster_name, + cluster_uuid: clusterInfo.cluster_uuid, + license_uuid: licenseInfo?.uid, + }; + + // Fetch EP Alerts + + const endpointAlerts = await receiver.fetchTimelineEndpointAlerts(3); + + // No EP Alerts -> Nothing to do + + if ( + endpointAlerts.hits.hits?.length === 0 || + endpointAlerts.hits.hits?.length === undefined + ) { + logger.debug('no endpoint alerts received. exiting telemetry task.'); + return counter; + } + + // Build process tree for each EP Alert recieved + + for (const alert of endpointAlerts.hits.hits) { + const eventId = alert._source ? alert._source['event.id'] : 'unknown'; + const alertUUID = alert._source ? alert._source['kibana.alert.uuid'] : 'unknown'; + + const entities = resolverEntity([alert]); + + // Build Tree + + const tree = await receiver.buildProcessTree( + entities[0].id, + entities[0].schema, + startOfDay, + endOfDay + ); + + const nodeIds = [] as string[]; + for (const node of tree) { + const nodeId = node?.id.toString(); + nodeIds.push(nodeId); + } + + // Fetch event lineage + + const timelineEvents = await receiver.fetchTimelineEvents(nodeIds); + + const eventsStore = new Map(); + for (const event of timelineEvents.hits.hits) { + const doc = event._source; + + if (doc !== null && doc !== undefined) { + const entityId = doc?.process?.entity_id?.toString(); + if (entityId !== null && entityId !== undefined) eventsStore.set(entityId, doc); + } + } + + // Create telemetry record + + const telemetryTimeline: TimelineTelemetryEvent[] = []; + for (const node of tree) { + const id = node.id.toString(); + const event = eventsStore.get(id); + + const timelineTelemetryEvent: TimelineTelemetryEvent = { + ...node, + event, + }; + + telemetryTimeline.push(timelineTelemetryEvent); + } + + const record: TimelineTelemetryTemplate = { + '@timestamp': moment().toISOString(), + ...baseDocument, + alert_id: alertUUID, + event_id: eventId, + timeline: telemetryTimeline, + }; + + sender.sendOnDemand(TELEMETRY_CHANNEL_TIMELINE, [record]); + counter += 1; + } + + logger.debug(`sent ${counter} timelines. exiting telemetry task.`); + return counter; + }, + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts index d70a011ea85aa..7c22bed299fc3 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts @@ -6,6 +6,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { AlertEvent, ResolverNode, SafeResolverEvent } from '../../../common/endpoint/types'; type BaseSearchTypes = string | number | boolean | object; export type SearchTypes = BaseSearchTypes | BaseSearchTypes[] | undefined; @@ -357,3 +358,20 @@ export interface RuleSearchResult { params: DetectionRuleParms; }; } + +// EP Timeline telemetry + +export type EnhancedAlertEvent = AlertEvent & { 'event.id': string; 'kibana.alert.uuid': string }; + +export type TimelineTelemetryEvent = ResolverNode & { event: SafeResolverEvent | undefined }; + +export interface TimelineTelemetryTemplate { + '@timestamp': string; + cluster_uuid: string; + cluster_name: string; + version: string | undefined; + license_uuid: string | undefined; + alert_id: string | undefined; + event_id: string; + timeline: TimelineTelemetryEvent[]; +} From e2190233d8c13321bdf57bd6818be2f9adedc0f5 Mon Sep 17 00:00:00 2001 From: DeDe Morton Date: Mon, 23 May 2022 10:22:21 -0700 Subject: [PATCH 101/120] Discourage use of elastic user in add data tutorials (#132084) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../instructions/auditbeat_instructions.ts | 20 +++++++++++++++---- .../instructions/cloud_instructions.ts | 5 ++++- .../instructions/filebeat_instructions.ts | 20 +++++++++++++++---- .../instructions/functionbeat_instructions.ts | 10 ++++++++-- .../instructions/heartbeat_instructions.ts | 20 +++++++++++++++---- .../instructions/metricbeat_instructions.ts | 20 +++++++++++++++---- .../instructions/winlogbeat_instructions.ts | 5 ++++- .../translations/translations/fr-FR.json | 19 ------------------ .../translations/translations/ja-JP.json | 19 ------------------ .../translations/translations/zh-CN.json | 19 ------------------ 10 files changed, 80 insertions(+), 77 deletions(-) diff --git a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts index 3968aff312380..2e1d0c57dc680 100644 --- a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts @@ -193,13 +193,16 @@ export const createAuditbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ +> **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ +authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.auditbeat}/securing-auditbeat.html', }, } ), @@ -231,13 +234,16 @@ export const createAuditbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ +> **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ +authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.auditbeat}/securing-auditbeat.html', }, } ), @@ -269,13 +275,16 @@ export const createAuditbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ +> **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ +authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.auditbeat}/securing-auditbeat.html', }, } ), @@ -310,13 +319,16 @@ export const createAuditbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ + > **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ + authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.auditbeat}/securing-auditbeat.html', }, } ), diff --git a/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts b/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts index 6d547b2a1d40d..3562e4fb8b8ce 100644 --- a/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts @@ -15,7 +15,10 @@ export const cloudPasswordAndResetLink = i18n.translate( 'Where {passwordTemplate} is the password of the `elastic` user.' + `\\{#config.cloud.profileUrl\\} Forgot the password? [Reset in Elastic Cloud](\\{config.cloud.baseUrl\\}\\{config.cloud.deploymentUrl\\}/security). - \\{/config.cloud.profileUrl\\}`, + \\{/config.cloud.profileUrl\\}` + + '\n\n\ +> **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ +authorized users or API keys, and do not expose passwords in configuration files.', values: { passwordTemplate: '``' }, } ); diff --git a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts index 89445510f2b3d..247343f57b070 100644 --- a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts @@ -183,13 +183,16 @@ export const createFilebeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ +> **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ +authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.filebeat}/securing-filebeat.html', }, } ), @@ -221,13 +224,16 @@ export const createFilebeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ +> **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ +authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.filebeat}/securing-filebeat.html', }, } ), @@ -259,13 +265,16 @@ export const createFilebeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ +> **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ +authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.filebeat}/securing-filebeat.html', }, } ), @@ -300,13 +309,16 @@ export const createFilebeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ +> **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ +authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.filebeat}/securing-filebeat.html', }, } ), diff --git a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts index 60d6fa5cb813b..c6bb2694b2f2a 100644 --- a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts @@ -152,13 +152,16 @@ Kibana index pattern. It is normally safe to omit this command.', defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ + > **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ + authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.functionbeat}/securing-functionbeat.html', }, } ), @@ -196,13 +199,16 @@ Kibana index pattern. It is normally safe to omit this command.', defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ + > **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ + authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.functionbeat}/securing-functionbeat.html', }, } ), diff --git a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts index 5cbd1641bf09a..1fbea1ddf58a1 100644 --- a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts @@ -174,13 +174,16 @@ export const createHeartbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ +> **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ +authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.heartbeat}/securing-heartbeat.html', }, } ), @@ -212,13 +215,16 @@ export const createHeartbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ +> **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ +authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.heartbeat}/securing-heartbeat.html', }, } ), @@ -250,13 +256,16 @@ export const createHeartbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ +> **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ +authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.heartbeat}/securing-heartbeat.html', }, } ), @@ -291,13 +300,16 @@ export const createHeartbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ + > **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ + authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.heartbeat}/securing-heartbeat.html', }, } ), diff --git a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts index 02cd53dddbc1f..04e487b00baad 100644 --- a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts @@ -186,13 +186,16 @@ export const createMetricbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ + > **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ + authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.metricbeat}/securing-metricbeat.html', }, } ), @@ -224,13 +227,16 @@ export const createMetricbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ + > **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ + authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.metricbeat}/securing-metricbeat.html', }, } ), @@ -262,13 +268,16 @@ export const createMetricbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ + > **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ + authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.metricbeat}/securing-metricbeat.html', }, } ), @@ -303,13 +312,16 @@ export const createMetricbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ + > **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ + authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.metricbeat}/securing-metricbeat.html', }, } ), diff --git a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts index 2c33285899f65..c06994e060d17 100644 --- a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts @@ -99,13 +99,16 @@ export const createWinlogbeatInstructions = (context: TutorialContext) => { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ - default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.\n\n\ + > **_Important:_** Do not use the built-in `elastic` user to secure clients in a production environment. Instead set up \ + authorized users or API keys, and do not expose passwords in configuration files. [Learn more]({linkUrl}).', values: { passwordTemplate: '``', esUrlTemplate: '``', kibanaUrlTemplate: '``', configureSslUrl: SSL_DOC_URL, esCertFingerprintTemplate: '``', + linkUrl: '{config.docs.beats.winlogbeat}/securing-winlogbeat.html', }, } ), diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b62a957cfa927..56d2f64de0a65 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -3821,16 +3821,12 @@ "home.tutorials.common.auditbeatCloudInstructions.config.rpmTitle": "Modifier la configuration", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTitle": "Modifier la configuration", - "home.tutorials.common.auditbeatInstructions.config.debTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.auditbeatInstructions.config.debTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.auditbeatInstructions.config.debTitle": "Modifier la configuration", - "home.tutorials.common.auditbeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.auditbeatInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.auditbeatInstructions.config.osxTitle": "Modifier la configuration", - "home.tutorials.common.auditbeatInstructions.config.rpmTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.auditbeatInstructions.config.rpmTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.auditbeatInstructions.config.rpmTitle": "Modifier la configuration", - "home.tutorials.common.auditbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.auditbeatInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.auditbeatInstructions.config.windowsTitle": "Modifier la configuration", "home.tutorials.common.auditbeatInstructions.install.debTextPost": "Vous cherchez les packages 32 bits ? Consultez la [page de téléchargement]({linkUrl}).", @@ -3879,16 +3875,12 @@ "home.tutorials.common.filebeatEnableInstructions.windowsTextPost": "Modifiez les paramètres dans le fichier ''modules.d/{moduleName}.yml''.", "home.tutorials.common.filebeatEnableInstructions.windowsTextPre": "Dans le dossier {path}, exécutez la commande suivante :", "home.tutorials.common.filebeatEnableInstructions.windowsTitle": "Activer et configurer le module {moduleName}", - "home.tutorials.common.filebeatInstructions.config.debTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.filebeatInstructions.config.debTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.filebeatInstructions.config.debTitle": "Modifier la configuration", - "home.tutorials.common.filebeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.filebeatInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.filebeatInstructions.config.osxTitle": "Modifier la configuration", - "home.tutorials.common.filebeatInstructions.config.rpmTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.filebeatInstructions.config.rpmTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.filebeatInstructions.config.rpmTitle": "Modifier la configuration", - "home.tutorials.common.filebeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.filebeatInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.filebeatInstructions.config.windowsTitle": "Modifier la configuration", "home.tutorials.common.filebeatInstructions.install.debTextPost": "Vous cherchez les packages 32 bits ? Consultez la [page de téléchargement]({linkUrl}).", @@ -3929,10 +3921,8 @@ "home.tutorials.common.functionbeatEnableOnPremInstructions.defaultTitle": "Configurer le groupe de logs Cloudwatch", "home.tutorials.common.functionbeatEnableOnPremInstructionsOSXLinux.textPre": "Modifiez les paramètres dans le fichier ''functionbeat.yml''.", "home.tutorials.common.functionbeatEnableOnPremInstructionsWindows.textPre": "Modifiez les paramètres dans le fichier {path}.", - "home.tutorials.common.functionbeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.functionbeatInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.functionbeatInstructions.config.osxTitle": "Configurer le cluster Elastic", - "home.tutorials.common.functionbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.functionbeatInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.functionbeatInstructions.config.windowsTitle": "Modifier la configuration", "home.tutorials.common.functionbeatInstructions.deploy.osxTextPre": "Ceci permet d'installer Functionbeat en tant que fonction Lambda. La commande ''setup'' vérifie la configuration d'Elasticsearch et charge le modèle d'indexation Kibana. L'omission de cette commande est normalement sans risque.", @@ -3973,16 +3963,12 @@ "home.tutorials.common.heartbeatEnableOnPremInstructions.osxTextPre": "Modifiez le paramètre ''heartbeat.monitors'' dans le fichier ''heartbeat.yml''.", "home.tutorials.common.heartbeatEnableOnPremInstructions.rpmTextPre": "Modifiez le paramètre ''heartbeat.monitors'' dans le fichier ''heartbeat.yml''.", "home.tutorials.common.heartbeatEnableOnPremInstructions.windowsTextPre": "Modifiez le paramètre ''heartbeat.monitors'' dans le fichier ''heartbeat.yml''.", - "home.tutorials.common.heartbeatInstructions.config.debTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.heartbeatInstructions.config.debTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.heartbeatInstructions.config.debTitle": "Modifier la configuration", - "home.tutorials.common.heartbeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.heartbeatInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.heartbeatInstructions.config.osxTitle": "Modifier la configuration", - "home.tutorials.common.heartbeatInstructions.config.rpmTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.heartbeatInstructions.config.rpmTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.heartbeatInstructions.config.rpmTitle": "Modifier la configuration", - "home.tutorials.common.heartbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.heartbeatInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.heartbeatInstructions.config.windowsTitle": "Modifier la configuration", "home.tutorials.common.heartbeatInstructions.install.debTextPost": "Vous cherchez les packages 32 bits ? Consultez la [page de téléchargement]({link}).", @@ -4036,16 +4022,12 @@ "home.tutorials.common.metricbeatEnableInstructions.windowsTextPost": "Modifiez les paramètres dans le fichier ''modules.d/{moduleName}.yml''.", "home.tutorials.common.metricbeatEnableInstructions.windowsTextPre": "Dans le dossier {path}, exécutez la commande suivante :", "home.tutorials.common.metricbeatEnableInstructions.windowsTitle": "Activer et configurer le module {moduleName}", - "home.tutorials.common.metricbeatInstructions.config.debTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.metricbeatInstructions.config.debTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.metricbeatInstructions.config.debTitle": "Modifier la configuration", - "home.tutorials.common.metricbeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.metricbeatInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.metricbeatInstructions.config.osxTitle": "Modifier la configuration", - "home.tutorials.common.metricbeatInstructions.config.rpmTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.metricbeatInstructions.config.rpmTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.metricbeatInstructions.config.rpmTitle": "Modifier la configuration", - "home.tutorials.common.metricbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.metricbeatInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.metricbeatInstructions.config.windowsTitle": "Modifier la configuration", "home.tutorials.common.metricbeatInstructions.install.debTextPost": "Vous cherchez les packages 32 bits ? Consultez la [page de téléchargement]({link}).", @@ -4080,7 +4062,6 @@ "home.tutorials.common.winlogbeat.premInstructions.gettingStarted.title": "Commencer", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTitle": "Modifier la configuration", - "home.tutorials.common.winlogbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.", "home.tutorials.common.winlogbeatInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.winlogbeatInstructions.config.windowsTitle": "Modifier la configuration", "home.tutorials.common.winlogbeatInstructions.install.windowsTextPost": "Modifiez les paramètres sous \"output.elasticsearch\" dans le fichier {path} afin de pointer vers votre installation Elasticsearch.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fe7056a5e3ec1..91996cc8f39dd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3915,16 +3915,12 @@ "home.tutorials.common.auditbeatCloudInstructions.config.rpmTitle": "構成を編集する", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTitle": "構成を編集する", - "home.tutorials.common.auditbeatInstructions.config.debTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.auditbeatInstructions.config.debTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatInstructions.config.debTitle": "構成を編集する", - "home.tutorials.common.auditbeatInstructions.config.osxTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.auditbeatInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.auditbeatInstructions.config.rpmTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.auditbeatInstructions.config.rpmTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatInstructions.config.rpmTitle": "構成を編集する", - "home.tutorials.common.auditbeatInstructions.config.windowsTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.auditbeatInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.auditbeatInstructions.install.debTextPost": "32 ビットパッケージをお探しですか?[ダウンロードページ]({linkUrl})をご覧ください。", @@ -3973,16 +3969,12 @@ "home.tutorials.common.filebeatEnableInstructions.windowsTextPost": "「modules.d/{moduleName}.yml」」ファイルで設定を変更します。", "home.tutorials.common.filebeatEnableInstructions.windowsTextPre": "「{path}」フォルダから次のファイルを実行します:", "home.tutorials.common.filebeatEnableInstructions.windowsTitle": "{moduleName} モジュールを有効にし構成します", - "home.tutorials.common.filebeatInstructions.config.debTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.filebeatInstructions.config.debTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatInstructions.config.debTitle": "構成を編集する", - "home.tutorials.common.filebeatInstructions.config.osxTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.filebeatInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.filebeatInstructions.config.rpmTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.filebeatInstructions.config.rpmTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatInstructions.config.rpmTitle": "構成を編集する", - "home.tutorials.common.filebeatInstructions.config.windowsTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.filebeatInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.filebeatInstructions.install.debTextPost": "32 ビットパッケージをお探しですか?[ダウンロードページ]({linkUrl})をご覧ください。", @@ -4023,10 +4015,8 @@ "home.tutorials.common.functionbeatEnableOnPremInstructions.defaultTitle": "Cloudwatch ロググループの構成", "home.tutorials.common.functionbeatEnableOnPremInstructionsOSXLinux.textPre": "「functionbeat.yml」ファイルで設定を変更します。", "home.tutorials.common.functionbeatEnableOnPremInstructionsWindows.textPre": "{path} ファイルで設定を変更します。", - "home.tutorials.common.functionbeatInstructions.config.osxTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.functionbeatInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.functionbeatInstructions.config.osxTitle": "Elastic クラスターの構成", - "home.tutorials.common.functionbeatInstructions.config.windowsTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.functionbeatInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.functionbeatInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.functionbeatInstructions.deploy.osxTextPre": "これにより Functionbeat が Lambda 関数としてインストールされます「setup」コマンドで Elasticsearch の構成を確認し、Kibana インデックスパターンを読み込みます。通常このコマンドを省いても大丈夫です。", @@ -4067,16 +4057,12 @@ "home.tutorials.common.heartbeatEnableOnPremInstructions.osxTextPre": "「heartbeat.yml」ファイルの「heartbeat.monitors」設定を変更します。", "home.tutorials.common.heartbeatEnableOnPremInstructions.rpmTextPre": "「heartbeat.yml」ファイルの「heartbeat.monitors」設定を変更します。", "home.tutorials.common.heartbeatEnableOnPremInstructions.windowsTextPre": "「heartbeat.yml」ファイルの「heartbeat.monitors」設定を変更します。", - "home.tutorials.common.heartbeatInstructions.config.debTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.heartbeatInstructions.config.debTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatInstructions.config.debTitle": "構成を編集する", - "home.tutorials.common.heartbeatInstructions.config.osxTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.heartbeatInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.heartbeatInstructions.config.rpmTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.heartbeatInstructions.config.rpmTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatInstructions.config.rpmTitle": "構成を編集する", - "home.tutorials.common.heartbeatInstructions.config.windowsTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.heartbeatInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.heartbeatInstructions.install.debTextPost": "32 ビットパッケージをお探しですか?[ダウンロードページ]({link})をご覧ください。", @@ -4130,16 +4116,12 @@ "home.tutorials.common.metricbeatEnableInstructions.windowsTextPost": "「modules.d/{moduleName}.yml」」ファイルで設定を変更します。", "home.tutorials.common.metricbeatEnableInstructions.windowsTextPre": "「{path}」フォルダから次のファイルを実行します:", "home.tutorials.common.metricbeatEnableInstructions.windowsTitle": "{moduleName} モジュールを有効にし構成します", - "home.tutorials.common.metricbeatInstructions.config.debTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.metricbeatInstructions.config.debTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatInstructions.config.debTitle": "構成を編集する", - "home.tutorials.common.metricbeatInstructions.config.osxTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.metricbeatInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.metricbeatInstructions.config.rpmTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.metricbeatInstructions.config.rpmTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatInstructions.config.rpmTitle": "構成を編集する", - "home.tutorials.common.metricbeatInstructions.config.windowsTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.metricbeatInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.metricbeatInstructions.install.debTextPost": "32 ビットパッケージをお探しですか?[ダウンロードページ]({link})をご覧ください。", @@ -4174,7 +4156,6 @@ "home.tutorials.common.winlogbeat.premInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTitle": "構成を編集する", - "home.tutorials.common.winlogbeatInstructions.config.windowsTextPostMarkdown": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。Elasticsearchで生成されたデフォルトの証明書を使用して[SSLを構成]({configureSslUrl})するには、{esCertFingerprintTemplate}でフィンガープリントを追加します。", "home.tutorials.common.winlogbeatInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.winlogbeatInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.winlogbeatInstructions.install.windowsTextPost": "{path} ファイルの「output.elasticsearch」を Elasticsearch のインストールに設定します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 990a113fcd9d6..3bc2a40181935 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3925,16 +3925,12 @@ "home.tutorials.common.auditbeatCloudInstructions.config.rpmTitle": "编辑配置", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTitle": "编辑配置", - "home.tutorials.common.auditbeatInstructions.config.debTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.auditbeatInstructions.config.debTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.auditbeatInstructions.config.debTitle": "编辑配置", - "home.tutorials.common.auditbeatInstructions.config.osxTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.auditbeatInstructions.config.osxTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.auditbeatInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.auditbeatInstructions.config.rpmTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.auditbeatInstructions.config.rpmTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.auditbeatInstructions.config.rpmTitle": "编辑配置", - "home.tutorials.common.auditbeatInstructions.config.windowsTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.auditbeatInstructions.config.windowsTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.auditbeatInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.auditbeatInstructions.install.debTextPost": "寻找 32 位软件包?请参阅[下载页面]({linkUrl})。", @@ -3983,16 +3979,12 @@ "home.tutorials.common.filebeatEnableInstructions.windowsTextPost": "在 `modules.d/{moduleName}.yml` 文件中修改设置。", "home.tutorials.common.filebeatEnableInstructions.windowsTextPre": "从 {path} 文件夹中,运行:", "home.tutorials.common.filebeatEnableInstructions.windowsTitle": "启用和配置 {moduleName} 模块", - "home.tutorials.common.filebeatInstructions.config.debTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.filebeatInstructions.config.debTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.filebeatInstructions.config.debTitle": "编辑配置", - "home.tutorials.common.filebeatInstructions.config.osxTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.filebeatInstructions.config.osxTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.filebeatInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.filebeatInstructions.config.rpmTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.filebeatInstructions.config.rpmTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.filebeatInstructions.config.rpmTitle": "编辑配置", - "home.tutorials.common.filebeatInstructions.config.windowsTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.filebeatInstructions.config.windowsTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.filebeatInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.filebeatInstructions.install.debTextPost": "寻找 32 位软件包?请参阅[下载页面]({linkUrl})。", @@ -4033,10 +4025,8 @@ "home.tutorials.common.functionbeatEnableOnPremInstructions.defaultTitle": "配置 Cloudwatch 日志组", "home.tutorials.common.functionbeatEnableOnPremInstructionsOSXLinux.textPre": "在 `functionbeat.yml` 文件中修改设置。", "home.tutorials.common.functionbeatEnableOnPremInstructionsWindows.textPre": "在 {path} 文件中修改设置。", - "home.tutorials.common.functionbeatInstructions.config.osxTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.functionbeatInstructions.config.osxTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.functionbeatInstructions.config.osxTitle": "配置 Elastic 集群", - "home.tutorials.common.functionbeatInstructions.config.windowsTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.functionbeatInstructions.config.windowsTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.functionbeatInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.functionbeatInstructions.deploy.osxTextPre": "这会将 Functionbeat 安装为 Lambda 函数。`setup` 命令检查 Elasticsearch 配置并加载 Kibana 索引模式。通常可省略此命令。", @@ -4077,16 +4067,12 @@ "home.tutorials.common.heartbeatEnableOnPremInstructions.osxTextPre": "在 `heartbeat.yml` 文件中编辑 `heartbeat.monitors` 设置。", "home.tutorials.common.heartbeatEnableOnPremInstructions.rpmTextPre": "在 `heartbeat.yml` 文件中编辑 `heartbeat.monitors` 设置。", "home.tutorials.common.heartbeatEnableOnPremInstructions.windowsTextPre": "在 `heartbeat.yml` 文件中编辑 `heartbeat.monitors` 设置。", - "home.tutorials.common.heartbeatInstructions.config.debTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.heartbeatInstructions.config.debTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.heartbeatInstructions.config.debTitle": "编辑配置", - "home.tutorials.common.heartbeatInstructions.config.osxTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.heartbeatInstructions.config.osxTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.heartbeatInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.heartbeatInstructions.config.rpmTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.heartbeatInstructions.config.rpmTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.heartbeatInstructions.config.rpmTitle": "编辑配置", - "home.tutorials.common.heartbeatInstructions.config.windowsTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.heartbeatInstructions.config.windowsTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.heartbeatInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.heartbeatInstructions.install.debTextPost": "寻找 32 位软件包?请参阅[下载页面]({link})。", @@ -4140,16 +4126,12 @@ "home.tutorials.common.metricbeatEnableInstructions.windowsTextPost": "在 `modules.d/{moduleName}.yml` 文件中修改设置。", "home.tutorials.common.metricbeatEnableInstructions.windowsTextPre": "从 {path} 文件夹中,运行:", "home.tutorials.common.metricbeatEnableInstructions.windowsTitle": "启用和配置 {moduleName} 模块", - "home.tutorials.common.metricbeatInstructions.config.debTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.metricbeatInstructions.config.debTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.metricbeatInstructions.config.debTitle": "编辑配置", - "home.tutorials.common.metricbeatInstructions.config.osxTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.metricbeatInstructions.config.osxTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.metricbeatInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.metricbeatInstructions.config.rpmTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.metricbeatInstructions.config.rpmTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.metricbeatInstructions.config.rpmTitle": "编辑配置", - "home.tutorials.common.metricbeatInstructions.config.windowsTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.metricbeatInstructions.config.windowsTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.metricbeatInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.metricbeatInstructions.install.debTextPost": "寻找 32 位软件包?请参阅[下载页面]({link})。", @@ -4184,7 +4166,6 @@ "home.tutorials.common.winlogbeat.premInstructions.gettingStarted.title": "入门", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTitle": "编辑配置", - "home.tutorials.common.winlogbeatInstructions.config.windowsTextPostMarkdown": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。要使用 Elasticsearch 生成的默认证书 [配置 SSL]({configureSslUrl}),请在 {esCertFingerprintTemplate} 中添加其指纹。", "home.tutorials.common.winlogbeatInstructions.config.windowsTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.winlogbeatInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.winlogbeatInstructions.install.windowsTextPost": "在 {path} 文件中修改 `output.elasticsearch` 下的设置以指向您的 Elasticsearch 安装。", From 2d1ac53300da885c945fae563b7607a5bcb0e667 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Mon, 23 May 2022 12:43:56 -0500 Subject: [PATCH 102/120] [easy][shared-ux] Fix typos in RedirectAppLinks (#132563) --- packages/shared-ux/link/redirect_app/README.mdx | 8 ++++---- packages/shared-ux/link/redirect_app/src/index.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/shared-ux/link/redirect_app/README.mdx b/packages/shared-ux/link/redirect_app/README.mdx index 8e2eada760ea2..07d4f75ab764d 100644 --- a/packages/shared-ux/link/redirect_app/README.mdx +++ b/packages/shared-ux/link/redirect_app/README.mdx @@ -70,17 +70,17 @@ This is the component is likely the most useful to solutions in Kibana. It assu ```tsx import { RedirectAppLinks } from '@kbn/shared-ux-links-redirect-app'; - { ... }}> + { ... }}> . Go to another-app . - + {/* OR */} - + . Go to another-app . - + ``` \ No newline at end of file diff --git a/packages/shared-ux/link/redirect_app/src/index.tsx b/packages/shared-ux/link/redirect_app/src/index.tsx index 5efb99cc48664..09317ebab59f7 100644 --- a/packages/shared-ux/link/redirect_app/src/index.tsx +++ b/packages/shared-ux/link/redirect_app/src/index.tsx @@ -7,7 +7,7 @@ */ export { RedirectAppLinks as RedirectAppLinksContainer } from './redirect_app_links'; -export { RedirectAppLinks as RedirectAppLinksComponent } from './redirect_app_links'; +export { RedirectAppLinks as RedirectAppLinksComponent } from './redirect_app_links.component'; export { RedirectAppLinksKibanaProvider, RedirectAppLinksProvider } from './services'; import React, { FC } from 'react'; From 5ee756b7ace06bcd1da37eb81d8b55b598da4d99 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 23 May 2022 11:51:06 -0600 Subject: [PATCH 103/120] [Maps] use EmsSpriteSheet type from @elastic/ems-client (#132715) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ems_vector_tile_layer.tsx | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx index 6f8bc3470d792..679629b838835 100644 --- a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx @@ -6,7 +6,7 @@ */ import type { Map as MbMap, LayerSpecification, StyleSpecification } from '@kbn/mapbox-gl'; -import { TMSService } from '@elastic/ems-client'; +import { type EmsSpriteSheet, TMSService } from '@elastic/ems-client'; import { i18n } from '@kbn/i18n'; import _ from 'lodash'; // @ts-expect-error @@ -30,20 +30,6 @@ interface SourceRequestMeta { tileLayerId: string; } -// TODO remove once ems_client exports EmsSpriteSheet and EmsSprite type -interface EmsSprite { - height: number; - pixelRatio: number; - sdf?: boolean; - width: number; - x: number; - y: number; -} - -export interface EmsSpriteSheet { - [spriteName: string]: EmsSprite; -} - interface SourceRequestData { spriteSheetImageData?: ImageData; vectorStyleSheet?: StyleSpecification; From f540c5e39232c69408c0af8557e264329804e9bb Mon Sep 17 00:00:00 2001 From: Kristof C Date: Mon, 23 May 2022 12:56:15 -0500 Subject: [PATCH 104/120] [Security Solution] [Detection & Response] 131827 Update Detections Response view with pagination and opening numbers in timeline (#131828) * Fix alert colour pallete & alerts chart header size * Add pagination and navigation to timeline capability * fix translation name conflict * Rename hook file to snake case to match elastic formatting * Change name scheme oof navigateToTimeline to OpenInTimeline & remove styled components Co-authored-by: Kristof-Pierre Cummings Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../table/use_action_cell_data_provider.ts | 2 +- .../alerts_by_status/alerts_by_status.tsx | 13 +- .../cases_by_status/cases_by_status.tsx | 10 +- .../cases_table/cases_table.test.tsx | 2 +- .../cases_table/cases_table.tsx | 2 +- .../hooks/use_navigate_to_timeline.tsx | 76 +++++++ .../host_alerts_table.test.tsx | 59 ++++-- .../host_alerts_table/host_alerts_table.tsx | 97 +++++---- .../host_alerts_table/mock_data.ts | 4 + .../use_host_alerts_items.test.ts | 16 ++ .../use_host_alerts_items.ts | 200 +++++++++++------- .../rule_alerts_table.test.tsx | 2 +- .../rule_alerts_table/rule_alerts_table.tsx | 18 +- .../detection_response/translations.ts | 18 +- .../user_alerts_table/mock_data.ts | 4 + .../use_user_alerts_items.test.ts | 16 ++ .../use_user_alerts_items.ts | 199 ++++++++++------- .../user_alerts_table.test.tsx | 59 ++++-- .../user_alerts_table/user_alerts_table.tsx | 95 +++++---- .../components/detection_response/util.tsx | 39 ---- .../components/detection_response/utils.tsx | 23 +- x-pack/plugins/timelines/common/index.ts | 1 + 22 files changed, 617 insertions(+), 338 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/overview/components/detection_response/hooks/use_navigate_to_timeline.tsx delete mode 100644 x-pack/plugins/security_solution/public/overview/components/detection_response/util.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts b/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts index 15a117817b627..1dded682f54ba 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts @@ -42,7 +42,7 @@ export interface UseActionCellDataProvider { values: string[] | null | undefined; } -const getDataProvider = (field: string, id: string, value: string): DataProvider => ({ +export const getDataProvider = (field: string, id: string, value: string): DataProvider => ({ and: [], enabled: true, id: escapeDataProviderId(id), diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx index f7f3cc9a81f7f..a3f49732267aa 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx @@ -49,11 +49,6 @@ const StyledLegendFlexItem = styled(EuiFlexItem)` padding-top: 45px; `; -// To Do remove this styled component once togglequery is updated: #131405 -const StyledEuiPanel = styled(EuiPanel)` - height: fit-content; -`; - interface AlertsByStatusProps { signalIndexName: string | null; } @@ -124,10 +119,7 @@ export const AlertsByStatus = ({ signalIndexName }: AlertsByStatusProps) => { return ( <> - + {loading && ( { } inspectMultiple toggleStatus={toggleStatus} @@ -212,7 +205,7 @@ export const AlertsByStatus = ({ signalIndexName }: AlertsByStatusProps) => { )} - + ); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/cases_by_status.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/cases_by_status.tsx index 67ed0d43c830c..cc4bfa5e26703 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/cases_by_status.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/cases_by_status.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { FormattedNumber } from '@kbn/i18n-react'; import numeral from '@elastic/numeral'; import { BarChart } from '../../../../common/components/charts/barchart'; -import { LastUpdatedAt } from '../util'; +import { LastUpdatedAt } from '../utils'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { HeaderSection } from '../../../../common/components/header_section'; import { @@ -112,10 +112,6 @@ const Wrapper = styled.div` width: 100%; `; -const StyledEuiPanel = styled(EuiPanel)` - height: 258px; -`; - const CasesByStatusComponent: React.FC = () => { const { toggleStatus, setToggleStatus } = useQueryToggle(CASES_BY_STATUS_ID); const { getAppUrl, navigateTo } = useNavigation(); @@ -155,7 +151,7 @@ const CasesByStatusComponent: React.FC = () => { ); return ( - + { )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.test.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.test.tsx index 4250c20059094..8f9872374e01c 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.test.tsx @@ -76,7 +76,7 @@ describe('CasesTable', () => { mockUseCaseItemsReturn({ isLoading: false }); const { getByText } = renderComponent(); - expect(getByText('Updated now')).toBeInTheDocument(); + expect(getByText(/Updated/)).toBeInTheDocument(); }); it('should render the table columns', () => { diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.tsx index 80d2495f57ab5..838dc8a1fa2de 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.tsx @@ -27,7 +27,7 @@ import { CaseDetailsLink } from '../../../../common/components/links'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { useNavigation, NavigateTo, GetAppUrl } from '../../../../common/lib/kibana'; import * as i18n from '../translations'; -import { LastUpdatedAt } from '../util'; +import { LastUpdatedAt } from '../utils'; import { StatusBadge } from './status_badge'; import { CaseItem, useCaseItems } from './use_case_items'; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/hooks/use_navigate_to_timeline.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/hooks/use_navigate_to_timeline.tsx new file mode 100644 index 0000000000000..417ec82be002a --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/hooks/use_navigate_to_timeline.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useDispatch } from 'react-redux'; + +import { getDataProvider } from '../../../../common/components/event_details/table/use_action_cell_data_provider'; +import { sourcererActions } from '../../../../common/store/sourcerer'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { DataProvider, TimelineId, TimelineType } from '../../../../../common/types/timeline'; +import { useCreateTimeline } from '../../../../timelines/components/timeline/properties/use_create_timeline'; +import { updateProviders } from '../../../../timelines/store/timeline/actions'; + +export const useNavigateToTimeline = () => { + const dispatch = useDispatch(); + + const clearTimeline = useCreateTimeline({ + timelineId: TimelineId.active, + timelineType: TimelineType.default, + }); + + const navigateToTimeline = (dataProvider: DataProvider) => { + // Reset the current timeline + clearTimeline(); + // Update the timeline's providers to match the current prevalence field query + dispatch( + updateProviders({ + id: TimelineId.active, + providers: [dataProvider], + }) + ); + // Only show detection alerts + // (This is required so the timeline event count matches the prevalence count) + dispatch( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: 'security-solution-default', + selectedPatterns: ['.alerts-security.alerts-default'], + }) + ); + }; + + const openHostInTimeline = ({ hostName, severity }: { hostName: string; severity?: string }) => { + const dataProvider = getDataProvider('host.name', '', hostName); + + if (severity) { + dataProvider.and.push(getDataProvider('kibana.alert.severity', '', severity)); + } + + navigateToTimeline(dataProvider); + }; + + const openUserInTimeline = ({ userName, severity }: { userName: string; severity?: string }) => { + const dataProvider = getDataProvider('user.name', '', userName); + + if (severity) { + dataProvider.and.push(getDataProvider('kibana.alert.severity', '', severity)); + } + navigateToTimeline(dataProvider); + }; + + const openRuleInTimeline = (ruleName: string) => { + const dataProvider = getDataProvider('kibana.alert.rule.name', '', ruleName); + + navigateToTimeline(dataProvider); + }; + + return { + openHostInTimeline, + openRuleInTimeline, + openUserInTimeline, + }; +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx index a89055a72df6f..5172db743404c 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; import { parsedVulnerableHostsAlertsResult } from './mock_data'; @@ -30,6 +30,11 @@ const defaultUseHostAlertsItemsReturn: UseHostAlertsItemsReturn = { items: [], isLoading: false, updatedAt: Date.now(), + pagination: { + currentPage: 0, + pageCount: 0, + setPage: () => null, + }, }; const mockUseHostAlertsItems = jest.fn(() => defaultUseHostAlertsItemsReturn); const mockUseHostAlertsItemsReturn = (overrides: Partial) => { @@ -47,34 +52,33 @@ const renderComponent = () => ); -// FLAKY: https://github.com/elastic/kibana/issues/131611 -describe.skip('HostAlertsTable', () => { +describe('HostAlertsTable', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should render empty table', () => { - const { getByText, getByTestId } = renderComponent(); + const { getByText, queryByTestId } = renderComponent(); - expect(getByTestId('severityHostAlertsPanel')).toBeInTheDocument(); + expect(queryByTestId('severityHostAlertsPanel')).toBeInTheDocument(); + expect(queryByTestId('hostTablePaginator')).not.toBeInTheDocument(); expect(getByText('No alerts to display')).toBeInTheDocument(); - expect(getByTestId('severityHostAlertsButton')).toBeInTheDocument(); }); it('should render a loading table', () => { mockUseHostAlertsItemsReturn({ isLoading: true }); - const { getByText, getByTestId } = renderComponent(); + const { getByText, queryByTestId } = renderComponent(); expect(getByText('Updating...')).toBeInTheDocument(); - expect(getByTestId('severityHostAlertsButton')).toBeInTheDocument(); - expect(getByTestId('severityHostAlertsTable')).toHaveClass('euiBasicTable-loading'); + expect(queryByTestId('severityHostAlertsTable')).toHaveClass('euiBasicTable-loading'); + expect(queryByTestId('hostTablePaginator')).not.toBeInTheDocument(); }); it('should render the updated at subtitle', () => { mockUseHostAlertsItemsReturn({ isLoading: false }); const { getByText } = renderComponent(); - expect(getByText('Updated now')).toBeInTheDocument(); + expect(getByText(/Updated/)).toBeInTheDocument(); }); it('should render the table columns', () => { @@ -92,13 +96,32 @@ describe.skip('HostAlertsTable', () => { it('should render the table items', () => { mockUseHostAlertsItemsReturn({ items: [parsedVulnerableHostsAlertsResult[0]] }); - const { getByTestId } = renderComponent(); - - expect(getByTestId('hostSeverityAlertsTable-hostName')).toHaveTextContent('Host-342m5gl1g2'); - expect(getByTestId('hostSeverityAlertsTable-totalAlerts')).toHaveTextContent('100'); - expect(getByTestId('hostSeverityAlertsTable-critical')).toHaveTextContent('5'); - expect(getByTestId('hostSeverityAlertsTable-high')).toHaveTextContent('50'); - expect(getByTestId('hostSeverityAlertsTable-medium')).toHaveTextContent('5'); - expect(getByTestId('hostSeverityAlertsTable-low')).toHaveTextContent('40'); + const { queryByTestId } = renderComponent(); + + expect(queryByTestId('hostSeverityAlertsTable-hostName')).toHaveTextContent('Host-342m5gl1g2'); + expect(queryByTestId('hostSeverityAlertsTable-totalAlerts')).toHaveTextContent('100'); + expect(queryByTestId('hostSeverityAlertsTable-critical')).toHaveTextContent('5'); + expect(queryByTestId('hostSeverityAlertsTable-high')).toHaveTextContent('50'); + expect(queryByTestId('hostSeverityAlertsTable-medium')).toHaveTextContent('5'); + expect(queryByTestId('hostSeverityAlertsTable-low')).toHaveTextContent('40'); + expect(queryByTestId('hostTablePaginator')).not.toBeInTheDocument(); + }); + + it('should render the paginator if more than 4 results', () => { + const mockSetPage = jest.fn(); + + mockUseHostAlertsItemsReturn({ + pagination: { + currentPage: 1, + pageCount: 3, + setPage: mockSetPage, + }, + }); + const { queryByTestId, getByText } = renderComponent(); + const page3 = getByText('3'); + expect(queryByTestId('hostTablePaginator')).toBeInTheDocument(); + + fireEvent.click(page3); + expect(mockSetPage).toHaveBeenCalledWith(2); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx index 49ad4352cb586..f3151db3927db 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx @@ -5,72 +5,56 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; +import React, { useMemo } from 'react'; import { EuiBasicTable, EuiBasicTableColumn, - EuiButton, EuiEmptyPrompt, EuiHealth, + EuiLink, EuiPanel, EuiSpacer, + EuiTablePagination, } from '@elastic/eui'; -import { SecurityPageName } from '../../../../app/types'; import { FormattedCount } from '../../../../common/components/formatted_number'; import { HeaderSection } from '../../../../common/components/header_section'; import { HoverVisibilityContainer } from '../../../../common/components/hover_visibility_container'; import { BUTTON_CLASS as INPECT_BUTTON_CLASS } from '../../../../common/components/inspect'; import { HostDetailsLink } from '../../../../common/components/links'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; -import { useNavigation, NavigateTo, GetAppUrl } from '../../../../common/lib/kibana'; +import { useNavigateToTimeline } from '../hooks/use_navigate_to_timeline'; import * as i18n from '../translations'; -import { LastUpdatedAt, SEVERITY_COLOR } from '../util'; +import { ITEMS_PER_PAGE, LastUpdatedAt, SEVERITY_COLOR } from '../utils'; import { HostAlertsItem, useHostAlertsItems } from './use_host_alerts_items'; -type GetTableColumns = (params: { - getAppUrl: GetAppUrl; - navigateTo: NavigateTo; -}) => Array>; - interface HostAlertsTableProps { signalIndexName: string | null; } -const DETECTION_RESPONSE_HOST_SEVERITY_QUERY_ID = 'vulnerableHostsBySeverityQuery'; +type GetTableColumns = ( + handleClick: (params: { hostName: string; severity?: string }) => void +) => Array>; -// To Do remove this styled component once togglequery is updated: #131405 -const StyledEuiPanel = styled(EuiPanel)` - height: fit-content; -`; +const DETECTION_RESPONSE_HOST_SEVERITY_QUERY_ID = 'vulnerableHostsBySeverityQuery'; export const HostAlertsTable = React.memo(({ signalIndexName }: HostAlertsTableProps) => { - const { getAppUrl, navigateTo } = useNavigation(); + const { openHostInTimeline } = useNavigateToTimeline(); const { toggleStatus, setToggleStatus } = useQueryToggle( DETECTION_RESPONSE_HOST_SEVERITY_QUERY_ID ); - - const { items, isLoading, updatedAt } = useHostAlertsItems({ + const { items, isLoading, updatedAt, pagination } = useHostAlertsItems({ skip: !toggleStatus, queryId: DETECTION_RESPONSE_HOST_SEVERITY_QUERY_ID, signalIndexName, }); - const navigateToHosts = useCallback(() => { - navigateTo({ deepLinkId: SecurityPageName.hosts }); - }, [navigateTo]); - - const columns = useMemo( - () => getTableColumns({ getAppUrl, navigateTo }), - [getAppUrl, navigateTo] - ); + const columns = useMemo(() => getTableColumns(openHostInTimeline), [openHostInTimeline]); return ( - //
- + {toggleStatus && ( <> @@ -91,20 +76,26 @@ export const HostAlertsTable = React.memo(({ signalIndexName }: HostAlertsTableP } /> - - {i18n.VIEW_ALL_HOST_ALERTS} - + {pagination.pageCount > 1 && ( + + )} )} - + - //
); }); HostAlertsTable.displayName = 'HostAlertsTable'; -const getTableColumns: GetTableColumns = ({ getAppUrl, navigateTo }) => [ +const getTableColumns: GetTableColumns = (handleClick) => [ { field: 'hostName', name: i18n.HOST_ALERTS_HOSTNAME_COLUMN, @@ -117,41 +108,59 @@ const getTableColumns: GetTableColumns = ({ getAppUrl, navigateTo }) => [ field: 'totalAlerts', name: i18n.ALERTS_TEXT, 'data-test-subj': 'hostSeverityAlertsTable-totalAlerts', - render: (totalAlerts: number) => , + render: (totalAlerts: number, { hostName }) => ( + handleClick({ hostName })}> + + + ), }, { field: 'critical', name: i18n.STATUS_CRITICAL_LABEL, - render: (count: number) => ( + render: (count: number, { hostName }) => ( - + handleClick({ hostName, severity: 'critical' })} + > + + ), }, { field: 'high', name: i18n.STATUS_HIGH_LABEL, - render: (count: number) => ( + render: (count: number, { hostName }) => ( - + handleClick({ hostName, severity: 'high' })}> + + ), }, { field: 'medium', name: i18n.STATUS_MEDIUM_LABEL, - render: (count: number) => ( + render: (count: number, { hostName }) => ( - + handleClick({ hostName, severity: 'medium' })} + > + + ), }, { field: 'low', name: i18n.STATUS_LOW_LABEL, - render: (count: number) => ( + render: (count: number, { hostName }) => ( - + handleClick({ hostName, severity: 'low' })}> + + ), }, diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/mock_data.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/mock_data.ts index d46d1b5401a9e..3c53951dd657c 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/mock_data.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/mock_data.ts @@ -9,6 +9,9 @@ import { buildVulnerableHostAggregationQuery } from './use_host_alerts_items'; export const mockVulnerableHostsBySeverityResult = { aggregations: { + host_count: { + value: 4, + }, hostsBySeverity: { buckets: [ { @@ -119,6 +122,7 @@ export const mockQuery = () => ({ query: buildVulnerableHostAggregationQuery({ from: '2020-07-07T08:20:18.966Z', to: '2020-07-08T08:20:18.966Z', + currentPage: 0, }), indexName: 'signal-alerts', skip: false, diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/use_host_alerts_items.test.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/use_host_alerts_items.test.ts index 42568a390f686..d38f0a4bfaa77 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/use_host_alerts_items.test.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/use_host_alerts_items.test.ts @@ -74,6 +74,10 @@ describe('useVulnerableHostsCounters', () => { items: [], isLoading: false, updatedAt: dateNow, + pagination: expect.objectContaining({ + currentPage: 0, + pageCount: 0, + }), }); expect(mockUseQueryAlerts).toBeCalledWith(mockQuery()); @@ -91,6 +95,10 @@ describe('useVulnerableHostsCounters', () => { items: parsedVulnerableHostsAlertsResult, isLoading: false, updatedAt: dateNow, + pagination: expect.objectContaining({ + currentPage: 0, + pageCount: 1, + }), }); }); @@ -110,6 +118,10 @@ describe('useVulnerableHostsCounters', () => { items: parsedVulnerableHostsAlertsResult, isLoading: false, updatedAt: newDateNow, + pagination: expect.objectContaining({ + currentPage: 0, + pageCount: 1, + }), }); }); @@ -122,6 +134,10 @@ describe('useVulnerableHostsCounters', () => { items: [], isLoading: false, updatedAt: dateNow, + pagination: expect.objectContaining({ + currentPage: 0, + pageCount: 0, + }), }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/use_host_alerts_items.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/use_host_alerts_items.ts index a01cd709b44f6..62fcc4580b253 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/use_host_alerts_items.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/use_host_alerts_items.ts @@ -11,8 +11,13 @@ import { useQueryInspector } from '../../../../common/components/page/manage_que import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { GenericBuckets } from '../../../../../common/search_strategy'; import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query'; +import { getPageCount, ITEMS_PER_PAGE } from '../utils'; const HOSTS_BY_SEVERITY_AGG = 'hostsBySeverity'; +const defaultPagination = { + pageCount: 0, + currentPage: 0, +}; interface TimeRange { from: string; @@ -24,6 +29,7 @@ export interface UseHostAlertsItemsProps { queryId: string; signalIndexName: string | null; } + export interface HostAlertsItem { hostName: string; totalAlerts: number; @@ -37,11 +43,18 @@ export type UseHostAlertsItems = (props: UseHostAlertsItemsProps) => { items: HostAlertsItem[]; isLoading: boolean; updatedAt: number; + pagination: Pagination & { setPage: (pageNumber: number) => void }; }; +interface Pagination { + pageCount: number; + currentPage: number; +} + export const useHostAlertsItems: UseHostAlertsItems = ({ skip, queryId, signalIndexName }) => { const [updatedAt, setUpdatedAt] = useState(Date.now()); const [items, setItems] = useState([]); + const [paginationData, setPaginationData] = useState(defaultPagination); const { to, from, setQuery: setGlobalQuery, deleteQuery } = useGlobalTime(); @@ -53,20 +66,31 @@ export const useHostAlertsItems: UseHostAlertsItems = ({ skip, queryId, signalIn loading, refetch: refetchQuery, } = useQueryAlerts<{}, AlertCountersBySeverityAndHostAggregation>({ - query: buildVulnerableHostAggregationQuery({ from, to }), + query: buildVulnerableHostAggregationQuery({ + from, + to, + currentPage: paginationData.currentPage, + }), indexName: signalIndexName, skip, }); useEffect(() => { - setQuery(buildVulnerableHostAggregationQuery({ from, to })); - }, [setQuery, from, to]); + setQuery( + buildVulnerableHostAggregationQuery({ from, to, currentPage: paginationData.currentPage }) + ); + }, [setQuery, from, to, paginationData.currentPage]); useEffect(() => { if (data == null || !data.aggregations) { setItems([]); } else { setItems(parseHostsData(data.aggregations)); + + setPaginationData((p) => ({ + ...p, + pageCount: getPageCount(data.aggregations?.host_count.value), + })); } setUpdatedAt(Date.now()); }, [data]); @@ -77,6 +101,13 @@ export const useHostAlertsItems: UseHostAlertsItems = ({ skip, queryId, signalIn } }, [skip, refetchQuery]); + const setPage = (pageNumber: number) => { + setPaginationData((p) => ({ + ...p, + currentPage: pageNumber, + })); + }; + useQueryInspector({ deleteQuery, inspect: { @@ -88,87 +119,112 @@ export const useHostAlertsItems: UseHostAlertsItems = ({ skip, queryId, signalIn queryId, loading, }); - return { items, isLoading: loading, updatedAt }; -}; -export const buildVulnerableHostAggregationQuery = ({ from, to }: TimeRange) => ({ - query: { - bool: { - filter: [ - { - term: { - 'kibana.alert.workflow_status': 'open', - }, - }, - { - range: { - '@timestamp': { - gte: from, - lte: to, - }, - }, - }, - ], + return { + items, + isLoading: loading, + updatedAt, + pagination: { + ...paginationData, + setPage, }, - }, - size: 0, - aggs: { - [HOSTS_BY_SEVERITY_AGG]: { - terms: { - field: 'host.name', - order: [ - { - 'critical.doc_count': 'desc', - }, - { - 'high.doc_count': 'desc', - }, + }; +}; + +export const buildVulnerableHostAggregationQuery = ({ + from, + to, + currentPage, +}: TimeRange & { currentPage: number }) => { + const fromValue = ITEMS_PER_PAGE * currentPage; + + return { + query: { + bool: { + filter: [ { - 'medium.doc_count': 'desc', + term: { + 'kibana.alert.workflow_status': 'open', + }, }, { - 'low.doc_count': 'desc', + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, }, ], - size: 4, }, - aggs: { - critical: { - filter: { - term: { - 'kibana.alert.severity': 'critical', + }, + size: 0, + aggs: { + host_count: { cardinality: { field: 'host.name' } }, + [HOSTS_BY_SEVERITY_AGG]: { + terms: { + size: 100, + field: 'host.name', + order: [ + { + 'critical.doc_count': 'desc', }, - }, + { + 'high.doc_count': 'desc', + }, + { + 'medium.doc_count': 'desc', + }, + { + 'low.doc_count': 'desc', + }, + ], }, - high: { - filter: { - term: { - 'kibana.alert.severity': 'high', + aggs: { + critical: { + filter: { + term: { + 'kibana.alert.severity': 'critical', + }, }, }, - }, - medium: { - filter: { - term: { - 'kibana.alert.severity': 'medium', + high: { + filter: { + term: { + 'kibana.alert.severity': 'high', + }, }, }, - }, - low: { - filter: { - term: { - 'kibana.alert.severity': 'low', + medium: { + filter: { + term: { + 'kibana.alert.severity': 'medium', + }, + }, + }, + low: { + filter: { + term: { + 'kibana.alert.severity': 'low', + }, + }, + }, + bucketOfPagination: { + bucket_sort: { + from: fromValue, + size: 4, }, }, }, }, }, - }, -}); + }; +}; interface SeverityContainer { doc_count: number; } + interface AlertBySeverityBucketData extends GenericBuckets { low: SeverityContainer; medium: SeverityContainer; @@ -180,6 +236,7 @@ interface AlertCountersBySeverityAndHostAggregation { [HOSTS_BY_SEVERITY_AGG]: { buckets: AlertBySeverityBucketData[]; }; + host_count: { value: number }; } function parseHostsData( @@ -188,16 +245,15 @@ function parseHostsData( const buckets = rawAggregation?.[HOSTS_BY_SEVERITY_AGG].buckets ?? []; return buckets.reduce((accumalatedAlertsByHost, currentHost) => { - return [ - ...accumalatedAlertsByHost, - { - hostName: currentHost.key || '—', - totalAlerts: currentHost.doc_count, - low: currentHost.low.doc_count, - medium: currentHost.medium.doc_count, - high: currentHost.high.doc_count, - critical: currentHost.critical.doc_count, - }, - ]; + accumalatedAlertsByHost.push({ + hostName: currentHost.key || '—', + totalAlerts: currentHost.doc_count, + low: currentHost.low.doc_count, + medium: currentHost.medium.doc_count, + high: currentHost.high.doc_count, + critical: currentHost.critical.doc_count, + }); + + return accumalatedAlertsByHost; }, []); } diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.test.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.test.tsx index dafd1ca965a3f..16a13c426b550 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.test.tsx @@ -91,7 +91,7 @@ describe('RuleAlertsTable', () => { ); - expect(result.getByText('Updated now')).toBeInTheDocument(); + expect(result.getByText(/Updated/)).toBeInTheDocument(); }); it('should render the table columns', () => { diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx index 550e04753e886..470f9901a05c9 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx @@ -22,7 +22,7 @@ import { FormattedRelative } from '@kbn/i18n-react'; import { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import { HeaderSection } from '../../../../common/components/header_section'; -import { LastUpdatedAt, SEVERITY_COLOR } from '../util'; +import { LastUpdatedAt, SEVERITY_COLOR } from '../utils'; import * as i18n from '../translations'; import { useRuleAlertsItems, RuleAlertsItem } from './use_rule_alerts_items'; import { useNavigation, NavigateTo, GetAppUrl } from '../../../../common/lib/kibana'; @@ -31,6 +31,7 @@ import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { HoverVisibilityContainer } from '../../../../common/components/hover_visibility_container'; import { BUTTON_CLASS as INPECT_BUTTON_CLASS } from '../../../../common/components/inspect'; import { FormattedCount } from '../../../../common/components/formatted_number'; +import { useNavigateToTimeline } from '../hooks/use_navigate_to_timeline'; export interface RuleAlertsTableProps { signalIndexName: string | null; @@ -39,12 +40,13 @@ export interface RuleAlertsTableProps { export type GetTableColumns = (params: { getAppUrl: GetAppUrl; navigateTo: NavigateTo; + openRuleInTimeline: (ruleName: string) => void; }) => Array>; const DETECTION_RESPONSE_RULE_ALERTS_QUERY_ID = 'detection-response-rule-alerts-severity-table' as const; -export const getTableColumns: GetTableColumns = ({ getAppUrl, navigateTo }) => [ +export const getTableColumns: GetTableColumns = ({ getAppUrl, navigateTo, openRuleInTimeline }) => [ { field: 'name', name: i18n.RULE_ALERTS_COLUMN_RULE_NAME, @@ -79,7 +81,11 @@ export const getTableColumns: GetTableColumns = ({ getAppUrl, navigateTo }) => [ field: 'alert_count', name: i18n.RULE_ALERTS_COLUMN_ALERT_COUNT, 'data-test-subj': 'severityRuleAlertsTable-alertCount', - render: (alertCount: number) => , + render: (alertCount: number, { name }) => ( + openRuleInTimeline(name)}> + + + ), }, { field: 'severity', @@ -100,13 +106,15 @@ export const RuleAlertsTable = React.memo(({ signalIndexNa skip: !toggleStatus, }); + const { openRuleInTimeline } = useNavigateToTimeline(); + const navigateToAlerts = useCallback(() => { navigateTo({ deepLinkId: SecurityPageName.alerts }); }, [navigateTo]); const columns = useMemo( - () => getTableColumns({ getAppUrl, navigateTo }), - [getAppUrl, navigateTo] + () => getTableColumns({ getAppUrl, navigateTo, openRuleInTimeline }), + [getAppUrl, navigateTo, openRuleInTimeline] ); return ( diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/translations.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/translations.ts index d013ab258631a..a9773d09ba461 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/translations.ts @@ -92,14 +92,14 @@ export const RULE_ALERTS_SECTION_TITLE = i18n.translate( export const HOST_ALERTS_SECTION_TITLE = i18n.translate( 'xpack.securitySolution.detectionResponse.hostAlertsSectionTitle', { - defaultMessage: 'Vulnerable hosts by severity', + defaultMessage: 'Hosts by alert severity', } ); export const USER_ALERTS_SECTION_TITLE = i18n.translate( 'xpack.securitySolution.detectionResponse.userAlertsSectionTitle', { - defaultMessage: 'Vulnerable users by severity', + defaultMessage: 'Users by alert severity', } ); @@ -243,3 +243,17 @@ export const ERROR_MESSAGE_CASES = i18n.translate( defaultMessage: 'Error fetching case data', } ); + +export const HOST_TOOLTIP = i18n.translate( + 'xpack.securitySolution.detectionResponse.hostSectionTooltip', + { + defaultMessage: 'Maximum of 100 hosts. Please consult Alerts page for further information.', + } +); + +export const USER_TOOLTIP = i18n.translate( + 'xpack.securitySolution.detectionResponse.userSectionTooltip', + { + defaultMessage: 'Maximum of 100 users. Please consult Alerts page for further information.', + } +); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/mock_data.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/mock_data.ts index e10bf1b05f032..a5339de96e16c 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/mock_data.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/mock_data.ts @@ -9,6 +9,9 @@ import { buildVulnerableUserAggregationQuery } from './use_user_alerts_items'; export const mockVulnerableUsersBySeverityResult = { aggregations: { + user_count: { + value: 4, + }, usersBySeverity: { buckets: [ { @@ -119,6 +122,7 @@ export const mockQuery = () => ({ query: buildVulnerableUserAggregationQuery({ from: '2020-07-07T08:20:18.966Z', to: '2020-07-08T08:20:18.966Z', + currentPage: 0, }), indexName: 'signal-alerts', skip: false, diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/use_user_alerts_items.test.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/use_user_alerts_items.test.ts index 22ade5d3341c7..3aef486805322 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/use_user_alerts_items.test.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/use_user_alerts_items.test.ts @@ -73,6 +73,10 @@ describe('useUserAlertsItems', () => { items: [], isLoading: false, updatedAt: dateNow, + pagination: expect.objectContaining({ + currentPage: 0, + pageCount: 0, + }), }); expect(mockUseQueryAlerts).toBeCalledWith(mockQuery()); @@ -90,6 +94,10 @@ describe('useUserAlertsItems', () => { items: parsedVulnerableUserAlertsResult, isLoading: false, updatedAt: dateNow, + pagination: expect.objectContaining({ + currentPage: 0, + pageCount: 1, + }), }); }); @@ -109,6 +117,10 @@ describe('useUserAlertsItems', () => { items: parsedVulnerableUserAlertsResult, isLoading: false, updatedAt: newDateNow, + pagination: expect.objectContaining({ + currentPage: 0, + pageCount: 1, + }), }); }); @@ -121,6 +133,10 @@ describe('useUserAlertsItems', () => { items: [], isLoading: false, updatedAt: dateNow, + pagination: expect.objectContaining({ + currentPage: 0, + pageCount: 0, + }), }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/use_user_alerts_items.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/use_user_alerts_items.ts index 3e2778f82cde4..5c0280f093cbe 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/use_user_alerts_items.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/use_user_alerts_items.ts @@ -11,8 +11,13 @@ import { useQueryInspector } from '../../../../common/components/page/manage_que import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { GenericBuckets } from '../../../../../common/search_strategy'; import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query'; +import { getPageCount, ITEMS_PER_PAGE } from '../utils'; const USERS_BY_SEVERITY_AGG = 'usersBySeverity'; +const defaultPagination = { + pageCount: 0, + currentPage: 0, +}; interface TimeRange { from: string; @@ -38,11 +43,18 @@ export type UseUserAlertsItems = (props: UseUserAlertsItemsProps) => { items: UserAlertsItem[]; isLoading: boolean; updatedAt: number; + pagination: Pagination & { setPage: (pageNumber: number) => void }; }; +interface Pagination { + pageCount: number; + currentPage: number; +} + export const useUserAlertsItems: UseUserAlertsItems = ({ skip, queryId, signalIndexName }) => { const [updatedAt, setUpdatedAt] = useState(Date.now()); const [items, setItems] = useState([]); + const [paginationData, setPaginationData] = useState(defaultPagination); const { to, from, setQuery: setGlobalQuery, deleteQuery } = useGlobalTime(); @@ -54,20 +66,31 @@ export const useUserAlertsItems: UseUserAlertsItems = ({ skip, queryId, signalIn response, refetch: refetchQuery, } = useQueryAlerts<{}, AlertCountersBySeverityAggregation>({ - query: buildVulnerableUserAggregationQuery({ from, to }), + query: buildVulnerableUserAggregationQuery({ + from, + to, + currentPage: paginationData.currentPage, + }), indexName: signalIndexName, skip, }); useEffect(() => { - setQuery(buildVulnerableUserAggregationQuery({ from, to })); - }, [setQuery, from, to]); + setQuery( + buildVulnerableUserAggregationQuery({ from, to, currentPage: paginationData.currentPage }) + ); + }, [setQuery, from, to, paginationData.currentPage]); useEffect(() => { if (data == null || !data.aggregations) { setItems([]); } else { setItems(parseUsersData(data.aggregations)); + + setPaginationData((p) => ({ + ...p, + pageCount: getPageCount(data.aggregations?.user_count.value), + })); } setUpdatedAt(Date.now()); }, [data]); @@ -78,6 +101,13 @@ export const useUserAlertsItems: UseUserAlertsItems = ({ skip, queryId, signalIn } }, [skip, refetchQuery]); + const setPage = (pageNumber: number) => { + setPaginationData((p) => ({ + ...p, + currentPage: pageNumber, + })); + }; + useQueryInspector({ deleteQuery, inspect: { @@ -89,87 +119,112 @@ export const useUserAlertsItems: UseUserAlertsItems = ({ skip, queryId, signalIn queryId, loading, }); - return { items, isLoading: loading, updatedAt }; -}; -export const buildVulnerableUserAggregationQuery = ({ from, to }: TimeRange) => ({ - query: { - bool: { - filter: [ - { - term: { - 'kibana.alert.workflow_status': 'open', - }, - }, - { - range: { - '@timestamp': { - gte: from, - lte: to, - }, - }, - }, - ], + return { + items, + isLoading: loading, + updatedAt, + pagination: { + ...paginationData, + setPage, }, - }, - size: 0, - aggs: { - [USERS_BY_SEVERITY_AGG]: { - terms: { - field: 'user.name', - order: [ - { - 'critical.doc_count': 'desc', - }, - { - 'high.doc_count': 'desc', - }, + }; +}; + +export const buildVulnerableUserAggregationQuery = ({ + from, + to, + currentPage, +}: TimeRange & { currentPage: number }) => { + const fromValue = ITEMS_PER_PAGE * currentPage; + + return { + query: { + bool: { + filter: [ { - 'medium.doc_count': 'desc', + term: { + 'kibana.alert.workflow_status': 'open', + }, }, { - 'low.doc_count': 'desc', + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, }, ], - size: 4, }, - aggs: { - critical: { - filter: { - term: { - 'kibana.alert.severity': 'critical', + }, + size: 0, + aggs: { + user_count: { cardinality: { field: 'user.name' } }, + [USERS_BY_SEVERITY_AGG]: { + terms: { + size: 100, + field: 'user.name', + order: [ + { + 'critical.doc_count': 'desc', }, - }, + { + 'high.doc_count': 'desc', + }, + { + 'medium.doc_count': 'desc', + }, + { + 'low.doc_count': 'desc', + }, + ], }, - high: { - filter: { - term: { - 'kibana.alert.severity': 'high', + aggs: { + critical: { + filter: { + term: { + 'kibana.alert.severity': 'critical', + }, }, }, - }, - medium: { - filter: { - term: { - 'kibana.alert.severity': 'medium', + high: { + filter: { + term: { + 'kibana.alert.severity': 'high', + }, }, }, - }, - low: { - filter: { - term: { - 'kibana.alert.severity': 'low', + medium: { + filter: { + term: { + 'kibana.alert.severity': 'medium', + }, + }, + }, + low: { + filter: { + term: { + 'kibana.alert.severity': 'low', + }, + }, + }, + bucketOfPagination: { + bucket_sort: { + from: fromValue, + size: 4, }, }, }, }, }, - }, -}); + }; +}; interface SeverityContainer { doc_count: number; } + interface AlertBySeverityBucketData extends GenericBuckets { low: SeverityContainer; medium: SeverityContainer; @@ -181,22 +236,22 @@ interface AlertCountersBySeverityAggregation { [USERS_BY_SEVERITY_AGG]: { buckets: AlertBySeverityBucketData[]; }; + user_count: { value: number }; } function parseUsersData(rawAggregation: AlertCountersBySeverityAggregation): UserAlertsItem[] { const buckets = rawAggregation?.[USERS_BY_SEVERITY_AGG].buckets ?? []; return buckets.reduce((accumalatedAlertsByUser, currentUser) => { - return [ - ...accumalatedAlertsByUser, - { - userName: currentUser.key || '—', - totalAlerts: currentUser.doc_count, - low: currentUser.low.doc_count, - medium: currentUser.medium.doc_count, - high: currentUser.high.doc_count, - critical: currentUser.critical.doc_count, - }, - ]; + accumalatedAlertsByUser.push({ + userName: currentUser.key || '—', + totalAlerts: currentUser.doc_count, + low: currentUser.low.doc_count, + medium: currentUser.medium.doc_count, + high: currentUser.high.doc_count, + critical: currentUser.critical.doc_count, + }); + + return accumalatedAlertsByUser; }, []); } diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.test.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.test.tsx index 8e0b7656fda8e..a7c48f5092a39 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; import { parsedVulnerableUserAlertsResult } from './mock_data'; @@ -30,6 +30,11 @@ const defaultUseUserAlertsItemsReturn: UseUserAlertsItemsReturn = { items: [], isLoading: false, updatedAt: Date.now(), + pagination: { + currentPage: 0, + pageCount: 0, + setPage: () => null, + }, }; const mockUseUserAlertsItems = jest.fn(() => defaultUseUserAlertsItemsReturn); const mockUseUserAlertsItemsReturn = (overrides: Partial) => { @@ -47,34 +52,33 @@ const renderComponent = () => ); -// FLAKY: https://github.com/elastic/kibana/issues/132360 -describe.skip('UserAlertsTable', () => { +describe('UserAlertsTable', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should render empty table', () => { - const { getByText, getByTestId } = renderComponent(); + const { getByText, queryByTestId } = renderComponent(); - expect(getByTestId('severityUserAlertsPanel')).toBeInTheDocument(); + expect(queryByTestId('severityUserAlertsPanel')).toBeInTheDocument(); + expect(queryByTestId('userTablePaginator')).not.toBeInTheDocument(); expect(getByText('No alerts to display')).toBeInTheDocument(); - expect(getByTestId('severityUserAlertsButton')).toBeInTheDocument(); }); it('should render a loading table', () => { mockUseUserAlertsItemsReturn({ isLoading: true }); - const { getByText, getByTestId } = renderComponent(); + const { getByText, queryByTestId } = renderComponent(); expect(getByText('Updating...')).toBeInTheDocument(); - expect(getByTestId('severityUserAlertsButton')).toBeInTheDocument(); - expect(getByTestId('severityUserAlertsTable')).toHaveClass('euiBasicTable-loading'); + expect(queryByTestId('severityUserAlertsTable')).toHaveClass('euiBasicTable-loading'); + expect(queryByTestId('userTablePaginator')).not.toBeInTheDocument(); }); it('should render the updated at subtitle', () => { mockUseUserAlertsItemsReturn({ isLoading: false }); const { getByText } = renderComponent(); - expect(getByText('Updated now')).toBeInTheDocument(); + expect(getByText(/Updated/)).toBeInTheDocument(); }); it('should render the table columns', () => { @@ -92,13 +96,32 @@ describe.skip('UserAlertsTable', () => { it('should render the table items', () => { mockUseUserAlertsItemsReturn({ items: [parsedVulnerableUserAlertsResult[0]] }); - const { getByTestId } = renderComponent(); - - expect(getByTestId('userSeverityAlertsTable-userName')).toHaveTextContent('crffn20qcs'); - expect(getByTestId('userSeverityAlertsTable-totalAlerts')).toHaveTextContent('4'); - expect(getByTestId('userSeverityAlertsTable-critical')).toHaveTextContent('4'); - expect(getByTestId('userSeverityAlertsTable-high')).toHaveTextContent('1'); - expect(getByTestId('userSeverityAlertsTable-medium')).toHaveTextContent('1'); - expect(getByTestId('userSeverityAlertsTable-low')).toHaveTextContent('1'); + const { queryByTestId } = renderComponent(); + + expect(queryByTestId('userSeverityAlertsTable-userName')).toHaveTextContent('crffn20qcs'); + expect(queryByTestId('userSeverityAlertsTable-totalAlerts')).toHaveTextContent('4'); + expect(queryByTestId('userSeverityAlertsTable-critical')).toHaveTextContent('4'); + expect(queryByTestId('userSeverityAlertsTable-high')).toHaveTextContent('1'); + expect(queryByTestId('userSeverityAlertsTable-medium')).toHaveTextContent('1'); + expect(queryByTestId('userSeverityAlertsTable-low')).toHaveTextContent('1'); + expect(queryByTestId('userTablePaginator')).not.toBeInTheDocument(); + }); + + it('should render the paginator if more than 4 results', () => { + const mockSetPage = jest.fn(); + + mockUseUserAlertsItemsReturn({ + pagination: { + currentPage: 1, + pageCount: 3, + setPage: mockSetPage, + }, + }); + const { queryByTestId, getByText } = renderComponent(); + const page3 = getByText('3'); + expect(queryByTestId('userTablePaginator')).toBeInTheDocument(); + + fireEvent.click(page3); + expect(mockSetPage).toHaveBeenCalledWith(2); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx index 0416bb48e13e1..80104244fedf0 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx @@ -5,70 +5,56 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; +import React, { useMemo } from 'react'; import { EuiBasicTable, EuiBasicTableColumn, - EuiButton, EuiEmptyPrompt, EuiHealth, + EuiLink, EuiPanel, EuiSpacer, + EuiTablePagination, } from '@elastic/eui'; -import { SecurityPageName } from '../../../../app/types'; import { FormattedCount } from '../../../../common/components/formatted_number'; import { HeaderSection } from '../../../../common/components/header_section'; import { HoverVisibilityContainer } from '../../../../common/components/hover_visibility_container'; import { BUTTON_CLASS as INPECT_BUTTON_CLASS } from '../../../../common/components/inspect'; import { UserDetailsLink } from '../../../../common/components/links'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; -import { useNavigation, NavigateTo, GetAppUrl } from '../../../../common/lib/kibana'; +import { useNavigateToTimeline } from '../hooks/use_navigate_to_timeline'; import * as i18n from '../translations'; -import { LastUpdatedAt, SEVERITY_COLOR } from '../util'; +import { ITEMS_PER_PAGE, LastUpdatedAt, SEVERITY_COLOR } from '../utils'; import { UserAlertsItem, useUserAlertsItems } from './use_user_alerts_items'; -export interface UserAlertsTableProps { +interface UserAlertsTableProps { signalIndexName: string | null; } -type GetTableColumns = (params: { - getAppUrl: GetAppUrl; - navigateTo: NavigateTo; -}) => Array>; +type GetTableColumns = ( + handleClick: (params: { userName: string; severity?: string }) => void +) => Array>; const DETECTION_RESPONSE_USER_SEVERITY_QUERY_ID = 'vulnerableUsersBySeverityQuery'; -// To Do remove this styled component once togglequery is updated: #131405 -const StyledEuiPanel = styled(EuiPanel)` - height: fit-content; -`; - export const UserAlertsTable = React.memo(({ signalIndexName }: UserAlertsTableProps) => { - const { getAppUrl, navigateTo } = useNavigation(); + const { openUserInTimeline } = useNavigateToTimeline(); const { toggleStatus, setToggleStatus } = useQueryToggle( DETECTION_RESPONSE_USER_SEVERITY_QUERY_ID ); - const { items, isLoading, updatedAt } = useUserAlertsItems({ + const { items, isLoading, updatedAt, pagination } = useUserAlertsItems({ skip: !toggleStatus, queryId: DETECTION_RESPONSE_USER_SEVERITY_QUERY_ID, signalIndexName, }); - const navigateToAlerts = useCallback(() => { - navigateTo({ deepLinkId: SecurityPageName.users }); - }, [navigateTo]); - - const columns = useMemo( - () => getTableColumns({ getAppUrl, navigateTo }), - [getAppUrl, navigateTo] - ); + const columns = useMemo(() => getTableColumns(openUserInTimeline), [openUserInTimeline]); return ( - + } + tooltip={i18n.USER_TOOLTIP} /> - {toggleStatus && ( <> - - {i18n.VIEW_ALL_USER_ALERTS} - + {pagination.pageCount > 1 && ( + + )} )} - + ); }); UserAlertsTable.displayName = 'UserAlertsTable'; -const getTableColumns: GetTableColumns = ({ getAppUrl, navigateTo }) => [ +const getTableColumns: GetTableColumns = (handleClick) => [ { field: 'userName', name: i18n.USER_ALERTS_USERNAME_COLUMN, @@ -115,41 +108,59 @@ const getTableColumns: GetTableColumns = ({ getAppUrl, navigateTo }) => [ field: 'totalAlerts', name: i18n.ALERTS_TEXT, 'data-test-subj': 'userSeverityAlertsTable-totalAlerts', - render: (totalAlerts: number) => , + render: (totalAlerts: number, { userName }) => ( + handleClick({ userName })}> + + + ), }, { field: 'critical', name: i18n.STATUS_CRITICAL_LABEL, - render: (count: number) => ( + render: (count: number, { userName }) => ( - + handleClick({ userName, severity: 'critical' })} + > + + ), }, { field: 'high', name: i18n.STATUS_HIGH_LABEL, - render: (count: number) => ( + render: (count: number, { userName }) => ( - + handleClick({ userName, severity: 'high' })}> + + ), }, { field: 'medium', name: i18n.STATUS_MEDIUM_LABEL, - render: (count: number) => ( + render: (count: number, { userName }) => ( - {count} + handleClick({ userName, severity: 'medium' })} + > + + ), }, { field: 'low', name: i18n.STATUS_LOW_LABEL, - render: (count: number) => ( + render: (count: number, { userName }) => ( - + handleClick({ userName, severity: 'low' })}> + + ), }, diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/util.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/util.tsx deleted file mode 100644 index 4ceba66773397..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/util.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { FormattedRelative } from '@kbn/i18n-react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import * as i18n from './translations'; - -export const SEVERITY_COLOR = { - critical: '#EF6550', - high: '#EE9266', - medium: '#F3B689', - low: '#F8D9B2', -} as const; - -export interface LastUpdatedAtProps { - updatedAt: number; - isUpdating: boolean; -} -export const LastUpdatedAt: React.FC = ({ isUpdating, updatedAt }) => ( - - {isUpdating ? ( - {i18n.UPDATING} - ) : ( - - <>{i18n.UPDATED} - - - )} - -); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/utils.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/utils.tsx index 4ceba66773397..0a602b21f676f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/utils.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/utils.tsx @@ -6,21 +6,27 @@ */ import React from 'react'; -import { FormattedRelative } from '@kbn/i18n-react'; + import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedRelative } from '@kbn/i18n-react'; + import * as i18n from './translations'; export const SEVERITY_COLOR = { - critical: '#EF6550', - high: '#EE9266', - medium: '#F3B689', - low: '#F8D9B2', + critical: '#E7664C', + high: '#DA8B45', + medium: '#D6BF57', + low: '#54B399', } as const; +export const ITEMS_PER_PAGE = 4; +const MAX_ALLOWED_RESULTS = 100; + export interface LastUpdatedAtProps { updatedAt: number; isUpdating: boolean; } + export const LastUpdatedAt: React.FC = ({ isUpdating, updatedAt }) => ( {isUpdating ? ( @@ -37,3 +43,10 @@ export const LastUpdatedAt: React.FC = ({ isUpdating, update )} ); + +/** + * While there could be more than 100 hosts or users we only want to show 25 pages of results, + * and the host count cardinality result will always be the total count + * */ +export const getPageCount = (count: number = 0) => + Math.ceil(Math.min(count || 0, MAX_ALLOWED_RESULTS) / ITEMS_PER_PAGE); diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts index 96728a07432fd..1af23099dca01 100644 --- a/x-pack/plugins/timelines/common/index.ts +++ b/x-pack/plugins/timelines/common/index.ts @@ -37,6 +37,7 @@ export type { RowRenderer, SetEventsDeleted, SetEventsLoading, + TimelineType, } from './types'; export { IS_OPERATOR, EXISTS_OPERATOR, DataProviderType, TimelineId } from './types'; From dc9f2732a106c837140a5b74f5698076f3230529 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 23 May 2022 20:01:56 +0200 Subject: [PATCH 105/120] Add csp.disableUnsafeEval config option to remove the unsafe-eval CSP (#124484) Adds a new experimental Kibana setting called `csp.disableUnsafeEval` which will default to `false`. When set to `true`, it will remove `unsafe-eval` from our CSP. Also introduces a new module called `@kbn/handlebars` which is a replacement for the official `handlebars` module used in the frontend. This new module is necessary in order to avoid calling `eval`/`new Function` from within `handlebars` which is not allowed once `unsafe-eval` is removed from our CSP. The `@kbn/handlebars` module is simply an extension of the main `handlebars` module which adds a new compile function called `compileAST` (as an alternative to the regular `compile` function). This new function will not use code-generation from strings to compile the template but will instead generate an AST and return a render function with the same API as the function returned by the regular `compile` function. This is a little bit slower method, but since this is only meant to be used client-side, the slowdown should not be an issue. The following limitations exists when using `@kbn/handlebars`: The Inline partials handlebars template feature is not supported. Only the following compile options will be supported: - `knownHelpers` - `knownHelpersOnly` - `strict` - `assumeObjects` - `noEscape` - `data` Only the following runtime options will be supported: - `helpers` - `blockParams` - `data` Closes #36311 --- .eslintrc.js | 86 ++ .github/CODEOWNERS | 1 + api_docs/deprecations_by_api.mdx | 55 +- api_docs/deprecations_by_plugin.mdx | 88 +- api_docs/deprecations_by_team.mdx | 48 +- api_docs/kbn_handlebars.devdocs.json | 153 +++ api_docs/kbn_handlebars.mdx | 33 + api_docs/plugin_directory.mdx | 126 +- docs/setup/settings.asciidoc | 9 +- package.json | 2 + packages/BUILD.bazel | 2 + packages/kbn-handlebars/.gitignore | 1 + packages/kbn-handlebars/.patches/basic.patch | 612 +++++++++ packages/kbn-handlebars/.patches/blocks.patch | 461 +++++++ .../kbn-handlebars/.patches/builtins.patch | 872 +++++++++++++ .../kbn-handlebars/.patches/compiler.patch | 272 ++++ packages/kbn-handlebars/.patches/data.patch | 273 ++++ .../kbn-handlebars/.patches/helpers.patch | 1096 +++++++++++++++++ .../kbn-handlebars/.patches/regressions.patch | 518 ++++++++ .../kbn-handlebars/.patches/security.patch | 443 +++++++ packages/kbn-handlebars/.patches/strict.patch | 180 +++ .../.patches/subexpressions.patch | 318 +++++ packages/kbn-handlebars/.patches/utils.patch | 109 ++ .../.patches/whitespace-control.patch | 187 +++ packages/kbn-handlebars/BUILD.bazel | 115 ++ packages/kbn-handlebars/LICENSE | 29 + packages/kbn-handlebars/README.md | 192 +++ packages/kbn-handlebars/jest.config.js | 10 + packages/kbn-handlebars/package.json | 9 + .../scripts/check_for_test_changes.sh | 33 + packages/kbn-handlebars/scripts/print_ast.js | 45 + .../scripts/update_test_patches.sh | 24 + .../kbn-handlebars/src/__jest__/test_bench.ts | 170 +++ .../src/__snapshots__/index.test.ts.snap | 105 ++ packages/kbn-handlebars/src/index.test.ts | 93 ++ packages/kbn-handlebars/src/index.ts | 710 +++++++++++ .../src/upstream/index.basic.test.ts | 481 ++++++++ .../src/upstream/index.blocks.test.ts | 198 +++ .../src/upstream/index.builtins.test.ts | 649 ++++++++++ .../src/upstream/index.compiler.test.ts | 89 ++ .../src/upstream/index.data.test.ts | 255 ++++ .../src/upstream/index.helpers.test.ts | 954 ++++++++++++++ .../src/upstream/index.regressions.test.ts | 279 +++++ .../src/upstream/index.security.test.ts | 132 ++ .../src/upstream/index.strict.test.ts | 164 +++ .../src/upstream/index.subexpressions.test.ts | 214 ++++ .../src/upstream/index.utils.test.ts | 24 + .../upstream/index.whitespace-control.test.ts | 80 ++ packages/kbn-handlebars/tsconfig.json | 19 + src/core/server/csp/config.ts | 7 + src/core/server/csp/csp_config.test.mocks.ts | 25 + src/core/server/csp/csp_config.test.ts | 75 +- src/core/server/csp/csp_directives.test.ts | 8 +- src/core/server/csp/csp_directives.ts | 5 +- .../http_resources_service.test.ts | 12 +- .../http_resources_service.test.ts | 15 +- .../resources/base/bin/kibana-docker | 1 + src/dev/precommit_hook/casing_check_config.js | 4 + .../collectors/csp/csp_collector.test.ts | 1 + .../components/lib/replace_vars.ts | 19 +- .../components/lib/tick_formatter.js | 4 +- test/api_integration/apis/general/csp.js | 36 - test/api_integration/apis/general/index.js | 1 - test/common/config.js | 1 + .../functions/browser/markdown.ts | 3 +- .../plugins/canvas/common/lib/handlebars.js | 2 +- .../authentication_service.test.ts | 19 +- .../check_steps/use_expanded_row.test.tsx | 4 +- .../drilldowns/url_drilldown/handlebars.ts | 8 +- .../drilldowns/url_drilldown/url_template.ts | 13 +- .../tests/anonymous/login.ts | 4 +- .../tests/kerberos/kerberos_login.ts | 4 +- .../login_selector/basic_functionality.ts | 16 +- .../oidc/authorization_code_flow/oidc_auth.ts | 8 +- .../tests/oidc/implicit_flow/oidc_auth.ts | 12 +- .../tests/pki/pki_auth.ts | 4 +- .../tests/saml/saml_login.ts | 8 +- yarn.lock | 8 + 78 files changed, 11036 insertions(+), 309 deletions(-) create mode 100644 api_docs/kbn_handlebars.devdocs.json create mode 100644 api_docs/kbn_handlebars.mdx create mode 100644 packages/kbn-handlebars/.gitignore create mode 100644 packages/kbn-handlebars/.patches/basic.patch create mode 100644 packages/kbn-handlebars/.patches/blocks.patch create mode 100644 packages/kbn-handlebars/.patches/builtins.patch create mode 100644 packages/kbn-handlebars/.patches/compiler.patch create mode 100644 packages/kbn-handlebars/.patches/data.patch create mode 100644 packages/kbn-handlebars/.patches/helpers.patch create mode 100644 packages/kbn-handlebars/.patches/regressions.patch create mode 100644 packages/kbn-handlebars/.patches/security.patch create mode 100644 packages/kbn-handlebars/.patches/strict.patch create mode 100644 packages/kbn-handlebars/.patches/subexpressions.patch create mode 100644 packages/kbn-handlebars/.patches/utils.patch create mode 100644 packages/kbn-handlebars/.patches/whitespace-control.patch create mode 100644 packages/kbn-handlebars/BUILD.bazel create mode 100644 packages/kbn-handlebars/LICENSE create mode 100644 packages/kbn-handlebars/README.md create mode 100644 packages/kbn-handlebars/jest.config.js create mode 100644 packages/kbn-handlebars/package.json create mode 100755 packages/kbn-handlebars/scripts/check_for_test_changes.sh create mode 100755 packages/kbn-handlebars/scripts/print_ast.js create mode 100755 packages/kbn-handlebars/scripts/update_test_patches.sh create mode 100644 packages/kbn-handlebars/src/__jest__/test_bench.ts create mode 100644 packages/kbn-handlebars/src/__snapshots__/index.test.ts.snap create mode 100644 packages/kbn-handlebars/src/index.test.ts create mode 100644 packages/kbn-handlebars/src/index.ts create mode 100644 packages/kbn-handlebars/src/upstream/index.basic.test.ts create mode 100644 packages/kbn-handlebars/src/upstream/index.blocks.test.ts create mode 100644 packages/kbn-handlebars/src/upstream/index.builtins.test.ts create mode 100644 packages/kbn-handlebars/src/upstream/index.compiler.test.ts create mode 100644 packages/kbn-handlebars/src/upstream/index.data.test.ts create mode 100644 packages/kbn-handlebars/src/upstream/index.helpers.test.ts create mode 100644 packages/kbn-handlebars/src/upstream/index.regressions.test.ts create mode 100644 packages/kbn-handlebars/src/upstream/index.security.test.ts create mode 100644 packages/kbn-handlebars/src/upstream/index.strict.test.ts create mode 100644 packages/kbn-handlebars/src/upstream/index.subexpressions.test.ts create mode 100644 packages/kbn-handlebars/src/upstream/index.utils.test.ts create mode 100644 packages/kbn-handlebars/src/upstream/index.whitespace-control.test.ts create mode 100644 packages/kbn-handlebars/tsconfig.json create mode 100644 src/core/server/csp/csp_config.test.mocks.ts delete mode 100644 test/api_integration/apis/general/csp.js diff --git a/.eslintrc.js b/.eslintrc.js index 3ec2fe38b4d6f..b4dbfbaf8600b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -94,6 +94,22 @@ const SAFER_LODASH_SET_DEFINITELYTYPED_HEADER = ` */ `; +const KBN_HANDLEBARS_HEADER = ` +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See \`packages/kbn-handlebars/LICENSE\` for more information. + */ +`; + +const KBN_HANDLEBARS_HANDLEBARS_HEADER = ` +/* + * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See \`packages/kbn-handlebars/LICENSE\` for more information. + */ +`; + const packagePkgJsons = globby.sync('*/package.json', { cwd: Path.resolve(__dirname, 'packages'), absolute: true, @@ -293,6 +309,8 @@ module.exports = { SAFER_LODASH_SET_HEADER, SAFER_LODASH_SET_LODASH_HEADER, SAFER_LODASH_SET_DEFINITELYTYPED_HEADER, + KBN_HANDLEBARS_HEADER, + KBN_HANDLEBARS_HANDLEBARS_HEADER, ], }, ], @@ -325,6 +343,8 @@ module.exports = { SAFER_LODASH_SET_HEADER, SAFER_LODASH_SET_LODASH_HEADER, SAFER_LODASH_SET_DEFINITELYTYPED_HEADER, + KBN_HANDLEBARS_HEADER, + KBN_HANDLEBARS_HANDLEBARS_HEADER, ], }, ], @@ -364,6 +384,8 @@ module.exports = { SAFER_LODASH_SET_HEADER, SAFER_LODASH_SET_LODASH_HEADER, SAFER_LODASH_SET_DEFINITELYTYPED_HEADER, + KBN_HANDLEBARS_HEADER, + KBN_HANDLEBARS_HANDLEBARS_HEADER, ], }, ], @@ -393,6 +415,8 @@ module.exports = { OLD_ELASTIC_LICENSE_HEADER, SAFER_LODASH_SET_HEADER, SAFER_LODASH_SET_DEFINITELYTYPED_HEADER, + KBN_HANDLEBARS_HEADER, + KBN_HANDLEBARS_HANDLEBARS_HEADER, ], }, ], @@ -418,6 +442,8 @@ module.exports = { OLD_ELASTIC_LICENSE_HEADER, SAFER_LODASH_SET_LODASH_HEADER, SAFER_LODASH_SET_DEFINITELYTYPED_HEADER, + KBN_HANDLEBARS_HEADER, + KBN_HANDLEBARS_HANDLEBARS_HEADER, ], }, ], @@ -443,6 +469,66 @@ module.exports = { OLD_DUAL_LICENSE_HEADER, SAFER_LODASH_SET_HEADER, SAFER_LODASH_SET_LODASH_HEADER, + KBN_HANDLEBARS_HEADER, + KBN_HANDLEBARS_HANDLEBARS_HEADER, + ], + }, + ], + }, + }, + + /** + * @kbn/handlebars package requires special license headers + */ + { + files: ['packages/kbn-handlebars/**/*.{js,mjs,ts,tsx}'], + rules: { + '@kbn/eslint/require-license-header': [ + 'error', + { + license: KBN_HANDLEBARS_HEADER, + }, + ], + '@kbn/eslint/disallow-license-headers': [ + 'error', + { + licenses: [ + APACHE_2_0_LICENSE_HEADER, + DUAL_LICENSE_HEADER, + ELASTIC_LICENSE_HEADER, + OLD_DUAL_LICENSE_HEADER, + OLD_ELASTIC_LICENSE_HEADER, + SAFER_LODASH_SET_HEADER, + SAFER_LODASH_SET_LODASH_HEADER, + SAFER_LODASH_SET_DEFINITELYTYPED_HEADER, + KBN_HANDLEBARS_HANDLEBARS_HEADER, + ], + }, + ], + }, + }, + { + files: ['packages/kbn-handlebars/src/upstream/**/*.{js,mjs,ts,tsx}'], + rules: { + '@kbn/eslint/require-license-header': [ + 'error', + { + license: KBN_HANDLEBARS_HANDLEBARS_HEADER, + }, + ], + '@kbn/eslint/disallow-license-headers': [ + 'error', + { + licenses: [ + APACHE_2_0_LICENSE_HEADER, + DUAL_LICENSE_HEADER, + ELASTIC_LICENSE_HEADER, + OLD_DUAL_LICENSE_HEADER, + OLD_ELASTIC_LICENSE_HEADER, + SAFER_LODASH_SET_HEADER, + SAFER_LODASH_SET_LODASH_HEADER, + SAFER_LODASH_SET_DEFINITELYTYPED_HEADER, + KBN_HANDLEBARS_HEADER, ], }, ], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index abd63289e0480..65156ad05ae8f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -324,6 +324,7 @@ # Kibana Platform Security /packages/kbn-crypto/ @elastic/kibana-security +/packages/kbn-handlebars/ @elastic/kibana-security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core /src/plugins/interactive_setup/ @elastic/kibana-security /test/interactive_setup_api_integration/ @elastic/kibana-security diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index a9ce0254cea52..e71454b74d697 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -3,7 +3,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API summary: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-04-26 +date: 2022-05-23 tags: ['contributor', 'dev', 'apidocs', 'kibana'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. --- @@ -14,14 +14,14 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Referencing plugin(s) | Remove By | | ---------------|-----------|-----------| | | dataViews, maps, data | - | -| | dataViews, unifiedSearch, maps, data | - | +| | dataViews, maps, data | - | | | dataViews, discover, ux, savedObjects, dataViewEditor, maps, visDefaultEditor, data | - | | | dataViews, dataViewEditor, maps, visDefaultEditor, data | - | | | dataViews, unifiedSearch | - | | | dataViews, canvas | - | | | dataViews, unifiedSearch, data | - | | | dataViews, canvas, data | - | -| | dataViews, unifiedSearch, maps, data | - | +| | dataViews, maps, data | - | | | dataViews, dataViewEditor, maps, visDefaultEditor, data | - | | | dataViews, maps, data | - | | | dataViews | - | @@ -38,16 +38,17 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | dataViewManagement, dataViews | - | | | visTypeTimeseries, graph, dataViewManagement, dataViews | - | | | dataViews, dataViewManagement | - | +| | discover, visualizations, dashboard, lens, maps, monitoring | - | | | unifiedSearch, discover, maps, infra, graph, securitySolution, stackAlerts, inputControlVis, savedObjects | - | | | maps | - | | | data, infra, maps | - | -| | discover | - | -| | discover | - | +| | alerting, discover, securitySolution | - | +| | alerting, discover, securitySolution | - | | | data, discover, embeddable | - | | | advancedSettings, discover | - | | | advancedSettings, discover | - | -| | management, observability, infra, apm, cloudSecurityPosture, enterpriseSearch, securitySolution, synthetics, ux, kibanaOverview | - | -| | esUiShared, home, spaces, fleet, visualizations, lens, observability, dataEnhanced, ml, apm, cloudSecurityPosture, indexLifecycleManagement, synthetics, upgradeAssistant, ux, kibanaOverview, savedObjectsManagement | - | +| | management, observability, infra, apm, cloudSecurityPosture, enterpriseSearch, synthetics, ux, kibanaOverview | - | +| | esUiShared, home, data, spaces, fleet, visualizations, lens, observability, ml, apm, cloudSecurityPosture, indexLifecycleManagement, synthetics, upgradeAssistant, ux, kibanaOverview, savedObjectsManagement | - | | | canvas, visTypeXy | - | | | canvas | - | | | canvas | - | @@ -58,11 +59,12 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | canvas | - | | | canvas | - | | | canvas, visTypeXy | - | -| | management, spaces, observability, ml, canvas, cloudSecurityPosture, enterpriseSearch, osquery, securitySolution, kibanaOverview | - | +| | management, spaces, observability, ml, canvas, cloudSecurityPosture, enterpriseSearch, osquery, kibanaOverview | - | +| | actions, alerting | - | +| | encryptedSavedObjects, actions, data, cloud, ml, logstash, securitySolution | - | | | dashboard, lens, stackAlerts, visTypeTable, visTypeTimeseries, visTypeXy, visTypeVislib, expressionPartitionVis | - | -| | visTypeTimeseries, graph, dataViewManagement | - | -| | encryptedSavedObjects, actions, cloud, ml, dataEnhanced, logstash, securitySolution | - | | | dashboard | - | +| | visTypeTimeseries, graph, dataViewManagement | - | | | visTypeTimeseries | - | | | dataViewManagement | - | | | dataViewManagement | - | @@ -72,44 +74,36 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | visTypeGauge | - | | | visTypePie | - | | | visTypePie | - | -| | actions, alerting | - | | | console | - | +| | discover, dashboard, lens, urlDrilldown, stackAlerts | 8.1 | +| | discover, dashboard, lens, urlDrilldown, stackAlerts | 8.1 | +| | discover, dashboard, lens, urlDrilldown, stackAlerts | 8.1 | | | unifiedSearch | 8.1 | -| | unifiedSearch, dataEnhanced | 8.1 | -| | unifiedSearch, discover, dashboard, urlDrilldown, stackAlerts | 8.1 | -| | unifiedSearch | 8.1 | -| | unifiedSearch | 8.1 | -| | unifiedSearch, discover, dashboard, urlDrilldown, stackAlerts | 8.1 | -| | unifiedSearch, dataEnhanced | 8.1 | -| | unifiedSearch, discover, dashboard, urlDrilldown, stackAlerts | 8.1 | -| | unifiedSearch, dataEnhanced | 8.1 | -| | discover, stackAlerts, inputControlVis | 8.1 | -| | discover, stackAlerts, inputControlVis | 8.1 | -| | dataEnhanced | 8.1 | +| | stackAlerts, alerting, securitySolution, inputControlVis | 8.1 | +| | stackAlerts, alerting, securitySolution, inputControlVis | 8.1 | | | apm | 8.1 | | | dataViews, unifiedSearch | 8.2 | | | dataViews, unifiedSearch, data | 8.2 | | | visualizations, dashboard, lens, maps, ml, securitySolution, security | 8.8.0 | -| | lens, dashboard, maps | 8.8.0 | +| | dashboard, maps | 8.8.0 | | | embeddable, discover, presentationUtil, dashboard, graph | 8.8.0 | +| | spaces, security, alerting | 8.8.0 | | | spaces, security, actions, alerting, ml, remoteClusters, graph, indexLifecycleManagement, mapsEms, painlessLab, rollup, searchprofiler, snapshotRestore, transform, upgradeAssistant | 8.8.0 | | | apm, security, securitySolution | 8.8.0 | | | apm, security, securitySolution | 8.8.0 | | | securitySolution | 8.8.0 | | | savedObjectsTaggingOss, visualizations, dashboard, lens | 8.8.0 | | | dashboard | 8.8.0 | +| | monitoring | 8.8.0 | +| | monitoring, kibanaUsageCollection | 8.8.0 | | | cloud, apm | 8.8.0 | | | security, licenseManagement, ml, apm, crossClusterReplication, logstash, painlessLab, searchprofiler, watcher | 8.8.0 | | | management, fleet, security, kibanaOverview | 8.8.0 | -| | spaces, security, alerting | 8.8.0 | | | security, fleet | 8.8.0 | | | security, fleet | 8.8.0 | | | security, fleet | 8.8.0 | | | security | 8.8.0 | | | mapsEms | 8.8.0 | -| | visTypeVega | 8.8.0 | -| | monitoring, visTypeVega | 8.8.0 | -| | monitoring, kibanaUsageCollection | 8.8.0 | | | ml | 8.8.0 Note to maintainers: when looking at usages, mind that typical use could be inside a `catch` block, @@ -132,6 +126,7 @@ Safe to remove. | ---------------|------------| | | data | | | data | +| | data | | | data | | | data | | | data | @@ -168,12 +163,15 @@ Safe to remove. | | data | | | data | | | data | +| | data | | | data | +| | data | | | data | | | data | | | data | | | data | | | data | +| | data | | | data | | | data | | | data | @@ -181,11 +179,13 @@ Safe to remove. | | data | | | data | | | data | +| | data | | | data | | | data | | | data | | | data | | | data | +| | data | | | dataViews | | | dataViews | | | expressions | @@ -211,5 +211,6 @@ Safe to remove. | | reporting | | | taskManager | | | core | +| | core | | | core | | | core | \ No newline at end of file diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 4904da587db13..f94de1760dd44 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -3,7 +3,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin summary: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-04-26 +date: 2022-05-23 tags: ['contributor', 'dev', 'apidocs', 'kibana'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. --- @@ -33,6 +33,10 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| +| | [wrap_search_source_client.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.ts#:~:text=create) | - | +| | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch) | 8.1 | +| | [wrap_search_source_client.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.ts#:~:text=create) | - | +| | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch) | 8.1 | | | [plugin.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/plugin.test.ts#:~:text=getKibanaFeatures) | 8.8.0 | | | [plugin.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/plugin.ts#:~:text=license%24), [license_state.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/license_state.test.ts#:~:text=license%24), [license_state.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/license_state.test.ts#:~:text=license%24) | 8.8.0 | | | [task.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/usage/task.ts#:~:text=index) | - | @@ -113,6 +117,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| +| | [sync_dashboard_filter_state.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts#:~:text=syncQueryStateWithUrl), [sync_dashboard_filter_state.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts#:~:text=syncQueryStateWithUrl), [dashboard_listing.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx#:~:text=syncQueryStateWithUrl), [dashboard_listing.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx#:~:text=syncQueryStateWithUrl) | - | | | [export_csv_action.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/actions/export_csv_action.tsx#:~:text=fieldFormats) | - | | | [filter_utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/lib/filter_utils.ts#:~:text=Filter), [filter_utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/lib/filter_utils.ts#:~:text=Filter), [filter_utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/lib/filter_utils.ts#:~:text=Filter), [filter_utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/lib/filter_utils.ts#:~:text=Filter), [filter_utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/lib/filter_utils.ts#:~:text=Filter), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=Filter), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=Filter), [dashboard_state_slice.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts#:~:text=Filter), [dashboard_state_slice.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts#:~:text=Filter), [dashboard_state_slice.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts#:~:text=Filter)+ 5 more | 8.1 | | | [filter_utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/lib/filter_utils.ts#:~:text=Filter), [filter_utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/lib/filter_utils.ts#:~:text=Filter), [filter_utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/lib/filter_utils.ts#:~:text=Filter), [filter_utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/lib/filter_utils.ts#:~:text=Filter), [filter_utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/lib/filter_utils.ts#:~:text=Filter), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=Filter), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=Filter), [dashboard_state_slice.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts#:~:text=Filter), [dashboard_state_slice.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts#:~:text=Filter), [dashboard_state_slice.ts](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts#:~:text=Filter)+ 5 more | 8.1 | @@ -130,32 +135,21 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField)+ 16 more | - | +| | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/index.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternField), [agg_config.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/agg_config.test.ts#:~:text=IndexPatternField), [agg_config.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/agg_config.test.ts#:~:text=IndexPatternField)+ 16 more | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternsContract), [create_search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/create_search_source.ts#:~:text=IndexPatternsContract), [create_search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/create_search_source.ts#:~:text=IndexPatternsContract), [search_source_service.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source_service.ts#:~:text=IndexPatternsContract), [search_source_service.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source_service.ts#:~:text=IndexPatternsContract), [esaggs_fn.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts#:~:text=IndexPatternsContract), [esaggs_fn.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts#:~:text=IndexPatternsContract), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/types.ts#:~:text=IndexPatternsContract), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/types.ts#:~:text=IndexPatternsContract), [create_search_source.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/create_search_source.test.ts#:~:text=IndexPatternsContract)+ 19 more | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/index.ts#:~:text=IndexPatternsService) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/types.ts#:~:text=IndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPattern), [tabify_docs.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/tabify/tabify_docs.ts#:~:text=IndexPattern), [tabify_docs.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/tabify/tabify_docs.ts#:~:text=IndexPattern)+ 89 more | - | -| | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IFieldType), [date_histogram.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/buckets/date_histogram.ts#:~:text=IFieldType), [date_histogram.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/buckets/date_histogram.ts#:~:text=IFieldType), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IFieldType), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IFieldType), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IFieldType), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IFieldType), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/index.ts#:~:text=IFieldType), [create_filters_from_range_select.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts#:~:text=IFieldType), [create_filters_from_range_select.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts#:~:text=IFieldType)+ 6 more | 8.2 | -| | [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField)+ 16 more | - | -| | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IIndexPattern), [get_time.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/query/timefilter/get_time.ts#:~:text=IIndexPattern), [get_time.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/query/timefilter/get_time.ts#:~:text=IIndexPattern), [get_time.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/query/timefilter/get_time.ts#:~:text=IIndexPattern), [get_time.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/query/timefilter/get_time.ts#:~:text=IIndexPattern), [normalize_sort_request.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/normalize_sort_request.ts#:~:text=IIndexPattern), [normalize_sort_request.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/normalize_sort_request.ts#:~:text=IIndexPattern), [normalize_sort_request.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/normalize_sort_request.ts#:~:text=IIndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IIndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IIndexPattern)+ 23 more | - | +| | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IFieldType), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IFieldType), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IFieldType), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IFieldType), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IFieldType), [date_histogram.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/buckets/date_histogram.ts#:~:text=IFieldType), [date_histogram.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/buckets/date_histogram.ts#:~:text=IFieldType), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/index.ts#:~:text=IFieldType), [create_filters_from_range_select.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts#:~:text=IFieldType), [create_filters_from_range_select.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts#:~:text=IFieldType)+ 6 more | 8.2 | +| | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/index.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternField), [agg_config.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/agg_config.test.ts#:~:text=IndexPatternField), [agg_config.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/agg_config.test.ts#:~:text=IndexPatternField)+ 16 more | - | +| | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IIndexPattern), [get_time.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/query/timefilter/get_time.ts#:~:text=IIndexPattern), [get_time.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/query/timefilter/get_time.ts#:~:text=IIndexPattern), [get_time.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/query/timefilter/get_time.ts#:~:text=IIndexPattern), [get_time.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/query/timefilter/get_time.ts#:~:text=IIndexPattern), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IIndexPattern), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IIndexPattern), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IIndexPattern), [timefilter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/timefilter/timefilter.ts#:~:text=IIndexPattern), [timefilter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/timefilter/timefilter.ts#:~:text=IIndexPattern)+ 23 more | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternAttributes), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/index.ts#:~:text=IndexPatternAttributes), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternAttributes) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternsContract), [create_search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/create_search_source.ts#:~:text=IndexPatternsContract), [create_search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/create_search_source.ts#:~:text=IndexPatternsContract), [search_source_service.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source_service.ts#:~:text=IndexPatternsContract), [search_source_service.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source_service.ts#:~:text=IndexPatternsContract), [esaggs_fn.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts#:~:text=IndexPatternsContract), [esaggs_fn.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts#:~:text=IndexPatternsContract), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/types.ts#:~:text=IndexPatternsContract), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/types.ts#:~:text=IndexPatternsContract), [create_search_source.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/create_search_source.test.ts#:~:text=IndexPatternsContract)+ 19 more | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/index.ts#:~:text=IndexPatternsService) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/types.ts#:~:text=IndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPattern), [tabify_docs.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/tabify/tabify_docs.ts#:~:text=IndexPattern), [tabify_docs.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/tabify/tabify_docs.ts#:~:text=IndexPattern)+ 89 more | - | | | [aggs_service.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/search/aggs/aggs_service.ts#:~:text=indexPatternsServiceFactory), [esaggs.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/search/expressions/esaggs.ts#:~:text=indexPatternsServiceFactory), [search_service.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/search/search_service.ts#:~:text=indexPatternsServiceFactory) | - | +| | [connected_search_session_indicator.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/session/session_indicator/connected_search_session_indicator/connected_search_session_indicator.tsx#:~:text=RedirectAppLinks), [connected_search_session_indicator.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/session/session_indicator/connected_search_session_indicator/connected_search_session_indicator.tsx#:~:text=RedirectAppLinks), [connected_search_session_indicator.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/session/session_indicator/connected_search_session_indicator/connected_search_session_indicator.tsx#:~:text=RedirectAppLinks), [get_columns.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/session/sessions_mgmt/lib/get_columns.tsx#:~:text=RedirectAppLinks), [get_columns.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/session/sessions_mgmt/lib/get_columns.tsx#:~:text=RedirectAppLinks), [get_columns.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/session/sessions_mgmt/lib/get_columns.tsx#:~:text=RedirectAppLinks) | - | | | [data_table.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx#:~:text=executeTriggerActions), [data_table.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx#:~:text=executeTriggerActions) | - | - - - -## dataEnhanced - -| Deprecated API | Reference location(s) | Remove By | -| ---------------|-----------|-----------| -| | [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/types.ts#:~:text=KueryNode), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/types.ts#:~:text=KueryNode), [get_search_session_page.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts#:~:text=KueryNode), [get_search_session_page.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts#:~:text=KueryNode), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=KueryNode), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=KueryNode), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=KueryNode), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=KueryNode), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=KueryNode), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=KueryNode) | 8.1 | -| | [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=nodeBuilder), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=nodeBuilder), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=nodeBuilder), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=nodeBuilder), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=nodeBuilder), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=nodeBuilder), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=nodeBuilder), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=nodeBuilder), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=nodeBuilder), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=nodeBuilder)+ 2 more | 8.1 | -| | [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/types.ts#:~:text=KueryNode), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/types.ts#:~:text=KueryNode), [get_search_session_page.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts#:~:text=KueryNode), [get_search_session_page.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts#:~:text=KueryNode), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=KueryNode), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=KueryNode), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=KueryNode), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=KueryNode), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=KueryNode), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=KueryNode) | 8.1 | -| | [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/types.ts#:~:text=KueryNode), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/types.ts#:~:text=KueryNode), [get_search_session_page.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts#:~:text=KueryNode), [get_search_session_page.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts#:~:text=KueryNode), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=KueryNode), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=KueryNode), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=KueryNode), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=KueryNode), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=KueryNode), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=KueryNode) | 8.1 | -| | [get_columns.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx#:~:text=RedirectAppLinks), [get_columns.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx#:~:text=RedirectAppLinks), [get_columns.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx#:~:text=RedirectAppLinks), [connected_search_session_indicator.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx#:~:text=RedirectAppLinks), [connected_search_session_indicator.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx#:~:text=RedirectAppLinks), [connected_search_session_indicator.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx#:~:text=RedirectAppLinks) | - | -| | [session_service.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/session_service.ts#:~:text=authc) | - | +| | [session_service.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/search/session/session_service.ts#:~:text=authc) | - | @@ -197,9 +191,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPattern), [data_view_field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.test.ts#:~:text=IndexPattern), [data_view_field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.test.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/public/index.ts#:~:text=IndexPattern), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPattern), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPattern), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPattern), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPattern) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/public/index.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view_field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.test.ts#:~:text=IndexPatternField), [data_view_field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.test.ts#:~:text=IndexPatternField)+ 2 more | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IIndexPattern), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=IIndexPattern), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=IIndexPattern) | - | -| | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType)+ 7 more | 8.2 | +| | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IFieldType), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=IFieldType)+ 7 more | 8.2 | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternAttributes) | - | -| | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType)+ 7 more | 8.2 | +| | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IFieldType), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=IFieldType)+ 7 more | 8.2 | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IIndexPattern), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=IIndexPattern), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=IIndexPattern) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternAttributes) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternsContract), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/public/index.ts#:~:text=IndexPatternsContract) | - | @@ -228,13 +222,12 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | ---------------|-----------|-----------| | | [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=IndexPattern), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=IndexPattern), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=IndexPattern), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=IndexPattern) | - | | | [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=create), [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=create) | - | -| | [anchor.ts](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/context/services/anchor.ts#:~:text=fetch), [fetch_hits_in_interval.ts](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts#:~:text=fetch) | 8.1 | +| | [discover_state.ts](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/main/services/discover_state.ts#:~:text=syncQueryStateWithUrl), [discover_state.ts](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/main/services/discover_state.ts#:~:text=syncQueryStateWithUrl) | - | | | [plugin.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/plugin.tsx#:~:text=indexPatterns) | - | | | [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=Filter), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=Filter) | 8.1 | | | [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=Filter), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=Filter) | 8.1 | | | [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=IndexPattern), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=IndexPattern), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=IndexPattern), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=IndexPattern) | - | | | [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=create), [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=create) | - | -| | [anchor.ts](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/context/services/anchor.ts#:~:text=fetch), [fetch_hits_in_interval.ts](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts#:~:text=fetch) | 8.1 | | | [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=IndexPattern), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=IndexPattern) | - | | | [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=Filter), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=Filter) | 8.1 | | | [on_save_search.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx#:~:text=SavedObjectSaveModal), [on_save_search.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx#:~:text=SavedObjectSaveModal) | 8.8.0 | @@ -372,11 +365,14 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| +| | [app.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/app_plugin/app.tsx#:~:text=syncQueryStateWithUrl), [app.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/app_plugin/app.tsx#:~:text=syncQueryStateWithUrl) | - | | | [ranges.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx#:~:text=fieldFormats), [droppable.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts#:~:text=fieldFormats) | - | +| | [open_in_discover_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx#:~:text=Filter), [open_in_discover_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx#:~:text=Filter) | 8.1 | +| | [open_in_discover_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx#:~:text=Filter), [open_in_discover_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx#:~:text=Filter) | 8.1 | +| | [open_in_discover_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx#:~:text=Filter), [open_in_discover_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx#:~:text=Filter) | 8.1 | | | [workspace_panel.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx#:~:text=RedirectAppLinks), [workspace_panel.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx#:~:text=RedirectAppLinks), [workspace_panel.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx#:~:text=RedirectAppLinks) | - | | | [display_duplicate_title_confirm_modal.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/persistence/saved_objects_utils/display_duplicate_title_confirm_modal.ts#:~:text=SavedObject), [display_duplicate_title_confirm_modal.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/persistence/saved_objects_utils/display_duplicate_title_confirm_modal.ts#:~:text=SavedObject), [check_for_duplicate_title.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/persistence/saved_objects_utils/check_for_duplicate_title.ts#:~:text=SavedObject), [check_for_duplicate_title.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/persistence/saved_objects_utils/check_for_duplicate_title.ts#:~:text=SavedObject) | 8.8.0 | | | [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/app_plugin/types.ts#:~:text=onAppLeave), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/app_plugin/types.ts#:~:text=onAppLeave), [mounter.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/app_plugin/mounter.tsx#:~:text=onAppLeave) | 8.8.0 | -| | [saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts#:~:text=warning), [saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts#:~:text=warning) | 8.8.0 | @@ -415,6 +411,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [index.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/lazy_load_bundle/index.ts#:~:text=IndexPatternsContract), [index.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/lazy_load_bundle/index.ts#:~:text=IndexPatternsContract), [index.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/lazy_load_bundle/index.ts#:~:text=IndexPatternsContract), [index.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/lazy_load_bundle/index.ts#:~:text=IndexPatternsContract) | - | | | [es_tooltip_property.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts#:~:text=IndexPattern), [es_tooltip_property.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts#:~:text=IndexPattern), [percentile_agg_field.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/agg/percentile_agg_field.test.ts#:~:text=IndexPattern), [percentile_agg_field.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/agg/percentile_agg_field.test.ts#:~:text=IndexPattern), [get_docvalue_source_fields.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_search_source/util/get_docvalue_source_fields.test.ts#:~:text=IndexPattern), [get_docvalue_source_fields.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_search_source/util/get_docvalue_source_fields.test.ts#:~:text=IndexPattern), [get_docvalue_source_fields.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_search_source/util/get_docvalue_source_fields.test.ts#:~:text=IndexPattern), [es_tooltip_property.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts#:~:text=IndexPattern), [es_tooltip_property.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts#:~:text=IndexPattern), [percentile_agg_field.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/agg/percentile_agg_field.test.ts#:~:text=IndexPattern)+ 4 more | - | | | [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField)+ 84 more | - | +| | [global_sync.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/routes/map_page/url_state/global_sync.ts#:~:text=syncQueryStateWithUrl), [global_sync.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/routes/map_page/url_state/global_sync.ts#:~:text=syncQueryStateWithUrl) | - | | | [kibana_services.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/kibana_services.ts#:~:text=indexPatterns) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/lazy_load_bundle/index.ts#:~:text=IndexPatternsContract), [index.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/lazy_load_bundle/index.ts#:~:text=IndexPatternsContract), [index.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/lazy_load_bundle/index.ts#:~:text=IndexPatternsContract), [index.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/lazy_load_bundle/index.ts#:~:text=IndexPatternsContract) | - | | | [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField), [single_field_select.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/components/single_field_select.tsx#:~:text=IndexPatternField)+ 84 more | - | @@ -463,6 +460,7 @@ so TS and code-reference navigation might not highlight them. | | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| +| | [url_state.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/public/url_state.ts#:~:text=syncQueryStateWithUrl), [url_state.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/public/url_state.ts#:~:text=syncQueryStateWithUrl) | - | | | [legacy_shims.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/public/legacy_shims.ts#:~:text=injectedMetadata) | 8.8.0 | | | [bulk_uploader.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.ts#:~:text=process) | 8.8.0 | @@ -595,9 +593,11 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| +| | [wrap_search_source_client.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.ts#:~:text=create) | - | +| | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch) | 8.1 | | | [middleware.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=indexPatterns), [dependencies_start_mock.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts#:~:text=indexPatterns) | - | -| | [use_primary_navigation.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx#:~:text=KibanaPageTemplateProps), [use_primary_navigation.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx#:~:text=KibanaPageTemplateProps), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx#:~:text=KibanaPageTemplateProps), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx#:~:text=KibanaPageTemplateProps) | - | -| | [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx#:~:text=KibanaPageTemplate), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx#:~:text=KibanaPageTemplate) | - | +| | [wrap_search_source_client.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.ts#:~:text=create) | - | +| | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch) | 8.1 | | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [isolation.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts#:~:text=mode), [isolation.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts#:~:text=mode)+ 2 more | 8.8.0 | | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [isolation.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts#:~:text=mode), [isolation.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts#:~:text=mode)+ 2 more | 8.8.0 | | | [request_context_factory.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/request_context_factory.ts#:~:text=authc), [request_context_factory.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/request_context_factory.ts#:~:text=authc), [create_signals_migration_route.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts#:~:text=authc), [delete_signals_migration_route.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts#:~:text=authc), [finalize_signals_migration_route.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts#:~:text=authc), [open_close_signals_route.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts#:~:text=authc), [preview_rules_route.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts#:~:text=authc), [common.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts#:~:text=authc) | - | @@ -634,10 +634,10 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | | [fetch_search_source_query.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts#:~:text=fetch), [alert_type.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts#:~:text=fetch), [alert_type.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts#:~:text=fetch) | 8.1 | | | [entity_index_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/entity_index_expression.tsx#:~:text=indexPatterns), [boundary_index_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx#:~:text=indexPatterns), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/index.tsx#:~:text=indexPatterns), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/index.tsx#:~:text=indexPatterns), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/index.tsx#:~:text=indexPatterns) | - | | | [expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx#:~:text=fieldFormats) | - | -| | [read_only_filter_items.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx#:~:text=Filter), [read_only_filter_items.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx#:~:text=Filter), [search_source_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx#:~:text=Filter), [search_source_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx#:~:text=Filter) | 8.1 | -| | [read_only_filter_items.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx#:~:text=Filter), [read_only_filter_items.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx#:~:text=Filter), [search_source_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx#:~:text=Filter), [search_source_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx#:~:text=Filter) | 8.1 | +| | [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter) | 8.1 | +| | [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter) | 8.1 | | | [fetch_search_source_query.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts#:~:text=fetch), [alert_type.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts#:~:text=fetch), [alert_type.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts#:~:text=fetch) | 8.1 | -| | [read_only_filter_items.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx#:~:text=Filter), [read_only_filter_items.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx#:~:text=Filter), [search_source_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx#:~:text=Filter), [search_source_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx#:~:text=Filter) | 8.1 | +| | [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter) | 8.1 | @@ -645,8 +645,8 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [use_no_data_config.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/apps/use_no_data_config.ts#:~:text=KibanaPageTemplateProps), [use_no_data_config.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/apps/use_no_data_config.ts#:~:text=KibanaPageTemplateProps) | - | -| | [alert_messages.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/lib/alert_types/alert_messages.tsx#:~:text=RedirectAppLinks), [alert_messages.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/lib/alert_types/alert_messages.tsx#:~:text=RedirectAppLinks), [alert_messages.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/lib/alert_types/alert_messages.tsx#:~:text=RedirectAppLinks), [uptime_app.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/apps/uptime_app.tsx#:~:text=RedirectAppLinks), [uptime_app.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/apps/uptime_app.tsx#:~:text=RedirectAppLinks), [uptime_app.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/apps/uptime_app.tsx#:~:text=RedirectAppLinks) | - | +| | [use_no_data_config.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/legacy_uptime/app/use_no_data_config.ts#:~:text=KibanaPageTemplateProps), [use_no_data_config.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/legacy_uptime/app/use_no_data_config.ts#:~:text=KibanaPageTemplateProps), [use_no_data_config.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_no_data_config.ts#:~:text=KibanaPageTemplateProps), [use_no_data_config.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_no_data_config.ts#:~:text=KibanaPageTemplateProps) | - | +| | [alert_messages.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/alert_messages.tsx#:~:text=RedirectAppLinks), [alert_messages.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/alert_messages.tsx#:~:text=RedirectAppLinks), [alert_messages.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/alert_messages.tsx#:~:text=RedirectAppLinks), [uptime_app.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_app.tsx#:~:text=RedirectAppLinks), [uptime_app.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_app.tsx#:~:text=RedirectAppLinks), [uptime_app.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_app.tsx#:~:text=RedirectAppLinks), [synthetics_app.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx#:~:text=RedirectAppLinks), [synthetics_app.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx#:~:text=RedirectAppLinks), [synthetics_app.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx#:~:text=RedirectAppLinks) | - | @@ -662,22 +662,12 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx#:~:text=IndexPatternsContract), [index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx#:~:text=IndexPatternsContract), [create_index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx#:~:text=IndexPatternsContract), [create_index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx#:~:text=IndexPatternsContract), [index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx#:~:text=IndexPatternsContract), [index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx#:~:text=IndexPatternsContract), [create_index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx#:~:text=IndexPatternsContract), [create_index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx#:~:text=IndexPatternsContract) | - | -| | [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IIndexPattern), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IIndexPattern), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IIndexPattern), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern) | - | -| | [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IFieldType), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType)+ 25 more | 8.2 | +| | [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IIndexPattern), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IIndexPattern), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IIndexPattern) | - | +| | [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType) | 8.2 | | | [query_string_input.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/query_string_input/query_string_input.tsx#:~:text=indexPatterns) | - | | | [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=esFilters), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=esFilters), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=esFilters) | 8.1 | -| | [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [value.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts#:~:text=KueryNode)+ 2 more | 8.1 | -| | [use_filter_manager.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts#:~:text=Filter), [use_filter_manager.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [get_stub_filter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts#:~:text=Filter), [get_stub_filter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts#:~:text=Filter), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=Filter)+ 10 more | 8.1 | -| | [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=FILTERS), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=FILTERS), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=FILTERS), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=FILTERS), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=FILTERS), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=FILTERS) | 8.1 | -| | [filter_editor_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts#:~:text=toggleFilterNegated), [filter_editor_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts#:~:text=toggleFilterNegated), [filter_editor_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts#:~:text=toggleFilterNegated), [filter_editor_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts#:~:text=toggleFilterNegated), [filter_editor_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts#:~:text=toggleFilterNegated) | 8.1 | -| | [use_filter_manager.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts#:~:text=Filter), [use_filter_manager.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [get_stub_filter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts#:~:text=Filter), [get_stub_filter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts#:~:text=Filter), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=Filter)+ 10 more | 8.1 | -| | [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [value.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts#:~:text=KueryNode)+ 2 more | 8.1 | -| | [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IFieldType), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType)+ 60 more | 8.2 | -| | [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IIndexPattern), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IIndexPattern), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IIndexPattern), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IIndexPattern), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IIndexPattern), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IIndexPattern)+ 4 more | - | -| | [index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx#:~:text=IndexPatternsContract), [index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx#:~:text=IndexPatternsContract), [create_index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx#:~:text=IndexPatternsContract), [create_index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx#:~:text=IndexPatternsContract), [index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx#:~:text=IndexPatternsContract), [index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx#:~:text=IndexPatternsContract), [create_index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx#:~:text=IndexPatternsContract), [create_index_pattern_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx#:~:text=IndexPatternsContract) | - | -| | [use_filter_manager.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts#:~:text=Filter), [use_filter_manager.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [get_stub_filter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts#:~:text=Filter), [get_stub_filter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts#:~:text=Filter), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=Filter)+ 10 more | 8.1 | -| | [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [value.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts#:~:text=KueryNode)+ 2 more | 8.1 | +| | [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType)+ 8 more | 8.2 | +| | [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IIndexPattern), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IIndexPattern), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IIndexPattern), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IIndexPattern), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IIndexPattern), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IIndexPattern)+ 4 more | - | @@ -762,15 +752,6 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ -## visTypeVega - -| Deprecated API | Reference location(s) | Remove By | -| ---------------|-----------|-----------| -| | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/plugin.ts#:~:text=injectedMetadata) | 8.8.0 | -| | [search_api.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/data_model/search_api.ts#:~:text=injectedMetadata), [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/plugin.ts#:~:text=injectedMetadata) | 8.8.0 | - - - ## visTypeVislib | Deprecated API | Reference location(s) | Remove By | @@ -795,6 +776,7 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| +| | [app.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/app.tsx#:~:text=syncQueryStateWithUrl), [app.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/app.tsx#:~:text=syncQueryStateWithUrl) | - | | | [get_table_columns.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/utils/get_table_columns.tsx#:~:text=RedirectAppLinks), [get_table_columns.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/utils/get_table_columns.tsx#:~:text=RedirectAppLinks), [get_table_columns.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/utils/get_table_columns.tsx#:~:text=RedirectAppLinks) | - | | | [display_duplicate_title_confirm_modal.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_objects_utils/display_duplicate_title_confirm_modal.ts#:~:text=SavedObject), [display_duplicate_title_confirm_modal.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_objects_utils/display_duplicate_title_confirm_modal.ts#:~:text=SavedObject), [check_for_duplicate_title.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_objects_utils/check_for_duplicate_title.ts#:~:text=SavedObject), [check_for_duplicate_title.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_objects_utils/check_for_duplicate_title.ts#:~:text=SavedObject) | 8.8.0 | | | [visualize_top_nav.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx#:~:text=onAppLeave), [visualize_editor_common.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/components/visualize_editor_common.tsx#:~:text=onAppLeave), [app.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/app.tsx#:~:text=onAppLeave), [index.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/index.tsx#:~:text=onAppLeave) | 8.8.0 | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index aab61180e9dc8..816b8a9282d9f 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -3,7 +3,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team summary: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2022-04-26 +date: 2022-05-23 tags: ['contributor', 'dev', 'apidocs', 'kibana'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. --- @@ -25,12 +25,8 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Plugin | Deprecated API | Reference location(s) | Remove By | | --------|-------|-----------|-----------| -| dataViews | | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType)+ 7 more | 8.2 | -| dataViews | | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType)+ 23 more | 8.2 | -| dataEnhanced | | [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/types.ts#:~:text=KueryNode), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/types.ts#:~:text=KueryNode), [get_search_session_page.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts#:~:text=KueryNode), [get_search_session_page.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts#:~:text=KueryNode), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=KueryNode), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=KueryNode), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=KueryNode), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=KueryNode), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=KueryNode), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=KueryNode) | 8.1 | -| dataEnhanced | | [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=nodeBuilder), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=nodeBuilder), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=nodeBuilder), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=nodeBuilder), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=nodeBuilder), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=nodeBuilder), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=nodeBuilder), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=nodeBuilder), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=nodeBuilder), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=nodeBuilder)+ 2 more | 8.1 | -| dataEnhanced | | [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/types.ts#:~:text=KueryNode), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/types.ts#:~:text=KueryNode), [get_search_session_page.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts#:~:text=KueryNode), [get_search_session_page.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts#:~:text=KueryNode), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=KueryNode), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=KueryNode), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=KueryNode), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=KueryNode), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=KueryNode), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=KueryNode) | 8.1 | -| dataEnhanced | | [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/types.ts#:~:text=KueryNode), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/types.ts#:~:text=KueryNode), [get_search_session_page.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts#:~:text=KueryNode), [get_search_session_page.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts#:~:text=KueryNode), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=KueryNode), [check_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts#:~:text=KueryNode), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=KueryNode), [check_non_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/check_non_persisted_sessions.ts#:~:text=KueryNode), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=KueryNode), [expire_persisted_sessions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts#:~:text=KueryNode) | 8.1 | +| dataViews | | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IFieldType), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=IFieldType)+ 7 more | 8.2 | +| dataViews | | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IFieldType), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=IFieldType)+ 23 more | 8.2 | | urlDrilldown | | [context_variables.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts#:~:text=Filter), [context_variables.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts#:~:text=Filter), [url_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx#:~:text=Filter), [url_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx#:~:text=Filter), [data.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts#:~:text=Filter), [data.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts#:~:text=Filter) | 8.1 | | urlDrilldown | | [context_variables.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts#:~:text=Filter), [context_variables.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts#:~:text=Filter), [url_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx#:~:text=Filter), [url_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx#:~:text=Filter), [data.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts#:~:text=Filter), [data.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts#:~:text=Filter) | 8.1 | | urlDrilldown | | [context_variables.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts#:~:text=Filter), [context_variables.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts#:~:text=Filter), [url_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx#:~:text=Filter), [url_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx#:~:text=Filter), [data.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts#:~:text=Filter), [data.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts#:~:text=Filter) | 8.1 | @@ -42,10 +38,8 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Plugin | Deprecated API | Reference location(s) | Remove By | | --------|-------|-----------|-----------| -| discover | | [anchor.ts](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/context/services/anchor.ts#:~:text=fetch), [fetch_hits_in_interval.ts](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts#:~:text=fetch) | 8.1 | | discover | | [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=Filter), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=Filter) | 8.1 | | discover | | [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=Filter), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=Filter) | 8.1 | -| discover | | [anchor.ts](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/context/services/anchor.ts#:~:text=fetch), [fetch_hits_in_interval.ts](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts#:~:text=fetch) | 8.1 | | discover | | [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=Filter), [view_alert_route.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/view_alert/view_alert_route.tsx#:~:text=Filter) | 8.1 | | discover | | [on_save_search.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx#:~:text=SavedObjectSaveModal), [on_save_search.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx#:~:text=SavedObjectSaveModal), [save_modal.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/graph/public/components/save_modal.tsx#:~:text=SavedObjectSaveModal), [save_modal.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/graph/public/components/save_modal.tsx#:~:text=SavedObjectSaveModal) | 8.8.0 | | graph | | [plugin.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/graph/server/plugin.ts#:~:text=license%24) | 8.8.0 | @@ -161,13 +155,13 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | Plugin | Deprecated API | Reference location(s) | Remove By | | --------|-------|-----------|-----------| -| stackAlerts | | [fetch_search_source_query.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts#:~:text=fetch), [alert_type.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts#:~:text=fetch), [alert_type.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts#:~:text=fetch) | 8.1 | -| stackAlerts | | [read_only_filter_items.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx#:~:text=Filter), [read_only_filter_items.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx#:~:text=Filter), [search_source_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx#:~:text=Filter), [search_source_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx#:~:text=Filter) | 8.1 | -| stackAlerts | | [read_only_filter_items.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx#:~:text=Filter), [read_only_filter_items.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx#:~:text=Filter), [search_source_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx#:~:text=Filter), [search_source_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx#:~:text=Filter) | 8.1 | -| stackAlerts | | [fetch_search_source_query.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts#:~:text=fetch), [alert_type.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts#:~:text=fetch), [alert_type.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts#:~:text=fetch) | 8.1 | -| stackAlerts | | [read_only_filter_items.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx#:~:text=Filter), [read_only_filter_items.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx#:~:text=Filter), [search_source_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx#:~:text=Filter), [search_source_expression.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx#:~:text=Filter) | 8.1 | +| alerting | | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [fetch_search_source_query.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts#:~:text=fetch), [alert_type.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts#:~:text=fetch), [alert_type.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts#:~:text=fetch) | 8.1 | +| alerting | | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [fetch_search_source_query.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts#:~:text=fetch), [alert_type.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts#:~:text=fetch), [alert_type.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts#:~:text=fetch) | 8.1 | | alerting | | [plugin.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/plugin.test.ts#:~:text=getKibanaFeatures) | 8.8.0 | | alerting | | [plugin.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/plugin.ts#:~:text=license%24), [license_state.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/license_state.test.ts#:~:text=license%24), [license_state.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/lib/license_state.test.ts#:~:text=license%24), [plugin.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/actions/server/plugin.ts#:~:text=license%24), [license_state.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/actions/server/lib/license_state.test.ts#:~:text=license%24), [license_state.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/actions/server/lib/license_state.test.ts#:~:text=license%24) | 8.8.0 | +| stackAlerts | | [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter) | 8.1 | +| stackAlerts | | [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter) | 8.1 | +| stackAlerts | | [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter), [search_source_expression_form.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx#:~:text=Filter) | 8.1 | @@ -175,6 +169,8 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | Plugin | Deprecated API | Reference location(s) | Remove By | | --------|-------|-----------|-----------| +| securitySolution | | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch) | 8.1 | +| securitySolution | | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch) | 8.1 | | securitySolution | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [isolation.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts#:~:text=mode), [isolation.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts#:~:text=mode)+ 2 more | 8.8.0 | | securitySolution | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [isolation.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts#:~:text=mode), [isolation.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts#:~:text=mode)+ 2 more | 8.8.0 | | securitySolution | | [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/app/index.tsx#:~:text=onAppLeave) | 8.8.0 | @@ -204,17 +200,9 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | Plugin | Deprecated API | Reference location(s) | Remove By | | --------|-------|-----------|-----------| -| unifiedSearch | | [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IFieldType), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType)+ 25 more | 8.2 | +| unifiedSearch | | [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType) | 8.2 | | unifiedSearch | | [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=esFilters), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=esFilters), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=esFilters) | 8.1 | -| unifiedSearch | | [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [value.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts#:~:text=KueryNode)+ 2 more | 8.1 | -| unifiedSearch | | [use_filter_manager.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts#:~:text=Filter), [use_filter_manager.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [get_stub_filter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts#:~:text=Filter), [get_stub_filter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts#:~:text=Filter), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=Filter)+ 10 more | 8.1 | -| unifiedSearch | | [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=FILTERS), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=FILTERS), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=FILTERS), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=FILTERS), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=FILTERS), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=FILTERS) | 8.1 | -| unifiedSearch | | [filter_editor_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts#:~:text=toggleFilterNegated), [filter_editor_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts#:~:text=toggleFilterNegated), [filter_editor_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts#:~:text=toggleFilterNegated), [filter_editor_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts#:~:text=toggleFilterNegated), [filter_editor_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts#:~:text=toggleFilterNegated) | 8.1 | -| unifiedSearch | | [use_filter_manager.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts#:~:text=Filter), [use_filter_manager.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [get_stub_filter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts#:~:text=Filter), [get_stub_filter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts#:~:text=Filter), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=Filter)+ 10 more | 8.1 | -| unifiedSearch | | [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [value.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts#:~:text=KueryNode)+ 2 more | 8.1 | -| unifiedSearch | | [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IFieldType), [query_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType)+ 60 more | 8.2 | -| unifiedSearch | | [use_filter_manager.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts#:~:text=Filter), [use_filter_manager.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [apply_filter_action.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/actions/apply_filter_action.ts#:~:text=Filter), [get_stub_filter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts#:~:text=Filter), [get_stub_filter.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts#:~:text=Filter), [filter_label.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx#:~:text=Filter)+ 10 more | 8.1 | -| unifiedSearch | | [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [conjunction.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [operator.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts#:~:text=KueryNode), [value.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts#:~:text=KueryNode)+ 2 more | 8.1 | +| unifiedSearch | | [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value_suggestion_provider.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [value.ts](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts#:~:text=IFieldType), [field.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx#:~:text=IFieldType)+ 8 more | 8.2 | @@ -222,9 +210,9 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | Plugin | Deprecated API | Reference location(s) | Remove By | | --------|-------|-----------|-----------| -| lens | | [display_duplicate_title_confirm_modal.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/persistence/saved_objects_utils/display_duplicate_title_confirm_modal.ts#:~:text=SavedObject), [display_duplicate_title_confirm_modal.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/persistence/saved_objects_utils/display_duplicate_title_confirm_modal.ts#:~:text=SavedObject), [check_for_duplicate_title.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/persistence/saved_objects_utils/check_for_duplicate_title.ts#:~:text=SavedObject), [check_for_duplicate_title.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/persistence/saved_objects_utils/check_for_duplicate_title.ts#:~:text=SavedObject), [display_duplicate_title_confirm_modal.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_objects_utils/display_duplicate_title_confirm_modal.ts#:~:text=SavedObject), [display_duplicate_title_confirm_modal.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_objects_utils/display_duplicate_title_confirm_modal.ts#:~:text=SavedObject), [check_for_duplicate_title.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_objects_utils/check_for_duplicate_title.ts#:~:text=SavedObject), [check_for_duplicate_title.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_objects_utils/check_for_duplicate_title.ts#:~:text=SavedObject) | 8.8.0 | -| lens | | [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/app_plugin/types.ts#:~:text=onAppLeave), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/app_plugin/types.ts#:~:text=onAppLeave), [mounter.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/app_plugin/mounter.tsx#:~:text=onAppLeave), [visualize_top_nav.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx#:~:text=onAppLeave), [visualize_editor_common.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/components/visualize_editor_common.tsx#:~:text=onAppLeave), [app.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/app.tsx#:~:text=onAppLeave), [index.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/index.tsx#:~:text=onAppLeave) | 8.8.0 | -| lens | | [saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts#:~:text=warning), [saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts#:~:text=warning) | 8.8.0 | -| management | | [application.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/management/public/application.tsx#:~:text=appBasePath) | 8.8.0 | -| visTypeVega | | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/plugin.ts#:~:text=injectedMetadata) | 8.8.0 | -| visTypeVega | | [search_api.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/data_model/search_api.ts#:~:text=injectedMetadata), [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/vega/public/plugin.ts#:~:text=injectedMetadata) | 8.8.0 | \ No newline at end of file +| visualizations | | [display_duplicate_title_confirm_modal.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_objects_utils/display_duplicate_title_confirm_modal.ts#:~:text=SavedObject), [display_duplicate_title_confirm_modal.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_objects_utils/display_duplicate_title_confirm_modal.ts#:~:text=SavedObject), [check_for_duplicate_title.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_objects_utils/check_for_duplicate_title.ts#:~:text=SavedObject), [check_for_duplicate_title.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_objects_utils/check_for_duplicate_title.ts#:~:text=SavedObject), [display_duplicate_title_confirm_modal.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/persistence/saved_objects_utils/display_duplicate_title_confirm_modal.ts#:~:text=SavedObject), [display_duplicate_title_confirm_modal.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/persistence/saved_objects_utils/display_duplicate_title_confirm_modal.ts#:~:text=SavedObject), [check_for_duplicate_title.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/persistence/saved_objects_utils/check_for_duplicate_title.ts#:~:text=SavedObject), [check_for_duplicate_title.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/persistence/saved_objects_utils/check_for_duplicate_title.ts#:~:text=SavedObject) | 8.8.0 | +| visualizations | | [visualize_top_nav.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx#:~:text=onAppLeave), [visualize_editor_common.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/components/visualize_editor_common.tsx#:~:text=onAppLeave), [app.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/app.tsx#:~:text=onAppLeave), [index.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/visualize_app/index.tsx#:~:text=onAppLeave), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/app_plugin/types.ts#:~:text=onAppLeave), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/app_plugin/types.ts#:~:text=onAppLeave), [mounter.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/app_plugin/mounter.tsx#:~:text=onAppLeave) | 8.8.0 | +| lens | | [open_in_discover_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx#:~:text=Filter), [open_in_discover_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx#:~:text=Filter) | 8.1 | +| lens | | [open_in_discover_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx#:~:text=Filter), [open_in_discover_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx#:~:text=Filter) | 8.1 | +| lens | | [open_in_discover_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx#:~:text=Filter), [open_in_discover_drilldown.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx#:~:text=Filter) | 8.1 | +| management | | [application.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/management/public/application.tsx#:~:text=appBasePath) | 8.8.0 | \ No newline at end of file diff --git a/api_docs/kbn_handlebars.devdocs.json b/api_docs/kbn_handlebars.devdocs.json new file mode 100644 index 0000000000000..a7eaa1cd0c13c --- /dev/null +++ b/api_docs/kbn_handlebars.devdocs.json @@ -0,0 +1,153 @@ +{ + "id": "@kbn/handlebars", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.create", + "type": "Function", + "tags": [], + "label": "create", + "description": [ + "\nCreates an isolated Handlebars environment.\n\nEach environment has its own helpers.\nThis is only necessary for use cases that demand distinct helpers.\nMost use cases can use the root Handlebars environment directly.\n" + ], + "signature": [ + "() => typeof ", + { + "pluginId": "@kbn/handlebars", + "scope": "common", + "docId": "kibKbnHandlebarsPluginApi", + "section": "def-common.ExtendedHandlebars", + "text": "ExtendedHandlebars" + }, + " & typeof Handlebars" + ], + "path": "packages/kbn-handlebars/src/index.ts", + "deprecated": false, + "children": [], + "returnComment": [ + "A sandboxed/scoped version of the" + ], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.compileFnName", + "type": "CompoundType", + "tags": [], + "label": "compileFnName", + "description": [ + "\nIf the `unsafe-eval` CSP is set, this string constant will be `compile`,\notherwise `compileAST`.\n\nThis can be used to call the more optimized `compile` function in\nenvironments that support it, or fall back to `compileAST` on environments\nthat don't." + ], + "signature": [ + "\"compile\" | \"compileAST\"" + ], + "path": "packages/kbn-handlebars/src/index.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.ExtendedCompileOptions", + "type": "Type", + "tags": [], + "label": "ExtendedCompileOptions", + "description": [ + "\nSupported Handlebars compile options.\n\nThis is a subset of all the compile options supported by the upstream\nHandlebars module." + ], + "signature": [ + "{ data?: boolean | undefined; strict?: boolean | undefined; knownHelpers?: KnownHelpers | undefined; knownHelpersOnly?: boolean | undefined; assumeObjects?: boolean | undefined; noEscape?: boolean | undefined; }" + ], + "path": "packages/kbn-handlebars/src/index.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.ExtendedRuntimeOptions", + "type": "Type", + "tags": [], + "label": "ExtendedRuntimeOptions", + "description": [ + "\nSupported Handlebars runtime options\n\nThis is a subset of all the runtime options supported by the upstream\nHandlebars module." + ], + "signature": [ + "{ data?: any; helpers?: { [name: string]: Function; } | undefined; blockParams?: any[] | undefined; }" + ], + "path": "packages/kbn-handlebars/src/index.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.Handlebars", + "type": "CompoundType", + "tags": [], + "label": "Handlebars", + "description": [ + "\nA custom version of the Handlesbars module with an extra `compileAST` function." + ], + "signature": [ + "typeof ", + { + "pluginId": "@kbn/handlebars", + "scope": "common", + "docId": "kibKbnHandlebarsPluginApi", + "section": "def-common.ExtendedHandlebars", + "text": "ExtendedHandlebars" + }, + " & typeof Handlebars" + ], + "path": "packages/kbn-handlebars/src/index.ts", + "deprecated": false, + "initialIsOpen": false + } + ], + "objects": [ + { + "parentPluginId": "@kbn/handlebars", + "id": "def-common.ExtendedHandlebars", + "type": "Object", + "tags": [], + "label": "ExtendedHandlebars", + "description": [ + "\nNormally this namespace isn't used directly. It's required to be present by\nTypeScript when calling the `Handlebars.create()` function." + ], + "signature": [ + "typeof ", + { + "pluginId": "@kbn/handlebars", + "scope": "common", + "docId": "kibKbnHandlebarsPluginApi", + "section": "def-common.ExtendedHandlebars", + "text": "ExtendedHandlebars" + } + ], + "path": "packages/kbn-handlebars/src/index.ts", + "deprecated": false, + "initialIsOpen": false + } + ] + } +} \ No newline at end of file diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx new file mode 100644 index 0000000000000..79d09d800a950 --- /dev/null +++ b/api_docs/kbn_handlebars.mdx @@ -0,0 +1,33 @@ +--- +id: kibKbnHandlebarsPluginApi +slug: /kibana-dev-docs/api/kbn-handlebars +title: "@kbn/handlebars" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/handlebars plugin +date: 2022-05-23 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnHandlebarsObj from './kbn_handlebars.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 6 | 0 | 0 | 0 | + +## Common + +### Objects + + +### Functions + + +### Consts, variables and types + + diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index f43ac68f5f298..df36a234ba0ce 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -3,7 +3,7 @@ id: kibDevDocsPluginDirectory slug: /kibana-dev-docs/api-meta/plugin-api-directory title: Directory summary: Directory of public APIs available through plugins or packages. -date: 2022-04-26 +date: 2022-05-23 tags: ['contributor', 'dev', 'apidocs', 'kibana'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -12,123 +12,123 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Count | Plugins or Packages with a
public API | Number of teams | |--------------|----------|------------------------| -| 247 | 201 | 35 | +| 258 | 209 | 35 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 25958 | 170 | 19687 | 1153 | +| 26222 | 171 | 19770 | 870 | ## Plugin Directory | Plugin name           | Maintaining team | Description | API Cnt | Any Cnt | Missing
comments | Missing
exports | |--------------|----------------|-----------|--------------|----------|---------------|--------| -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 195 | 0 | 191 | 11 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 240 | 0 | 235 | 19 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 23 | 0 | 19 | 1 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 325 | 0 | 316 | 19 | -| | [APM UI](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 40 | 0 | 40 | 50 | +| | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | AIOps plugin maintained by ML team. | 12 | 0 | 0 | 0 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 359 | 0 | 350 | 19 | +| | [APM UI](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 40 | 0 | 40 | 51 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 9 | 0 | 9 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Considering using bfetch capabilities when fetching large amounts of data. This services supports batching HTTP requests and streaming responses back. | 78 | 1 | 69 | 2 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds Canvas application to Kibana | 9 | 0 | 8 | 3 | -| | [ResponseOps](https://github.com/orgs/elastic/teams/response-ops) | The Case management system in Kibana | 71 | 0 | 57 | 19 | +| | [ResponseOps](https://github.com/orgs/elastic/teams/response-ops) | The Case management system in Kibana | 66 | 0 | 52 | 22 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | - | 272 | 2 | 253 | 9 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 28 | 0 | 23 | 0 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 29 | 0 | 24 | 0 | | | [Cloud Security Posture](https://github.com/orgs/elastic/teams/cloud-posture-security) | The cloud security posture plugin | 14 | 0 | 14 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 13 | 0 | 13 | 1 | -| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Controls Plugin contains embeddable components intended to create a simple query interface for end users, and a powerful editing suite that allows dashboard authors to build controls | 203 | 0 | 197 | 6 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2524 | 15 | 977 | 33 | +| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Controls Plugin contains embeddable components intended to create a simple query interface for end users, and a powerful editing suite that allows dashboard authors to build controls | 205 | 0 | 199 | 7 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2541 | 15 | 977 | 33 | | crossClusterReplication | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | -| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | Add custom data integrations so they can be displayed in the Fleet integrations app | 98 | 0 | 79 | 1 | -| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds the Dashboard app to Kibana | 142 | 0 | 140 | 12 | +| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | Add custom data integrations so they can be displayed in the Fleet integrations app | 101 | 0 | 82 | 1 | +| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds the Dashboard app to Kibana | 143 | 0 | 141 | 12 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 52 | 0 | 51 | 0 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3414 | 38 | 2802 | 18 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Enhanced data plugin. (See src/plugins/data.) Enhances the main data plugin with a search session management UI. Includes a reusable search session indicator component to use in other applications. Exposes routes for managing search sessions. Includes a service that monitors, updates, and cleans up search session saved objects. | 16 | 0 | 16 | 2 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | This plugin provides the ability to create data views via a modal flyout from any kibana app | 13 | 0 | 7 | 0 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3482 | 38 | 2869 | 19 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | This plugin provides the ability to create data views via a modal flyout from any kibana app | 14 | 0 | 7 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Reusable data view field editor across Kibana | 42 | 0 | 37 | 3 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data view management app | 2 | 0 | 2 | 0 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 862 | 3 | 710 | 15 | -| | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | The Data Visualizer tools help you understand your data, by analyzing the metrics and fields in a log file or an existing Elasticsearch index. | 23 | 2 | 19 | 1 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 866 | 3 | 714 | 15 | +| | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | The Data Visualizer tools help you understand your data, by analyzing the metrics and fields in a log file or an existing Elasticsearch index. | 28 | 3 | 24 | 1 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 10 | 0 | 8 | 2 | -| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the Discover application and the saved search embeddable. | 76 | 0 | 60 | 7 | +| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the Discover application and the saved search embeddable. | 78 | 0 | 62 | 7 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 37 | 0 | 35 | 2 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds embeddables service to Kibana | 476 | 0 | 386 | 4 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds embeddables service to Kibana | 486 | 0 | 396 | 4 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Extends embeddable plugin with more functionality | 14 | 0 | 14 | 0 | -| | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides encryption and decryption utilities for saved objects containing sensitive information. | 48 | 0 | 44 | 0 | +| | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides encryption and decryption utilities for saved objects containing sensitive information. | 51 | 0 | 44 | 0 | | | [Enterprise Search](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | Adds dashboards for discovering and managing Enterprise Search products. | 2 | 0 | 2 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 112 | 3 | 108 | 3 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | The Event Annotation service contains expressions for event annotations | 49 | 0 | 49 | 3 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 91 | 0 | 91 | 9 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | The Event Annotation service contains expressions for event annotations | 90 | 0 | 90 | 5 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 100 | 0 | 100 | 9 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'error' renderer to expressions | 17 | 0 | 15 | 2 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression Gauge plugin adds a `gauge` renderer and function to the expression plugin. The renderer will display the `gauge` chart. | 61 | 0 | 61 | 2 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression Heatmap plugin adds a `heatmap` renderer and function to the expression plugin. The renderer will display the `heatmap` chart. | 104 | 0 | 100 | 3 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression Heatmap plugin adds a `heatmap` renderer and function to the expression plugin. The renderer will display the `heatmap` chart. | 107 | 0 | 103 | 3 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'image' function and renderer to expressions | 26 | 0 | 26 | 0 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'metric' function and renderer to expressions | 32 | 0 | 27 | 0 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression MetricVis plugin adds a `metric` renderer and function to the expression plugin. The renderer will display the `metric` chart. | 46 | 0 | 46 | 1 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression MetricVis plugin adds a `metric` renderer and function to the expression plugin. The renderer will display the `metric` chart. | 48 | 0 | 48 | 1 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression Partition Visualization plugin adds a `partitionVis` renderer and `pieVis`, `mosaicVis`, `treemapVis`, `waffleVis` functions to the expression plugin. The renderer will display the `pie`, `waffle`, `treemap` and `mosaic` charts. | 70 | 0 | 70 | 2 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'repeatImage' function and renderer to expressions | 32 | 0 | 32 | 0 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'revealImage' function and renderer to expressions | 14 | 0 | 14 | 3 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'shape' function and renderer to expressions | 148 | 0 | 146 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression Tagcloud plugin adds a `tagcloud` renderer and function to the expression plugin. The renderer will display the `Wordcloud` chart. | 7 | 0 | 7 | 0 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression XY plugin adds a `xy` renderer and function to the expression plugin. The renderer will display the `xy` chart. | 473 | 0 | 463 | 0 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds expression runtime to Kibana | 2158 | 17 | 1713 | 5 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression XY plugin adds a `xy` renderer and function to the expression plugin. The renderer will display the `xy` chart. | 143 | 0 | 133 | 14 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds expression runtime to Kibana | 2176 | 17 | 1722 | 5 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 222 | 0 | 95 | 2 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Index pattern fields and ambiguous values formatters | 286 | 6 | 247 | 3 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | The file upload plugin contains components and services for uploading a file, analyzing its data, and then importing the data into an Elasticsearch index. Supported file types include CSV, TSV, newline-delimited JSON and GeoJSON. | 62 | 0 | 62 | 2 | -| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1385 | 8 | 1268 | 10 | +| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1403 | 8 | 1281 | 10 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 68 | 0 | 14 | 5 | | globalSearchBar | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | | globalSearchProviders | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | | graph | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 0 | 0 | 0 | 0 | | grokdebugger | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 140 | 0 | 102 | 0 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 141 | 0 | 102 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 4 | 0 | 4 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 175 | 0 | 170 | 3 | -| | [Logs and Metrics UI](https://github.com/orgs/elastic/teams/logs-metrics-ui) | This plugin visualizes data from Filebeat and Metricbeat, and integrates with other Observability solutions | 31 | 0 | 28 | 5 | +| | [Logs and Metrics UI](https://github.com/orgs/elastic/teams/logs-metrics-ui) | This plugin visualizes data from Filebeat and Metricbeat, and integrates with other Observability solutions | 34 | 0 | 31 | 5 | | ingestPipelines | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | inputControlVis | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds Input Control visualization to Kibana | 0 | 0 | 0 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 123 | 2 | 96 | 4 | | | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides UI and APIs for the interactive setup mode. | 28 | 0 | 18 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 6 | 0 | 6 | 0 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 240 | 0 | 204 | 5 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 248 | 0 | 212 | 5 | | kibanaUsageCollection | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 0 | 0 | 0 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 615 | 3 | 420 | 9 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana. | 527 | 0 | 452 | 30 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana. | 574 | 0 | 497 | 35 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 8 | 0 | 8 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 3 | 0 | 3 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 117 | 0 | 42 | 10 | | | [Security detections response](https://github.com/orgs/elastic/teams/security-detections-response) | - | 198 | 0 | 90 | 49 | | logstash | [Logstash](https://github.com/orgs/elastic/teams/logstash) | - | 0 | 0 | 0 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | - | 41 | 0 | 41 | 6 | -| | [GIS](https://github.com/orgs/elastic/teams/kibana-gis) | - | 224 | 0 | 223 | 25 | +| | [GIS](https://github.com/orgs/elastic/teams/kibana-gis) | - | 226 | 0 | 225 | 25 | | | [GIS](https://github.com/orgs/elastic/teams/kibana-gis) | - | 67 | 0 | 67 | 0 | -| | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the machine learning features provided by Elastic. | 196 | 8 | 79 | 30 | +| | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the machine learning features provided by Elastic. | 254 | 10 | 81 | 31 | | | [Stack Monitoring](https://github.com/orgs/elastic/teams/stack-monitoring-ui) | - | 11 | 0 | 9 | 1 | | | [Stack Monitoring](https://github.com/orgs/elastic/teams/stack-monitoring-ui) | - | 9 | 0 | 9 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 34 | 0 | 34 | 2 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 17 | 0 | 17 | 0 | -| | [Observability UI](https://github.com/orgs/elastic/teams/observability-ui) | - | 373 | 2 | 370 | 30 | +| | [Observability UI](https://github.com/orgs/elastic/teams/observability-ui) | - | 376 | 2 | 373 | 30 | | | [Security asset management](https://github.com/orgs/elastic/teams/security-asset-management) | - | 13 | 0 | 13 | 0 | | painlessLab | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas). | 228 | 2 | 177 | 11 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 4 | 0 | 4 | 0 | | | [Kibana Reporting Services](https://github.com/orgs/elastic/teams/kibana-reporting-services) | Reporting Services enables applications to feature reports that the user can automate with Watcher and download later. | 36 | 0 | 16 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 21 | 0 | 21 | 0 | -| | [RAC](https://github.com/orgs/elastic/teams/rac) | - | 194 | 0 | 167 | 8 | +| | [RAC](https://github.com/orgs/elastic/teams/rac) | - | 202 | 0 | 174 | 9 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 24 | 0 | 19 | 2 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 192 | 2 | 151 | 5 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 110 | 0 | 97 | 0 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 54 | 0 | 50 | 0 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 76 | 0 | 70 | 3 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 90 | 0 | 45 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 32 | 0 | 13 | 0 | -| | [Kibana Reporting Services](https://github.com/orgs/elastic/teams/kibana-reporting-services) | Kibana Screenshotting Plugin | 27 | 0 | 7 | 4 | +| | [Kibana Reporting Services](https://github.com/orgs/elastic/teams/kibana-reporting-services) | Kibana Screenshotting Plugin | 28 | 0 | 8 | 4 | | searchprofiler | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides authentication and authorization features, and exposes functionality to understand the capabilities of the currently authenticated user. | 183 | 0 | 103 | 0 | | | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 46 | 0 | 46 | 19 | | | [Security Team](https://github.com/orgs/elastic/teams/security-team) | - | 3 | 0 | 3 | 1 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds URL Service and sharing capabilities to Kibana | 113 | 0 | 54 | 10 | | | [Shared UX](https://github.com/orgs/elastic/teams/shared-ux) | A plugin providing components and services for shared user experiences in Kibana. | 4 | 0 | 0 | 0 | -| | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 21 | 1 | 21 | 1 | +| | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 22 | 1 | 22 | 1 | | | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides the Spaces feature, which allows saved objects to be organized into meaningful categories. | 260 | 0 | 64 | 0 | | | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 4 | 0 | 4 | 0 | | synthetics | [Uptime](https://github.com/orgs/elastic/teams/uptime) | This plugin visualizes data from Synthetics and Heartbeat, and integrates with other Observability solutions. | 0 | 0 | 0 | 0 | @@ -137,13 +137,13 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 32 | 0 | 32 | 6 | | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 1 | 0 | 1 | 0 | | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 11 | 0 | 10 | 0 | -| | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 434 | 1 | 330 | 35 | +| | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 435 | 1 | 331 | 35 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. | 4 | 0 | 4 | 1 | | translations | [Kibana Localization](https://github.com/orgs/elastic/teams/kibana-localization) | - | 0 | 0 | 0 | 0 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 321 | 0 | 307 | 25 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 374 | 0 | 360 | 34 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds UI Actions service to Kibana | 130 | 0 | 91 | 11 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Extends UI Actions plugin with more functionality | 203 | 0 | 141 | 9 | -| | [Unified Search](https://github.com/orgs/elastic/teams/kibana-app-services) | Contains all the key functionality of Kibana's unified search experience.Contains all the key functionality of Kibana's unified search experience. | 79 | 2 | 75 | 11 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Extends UI Actions plugin with more functionality | 205 | 0 | 142 | 9 | +| | [Unified Search](https://github.com/orgs/elastic/teams/kibana-app-services) | Contains all the key functionality of Kibana's unified search experience.Contains all the key functionality of Kibana's unified search experience. | 82 | 2 | 78 | 13 | | upgradeAssistant | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | urlDrilldown | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds drilldown implementations to Kibana | 0 | 0 | 0 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | - | 12 | 0 | 12 | 0 | @@ -162,35 +162,40 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Registers the vega visualization. Is the elastic version of vega and vega-lite libraries. | 2 | 0 | 2 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the vislib visualizations. These are the classical area/line/bar, pie, gauge/goal and heatmap charts. We want to replace them with elastic-charts. | 26 | 0 | 25 | 1 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the new xy-axis chart using the elastic-charts library, which will eventually replace the vislib xy-axis charts including bar, area, and line. | 57 | 0 | 51 | 5 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the shared architecture among all the legacy visualizations, e.g. the visualization type registry or the visualization embeddable. | 365 | 12 | 344 | 14 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the shared architecture among all the legacy visualizations, e.g. the visualization type registry or the visualization embeddable. | 372 | 12 | 351 | 14 | | watcher | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | ## Package Directory | Package name           | Maintaining team | Description | API Cnt | Any Cnt | Missing
comments | Missing
exports | |--------------|----------------|-----------|--------------|----------|---------------|--------| -| | [Owner missing] | Elastic APM trace data generator | 70 | 0 | 70 | 10 | +| | [Owner missing] | Elastic APM trace data generator | 74 | 0 | 74 | 11 | | | [Owner missing] | - | 11 | 5 | 11 | 0 | | | [Owner missing] | Alerts components and hooks | 9 | 1 | 9 | 0 | | | Ahmad Bamieh ahmadbamieh@gmail.com | Kibana Analytics tool | 69 | 0 | 69 | 2 | -| | [Owner missing] | - | 88 | 1 | 10 | 0 | -| | [Owner missing] | - | 18 | 0 | 13 | 0 | +| | [Owner missing] | - | 95 | 0 | 7 | 0 | +| | [Owner missing] | - | 18 | 0 | 13 | 0 | +| | [Owner missing] | - | 22 | 0 | 13 | 0 | +| | [Owner missing] | - | 18 | 0 | 13 | 0 | +| | [Owner missing] | - | 20 | 0 | 4 | 0 | | | [Owner missing] | - | 16 | 0 | 16 | 0 | | | [Owner missing] | - | 11 | 0 | 11 | 0 | | | [Owner missing] | - | 10 | 0 | 10 | 0 | | | [Owner missing] | - | 18 | 0 | 9 | 1 | | | [Owner missing] | - | 4 | 0 | 4 | 0 | -| | [Owner missing] | - | 8 | 0 | 7 | 0 | | | [Owner missing] | - | 7 | 0 | 2 | 0 | -| | [Owner missing] | - | 59 | 0 | 15 | 0 | +| | [Owner missing] | - | 60 | 0 | 15 | 0 | | | [Owner missing] | - | 2 | 0 | 2 | 0 | | | [Owner missing] | - | 106 | 0 | 80 | 1 | -| | [Owner missing] | - | 64 | 0 | 44 | 2 | +| | [Owner missing] | - | 65 | 0 | 44 | 2 | | | [Owner missing] | - | 129 | 3 | 127 | 17 | | | [Owner missing] | - | 13 | 0 | 7 | 0 | | | [Owner missing] | elasticsearch datemath parser, used in kibana | 44 | 0 | 43 | 0 | -| | [Owner missing] | - | 120 | 3 | 109 | 0 | -| | [Owner missing] | - | 64 | 0 | 64 | 2 | +| | [Owner missing] | - | 9 | 1 | 9 | 0 | +| | [Owner missing] | - | 65 | 0 | 64 | 0 | +| | [Owner missing] | - | 15 | 0 | 9 | 0 | +| | [Owner missing] | - | 31 | 2 | 27 | 0 | +| | [Owner missing] | - | 65 | 0 | 65 | 2 | | | [Owner missing] | - | 1 | 0 | 1 | 0 | | | [Owner missing] | - | 27 | 0 | 14 | 1 | | | [Owner missing] | - | 213 | 1 | 159 | 11 | @@ -198,6 +203,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Owner missing] | - | 20 | 0 | 16 | 0 | | | [Owner missing] | - | 2 | 0 | 0 | 0 | | | [Owner missing] | - | 1 | 0 | 0 | 0 | +| | [Owner missing] | - | 6 | 0 | 0 | 0 | | | [Owner missing] | - | 51 | 0 | 48 | 0 | | | [Owner missing] | - | 43 | 0 | 36 | 0 | | | App Services | - | 35 | 4 | 35 | 0 | @@ -208,11 +214,12 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Owner missing] | - | 8 | 0 | 8 | 0 | | | [Owner missing] | - | 494 | 1 | 1 | 0 | | | [Owner missing] | - | 55 | 0 | 55 | 2 | -| | [Owner missing] | - | 45 | 0 | 45 | 10 | +| | [Owner missing] | - | 47 | 0 | 46 | 10 | +| | [Owner missing] | A library to convert APM traces into JSON format for performance testing. | 3 | 0 | 3 | 0 | | | [Owner missing] | - | 30 | 0 | 29 | 0 | | | [Owner missing] | - | 1 | 0 | 1 | 0 | | | [Owner missing] | Just some helpers for kibana plugin devs. | 1 | 0 | 1 | 0 | -| | [Owner missing] | - | 47 | 0 | 35 | 5 | +| | [Owner missing] | - | 45 | 0 | 33 | 5 | | | [Owner missing] | - | 21 | 0 | 10 | 0 | | | [Owner missing] | - | 74 | 0 | 71 | 0 | | | [Owner missing] | Security Solution auto complete | 50 | 1 | 35 | 0 | @@ -232,17 +239,18 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Owner missing] | - | 53 | 0 | 50 | 1 | | | [Owner missing] | - | 25 | 0 | 24 | 1 | | | [Owner missing] | - | 12 | 0 | 2 | 3 | -| | [Owner missing] | - | 34 | 0 | 6 | 6 | -| | [Owner missing] | - | 67 | 0 | 43 | 0 | -| | [Owner missing] | - | 10 | 0 | 2 | 0 | +| | [Owner missing] | - | 23 | 0 | 6 | 3 | +| | [Owner missing] | - | 8 | 0 | 2 | 3 | +| | [Owner missing] | - | 78 | 0 | 49 | 2 | +| | [Owner missing] | - | 16 | 0 | 7 | 0 | | | [Owner missing] | - | 9 | 0 | 3 | 0 | | | [Owner missing] | - | 2 | 0 | 2 | 0 | | | [Owner missing] | - | 92 | 1 | 59 | 1 | | | [Owner missing] | - | 4 | 0 | 2 | 0 | -| | Operations | - | 22 | 2 | 21 | 0 | +| | Operations | - | 38 | 2 | 21 | 0 | | | [Owner missing] | - | 2 | 0 | 2 | 0 | -| | Operations | - | 252 | 6 | 214 | 9 | -| | [Owner missing] | - | 132 | 8 | 103 | 2 | +| | Operations | - | 240 | 5 | 201 | 9 | +| | [Owner missing] | - | 135 | 8 | 103 | 2 | | | [Owner missing] | - | 72 | 0 | 55 | 0 | | | [Owner missing] | - | 29 | 0 | 2 | 0 | | | [Owner missing] | - | 83 | 0 | 83 | 1 | diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 79421e1ac90b5..24d62505e99e8 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -26,12 +26,19 @@ which may cause a delay before pages start being served. Set to `false` to disable Console. *Default: `true`* | `csp.rules:` - | deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."] +| deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."] A https://w3c.github.io/webappsec-csp/[Content Security Policy] template that disables certain unnecessary and potentially insecure capabilities in the browser. It is strongly recommended that you keep the default CSP rules that ship with {kib}. +| `csp.disableUnsafeEval` +| experimental[] Set this to `true` to remove the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#unsafe_eval_expressions[`unsafe-eval`] source expression from the `script-src` directive. *Default: `false`* + +By enabling `csp.disableUnsafeEval`, Kibana will use a custom version of the Handlebars template library which doesn't support https://handlebarsjs.com/guide/partials.html#inline-partials[inline partials]. +Handlebars is used in various locations in the Kibana frontend where custom templates can be supplied by the user when for instance setting up a visualisation. +If you experience any issues rendering Handlebars templates after turning on `csp.disableUnsafeEval`, or if you rely on inline partials, please revert this setting to `false` and https://github.com/elastic/kibana/issues/new/choose[open an issue] in the Kibana GitHub repository. + | `csp.script_src:` | Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src[Content Security Policy `script-src` directive]. diff --git a/package.json b/package.json index 7cc2500e8dd6e..f5c8a68efbf30 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "@kbn/eslint-plugin-imports": "link:bazel-bin/packages/kbn-eslint-plugin-imports", "@kbn/field-types": "link:bazel-bin/packages/kbn-field-types", "@kbn/flot-charts": "link:bazel-bin/packages/kbn-flot-charts", + "@kbn/handlebars": "link:bazel-bin/packages/kbn-handlebars", "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n", "@kbn/i18n-react": "link:bazel-bin/packages/kbn-i18n-react", "@kbn/interpreter": "link:bazel-bin/packages/kbn-interpreter", @@ -646,6 +647,7 @@ "@types/kbn__field-types": "link:bazel-bin/packages/kbn-field-types/npm_module_types", "@types/kbn__find-used-node-modules": "link:bazel-bin/packages/kbn-find-used-node-modules/npm_module_types", "@types/kbn__generate": "link:bazel-bin/packages/kbn-generate/npm_module_types", + "@types/kbn__handlebars": "link:bazel-bin/packages/kbn-handlebars/npm_module_types", "@types/kbn__i18n": "link:bazel-bin/packages/kbn-i18n/npm_module_types", "@types/kbn__i18n-react": "link:bazel-bin/packages/kbn-i18n-react/npm_module_types", "@types/kbn__import-resolver": "link:bazel-bin/packages/kbn-import-resolver/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 51db32d5d89f7..cc8925bc777c2 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -53,6 +53,7 @@ filegroup( "//packages/kbn-find-used-node-modules:build", "//packages/kbn-flot-charts:build", "//packages/kbn-generate:build", + "//packages/kbn-handlebars:build", "//packages/kbn-i18n-react:build", "//packages/kbn-i18n:build", "//packages/kbn-import-resolver:build", @@ -159,6 +160,7 @@ filegroup( "//packages/kbn-field-types:build_types", "//packages/kbn-find-used-node-modules:build_types", "//packages/kbn-generate:build_types", + "//packages/kbn-handlebars:build_types", "//packages/kbn-i18n-react:build_types", "//packages/kbn-i18n:build_types", "//packages/kbn-import-resolver:build_types", diff --git a/packages/kbn-handlebars/.gitignore b/packages/kbn-handlebars/.gitignore new file mode 100644 index 0000000000000..d36977dc47615 --- /dev/null +++ b/packages/kbn-handlebars/.gitignore @@ -0,0 +1 @@ +.tmp diff --git a/packages/kbn-handlebars/.patches/basic.patch b/packages/kbn-handlebars/.patches/basic.patch new file mode 100644 index 0000000000000..90027eedf0f40 --- /dev/null +++ b/packages/kbn-handlebars/.patches/basic.patch @@ -0,0 +1,612 @@ +1,11c1,21 +< global.handlebarsEnv = null; +< +< beforeEach(function() { +< global.handlebarsEnv = Handlebars.create(); +< }); +< +< describe('basic context', function() { +< it('most basic', function() { +< expectTemplate('{{foo}}') +< .withInput({ foo: 'foo' }) +< .toCompileTo('foo'); +--- +> /* +> * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/kbn-handlebars/LICENSE` for more information. +> */ +> +> import Handlebars from '..'; +> import { expectTemplate } from '../__jest__/test_bench'; +> +> describe('basic context', () => { +> it('most basic', () => { +> expectTemplate('{{foo}}').withInput({ foo: 'foo' }).toCompileTo('foo'); +> }); +> +> it('escaping', () => { +> expectTemplate('\\{{foo}}').withInput({ foo: 'food' }).toCompileTo('{{foo}}'); +> expectTemplate('content \\{{foo}}').withInput({ foo: 'food' }).toCompileTo('content {{foo}}'); +> expectTemplate('\\\\{{foo}}').withInput({ foo: 'food' }).toCompileTo('\\food'); +> expectTemplate('content \\\\{{foo}}').withInput({ foo: 'food' }).toCompileTo('content \\food'); +> expectTemplate('\\\\ {{foo}}').withInput({ foo: 'food' }).toCompileTo('\\\\ food'); +14,36c24 +< it('escaping', function() { +< expectTemplate('\\{{foo}}') +< .withInput({ foo: 'food' }) +< .toCompileTo('{{foo}}'); +< +< expectTemplate('content \\{{foo}}') +< .withInput({ foo: 'food' }) +< .toCompileTo('content {{foo}}'); +< +< expectTemplate('\\\\{{foo}}') +< .withInput({ foo: 'food' }) +< .toCompileTo('\\food'); +< +< expectTemplate('content \\\\{{foo}}') +< .withInput({ foo: 'food' }) +< .toCompileTo('content \\food'); +< +< expectTemplate('\\\\ {{foo}}') +< .withInput({ foo: 'food' }) +< .toCompileTo('\\\\ food'); +< }); +< +< it('compiling with a basic context', function() { +--- +> it('compiling with a basic context', () => { +40c28 +< world: 'world' +--- +> world: 'world', +42d29 +< .withMessage('It works if all the required keys are provided') +46,49c33,34 +< it('compiling with a string context', function() { +< expectTemplate('{{.}}{{length}}') +< .withInput('bye') +< .toCompileTo('bye3'); +--- +> it('compiling with a string context', () => { +> expectTemplate('{{.}}{{length}}').withInput('bye').toCompileTo('bye3'); +52c37 +< it('compiling with an undefined context', function() { +--- +> it('compiling with an undefined context', () => { +62c47 +< it('comments', function() { +--- +> it('comments', () => { +66c51 +< world: 'world' +--- +> world: 'world', +68d52 +< .withMessage('comments are ignored') +72,76c56 +< +< expectTemplate(' {{~!-- long-comment --~}} blah').toCompileTo( +< 'blah' +< ); +< +--- +> expectTemplate(' {{~!-- long-comment --~}} blah').toCompileTo('blah'); +78,82c58 +< +< expectTemplate(' {{!-- long-comment --~}} blah').toCompileTo( +< ' blah' +< ); +< +--- +> expectTemplate(' {{!-- long-comment --~}} blah').toCompileTo(' blah'); +84,87c60 +< +< expectTemplate(' {{~!-- long-comment --}} blah').toCompileTo( +< ' blah' +< ); +--- +> expectTemplate(' {{~!-- long-comment --}} blah').toCompileTo(' blah'); +90,91c63,64 +< it('boolean', function() { +< var string = '{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!'; +--- +> it('boolean', () => { +> const string = '{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!'; +95c68 +< world: 'world' +--- +> world: 'world', +97d69 +< .withMessage('booleans show the contents when true') +103c75 +< world: 'world' +--- +> world: 'world', +105d76 +< .withMessage('booleans do not show the contents when false') +109c80 +< it('zeros', function() { +--- +> it('zeros', () => { +113c84 +< num2: 0 +--- +> num2: 0, +117,119c88 +< expectTemplate('num: {{.}}') +< .withInput(0) +< .toCompileTo('num: 0'); +--- +> expectTemplate('num: {{.}}').withInput(0).toCompileTo('num: 0'); +126c95 +< it('false', function() { +--- +> it('false', () => { +131c100 +< val2: new Boolean(false) +--- +> val2: new Boolean(false), +135,137c104 +< expectTemplate('val: {{.}}') +< .withInput(false) +< .toCompileTo('val: false'); +--- +> expectTemplate('val: {{.}}').withInput(false).toCompileTo('val: false'); +146c113 +< val2: new Boolean(false) +--- +> val2: new Boolean(false), +156c123 +< it('should handle undefined and null', function() { +--- +> it('should handle undefined and null', () => { +159,167c126,128 +< awesome: function(_undefined, _null, options) { +< return ( +< (_undefined === undefined) + +< ' ' + +< (_null === null) + +< ' ' + +< typeof options +< ); +< } +--- +> awesome(_undefined: any, _null: any, options: any) { +> return (_undefined === undefined) + ' ' + (_null === null) + ' ' + typeof options; +> }, +173c134 +< undefined: function() { +--- +> undefined() { +175c136 +< } +--- +> }, +181c142 +< null: function() { +--- +> null() { +183c144 +< } +--- +> }, +188c149 +< it('newlines', function() { +--- +> it('newlines', () => { +190d150 +< +194,223c154,160 +< it('escaping text', function() { +< expectTemplate("Awesome's") +< .withMessage( +< "text is escaped so that it doesn't get caught on single quotes" +< ) +< .toCompileTo("Awesome's"); +< +< expectTemplate('Awesome\\') +< .withMessage("text is escaped so that the closing quote can't be ignored") +< .toCompileTo('Awesome\\'); +< +< expectTemplate('Awesome\\\\ foo') +< .withMessage("text is escaped so that it doesn't mess up backslashes") +< .toCompileTo('Awesome\\\\ foo'); +< +< expectTemplate('Awesome {{foo}}') +< .withInput({ foo: '\\' }) +< .withMessage("text is escaped so that it doesn't mess up backslashes") +< .toCompileTo('Awesome \\'); +< +< expectTemplate(" ' ' ") +< .withMessage('double quotes never produce invalid javascript') +< .toCompileTo(" ' ' "); +< }); +< +< it('escaping expressions', function() { +< expectTemplate('{{{awesome}}}') +< .withInput({ awesome: "&'\\<>" }) +< .withMessage("expressions with 3 handlebars aren't escaped") +< .toCompileTo("&'\\<>"); +--- +> it('escaping text', () => { +> expectTemplate("Awesome's").toCompileTo("Awesome's"); +> expectTemplate('Awesome\\').toCompileTo('Awesome\\'); +> expectTemplate('Awesome\\\\ foo').toCompileTo('Awesome\\\\ foo'); +> expectTemplate('Awesome {{foo}}').withInput({ foo: '\\' }).toCompileTo('Awesome \\'); +> expectTemplate(" ' ' ").toCompileTo(" ' ' "); +> }); +225,228c162,165 +< expectTemplate('{{&awesome}}') +< .withInput({ awesome: "&'\\<>" }) +< .withMessage("expressions with {{& handlebars aren't escaped") +< .toCompileTo("&'\\<>"); +--- +> it('escaping expressions', () => { +> expectTemplate('{{{awesome}}}').withInput({ awesome: "&'\\<>" }).toCompileTo("&'\\<>"); +> +> expectTemplate('{{&awesome}}').withInput({ awesome: "&'\\<>" }).toCompileTo("&'\\<>"); +232d168 +< .withMessage('by default expressions should be escaped') +237d172 +< .withMessage('escaping should properly handle amperstands') +241c176 +< it("functions returning safestrings shouldn't be escaped", function() { +--- +> it("functions returning safestrings shouldn't be escaped", () => { +244c179 +< awesome: function() { +--- +> awesome() { +246c181 +< } +--- +> }, +248d182 +< .withMessage("functions returning safestrings aren't escaped") +252c186 +< it('functions', function() { +--- +> it('functions', () => { +255c189 +< awesome: function() { +--- +> awesome() { +257c191 +< } +--- +> }, +259d192 +< .withMessage('functions are called and render their output') +264c197 +< awesome: function() { +--- +> awesome() { +267c200 +< more: 'More awesome' +--- +> more: 'More awesome', +269d201 +< .withMessage('functions are bound to the context') +273c205 +< it('functions with context argument', function() { +--- +> it('functions with context argument', () => { +276c208 +< awesome: function(context) { +--- +> awesome(context: any) { +279c211 +< frank: 'Frank' +--- +> frank: 'Frank', +281d212 +< .withMessage('functions are called with context arguments') +285c216 +< it('pathed functions with context argument', function() { +--- +> it('pathed functions with context argument', () => { +289c220 +< awesome: function(context) { +--- +> awesome(context: any) { +291c222 +< } +--- +> }, +293c224 +< frank: 'Frank' +--- +> frank: 'Frank', +295d225 +< .withMessage('functions are called with context arguments') +299c229 +< it('depthed functions with context argument', function() { +--- +> it('depthed functions with context argument', () => { +302c232 +< awesome: function(context) { +--- +> awesome(context: any) { +305c235 +< frank: 'Frank' +--- +> frank: 'Frank', +307d236 +< .withMessage('functions are called with context arguments') +311c240 +< it('block functions with context argument', function() { +--- +> it('block functions with context argument', () => { +314c243 +< awesome: function(context, options) { +--- +> awesome(context: any, options: any) { +316c245 +< } +--- +> }, +318d246 +< .withMessage('block functions are called with context and options') +322,325c250,251 +< it('depthed block functions with context argument', function() { +< expectTemplate( +< '{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}' +< ) +--- +> it('depthed block functions with context argument', () => { +> expectTemplate('{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}') +328c254 +< awesome: function(context, options) { +--- +> awesome(context: any, options: any) { +330c256 +< } +--- +> }, +332d257 +< .withMessage('block functions are called with context and options') +336c261 +< it('block functions without context argument', function() { +--- +> it('block functions without context argument', () => { +339c264 +< awesome: function(options) { +--- +> awesome(options: any) { +341c266 +< } +--- +> }, +343d267 +< .withMessage('block functions are called with options') +347c271 +< it('pathed block functions without context argument', function() { +--- +> it('pathed block functions without context argument', () => { +351c275 +< awesome: function() { +--- +> awesome() { +353,354c277,278 +< } +< } +--- +> }, +> }, +356d279 +< .withMessage('block functions are called with options') +360,363c283,284 +< it('depthed block functions without context argument', function() { +< expectTemplate( +< '{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}' +< ) +--- +> it('depthed block functions without context argument', () => { +> expectTemplate('{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}') +366c287 +< awesome: function() { +--- +> awesome() { +368c289 +< } +--- +> }, +370d290 +< .withMessage('block functions are called with options') +374,378c294,295 +< it('paths with hyphens', function() { +< expectTemplate('{{foo-bar}}') +< .withInput({ 'foo-bar': 'baz' }) +< .withMessage('Paths can contain hyphens (-)') +< .toCompileTo('baz'); +--- +> it('paths with hyphens', () => { +> expectTemplate('{{foo-bar}}').withInput({ 'foo-bar': 'baz' }).toCompileTo('baz'); +382d298 +< .withMessage('Paths can contain hyphens (-)') +387d302 +< .withMessage('Paths can contain hyphens (-)') +391c306 +< it('nested paths', function() { +--- +> it('nested paths', () => { +394d308 +< .withMessage('Nested paths access nested objects') +398c312 +< it('nested paths with empty string value', function() { +--- +> it('nested paths with empty string value', () => { +401d314 +< .withMessage('Nested paths access nested objects with empty string') +405c318 +< it('literal paths', function() { +--- +> it('literal paths', () => { +408d320 +< .withMessage('Literal paths can be used') +413d324 +< .withMessage('Literal paths can be used') +417c328 +< it('literal references', function() { +--- +> it('literal references', () => { +443c354 +< it("that current context path ({{.}}) doesn't hit helpers", function() { +--- +> it("that current context path ({{.}}) doesn't hit helpers", () => { +445a357 +> // @ts-expect-error Setting the helper to a string instead of a function doesn't make sense normally, but here it doesn't matter +450c362 +< it('complex but empty paths', function() { +--- +> it('complex but empty paths', () => { +455,457c367 +< expectTemplate('{{person/name}}') +< .withInput({ person: {} }) +< .toCompileTo(''); +--- +> expectTemplate('{{person/name}}').withInput({ person: {} }).toCompileTo(''); +460c370 +< it('this keyword in paths', function() { +--- +> it('this keyword in paths', () => { +463d372 +< .withMessage('This keyword in paths evaluates to current context') +468c377 +< hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }] +--- +> hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }], +470d378 +< .withMessage('This keyword evaluates in more complex paths') +474c382 +< it('this keyword nested inside path', function() { +--- +> it('this keyword nested inside path', () => { +476d383 +< Error, +480,482c387 +< expectTemplate('{{[this]}}') +< .withInput({ this: 'bar' }) +< .toCompileTo('bar'); +--- +> expectTemplate('{{[this]}}').withInput({ this: 'bar' }).toCompileTo('bar'); +489,491c394,396 +< it('this keyword in helpers', function() { +< var helpers = { +< foo: function(value) { +--- +> it('this keyword in helpers', () => { +> const helpers = { +> foo(value: any) { +493c398 +< } +--- +> }, +499d403 +< .withMessage('This keyword in paths evaluates to current context') +504c408 +< hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }] +--- +> hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }], +507d410 +< .withMessage('This keyword evaluates in more complex paths') +511c414 +< it('this keyword nested inside helpers param', function() { +--- +> it('this keyword nested inside helpers param', () => { +513d415 +< Error, +519c421 +< foo: function(value) { +--- +> foo(value: any) { +522c424 +< this: 'bar' +--- +> this: 'bar', +528c430 +< foo: function(value) { +--- +> foo(value: any) { +531c433 +< text: { this: 'bar' } +--- +> text: { this: 'bar' }, +536c438 +< it('pass string literals', function() { +--- +> it('pass string literals', () => { +538,541c440 +< +< expectTemplate('{{"foo"}}') +< .withInput({ foo: 'bar' }) +< .toCompileTo('bar'); +--- +> expectTemplate('{{"foo"}}').withInput({ foo: 'bar' }).toCompileTo('bar'); +545c444 +< foo: ['bar', 'baz'] +--- +> foo: ['bar', 'baz'], +550c449 +< it('pass number literals', function() { +--- +> it('pass number literals', () => { +552,556c451 +< +< expectTemplate('{{12}}') +< .withInput({ '12': 'bar' }) +< .toCompileTo('bar'); +< +--- +> expectTemplate('{{12}}').withInput({ '12': 'bar' }).toCompileTo('bar'); +558,562c453 +< +< expectTemplate('{{12.34}}') +< .withInput({ '12.34': 'bar' }) +< .toCompileTo('bar'); +< +--- +> expectTemplate('{{12.34}}').withInput({ '12.34': 'bar' }).toCompileTo('bar'); +565c456 +< '12.34': function(arg) { +--- +> '12.34'(arg: any) { +567c458 +< } +--- +> }, +572c463 +< it('pass boolean literals', function() { +--- +> it('pass boolean literals', () => { +574,581c465,466 +< +< expectTemplate('{{true}}') +< .withInput({ '': 'foo' }) +< .toCompileTo(''); +< +< expectTemplate('{{false}}') +< .withInput({ false: 'foo' }) +< .toCompileTo('foo'); +--- +> expectTemplate('{{true}}').withInput({ '': 'foo' }).toCompileTo(''); +> expectTemplate('{{false}}').withInput({ false: 'foo' }).toCompileTo('foo'); +584c469 +< it('should handle literals in subexpression', function() { +--- +> it('should handle literals in subexpression', () => { +587c472 +< false: function() { +--- +> false() { +589c474 +< } +--- +> }, +591c476 +< .withHelper('foo', function(arg) { +--- +> .withHelper('foo', function (arg) { diff --git a/packages/kbn-handlebars/.patches/blocks.patch b/packages/kbn-handlebars/.patches/blocks.patch new file mode 100644 index 0000000000000..731ee90e6e9b8 --- /dev/null +++ b/packages/kbn-handlebars/.patches/blocks.patch @@ -0,0 +1,461 @@ +1,3c1,12 +< describe('blocks', function() { +< it('array', function() { +< var string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!'; +--- +> /* +> * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/kbn-handlebars/LICENSE` for more information. +> */ +> +> import { expectTemplate } from '../__jest__/test_bench'; +> +> describe('blocks', () => { +> it('array', () => { +> const string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!'; +7,12c16,17 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ], +< world: 'world' +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +> world: 'world', +14d18 +< .withMessage('Arrays iterate over the contents when not empty') +20c24 +< world: 'world' +--- +> world: 'world', +22d25 +< .withMessage('Arrays ignore the contents when empty') +26,29c29,30 +< it('array without data', function() { +< expectTemplate( +< '{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}' +< ) +--- +> it('array without data', () => { +> expectTemplate('{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}') +31,36c32,33 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ], +< world: 'world' +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +> world: 'world', +38d34 +< .withCompileOptions({ compat: false }) +42,45c38,39 +< it('array with @index', function() { +< expectTemplate( +< '{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!' +< ) +--- +> it('array with @index', () => { +> expectTemplate('{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!') +47,52c41,42 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ], +< world: 'world' +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +> world: 'world', +54d43 +< .withMessage('The @index variable is used') +58,59c47,48 +< it('empty block', function() { +< var string = '{{#goodbyes}}{{/goodbyes}}cruel {{world}}!'; +--- +> it('empty block', () => { +> const string = '{{#goodbyes}}{{/goodbyes}}cruel {{world}}!'; +63,68c52,53 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ], +< world: 'world' +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +> world: 'world', +70d54 +< .withMessage('Arrays iterate over the contents when not empty') +76c60 +< world: 'world' +--- +> world: 'world', +78d61 +< .withMessage('Arrays ignore the contents when empty') +82c65 +< it('block with complex lookup', function() { +--- +> it('block with complex lookup', () => { +86,90c69 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ] +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +92,97c71 +< .withMessage( +< 'Templates can access variables in contexts up the stack with relative path syntax' +< ) +< .toCompileTo( +< 'goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ' +< ); +--- +> .toCompileTo('goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! '); +100c74 +< it('multiple blocks with complex lookup', function() { +--- +> it('multiple blocks with complex lookup', () => { +104,108c78 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ] +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +113,116c83,84 +< it('block with complex lookup using nested context', function() { +< expectTemplate( +< '{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}' +< ).toThrow(Error); +--- +> it('block with complex lookup using nested context', () => { +> expectTemplate('{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}').toThrow(Error); +119c87 +< it('block with deep nested complex lookup', function() { +--- +> it('block with deep nested complex lookup', () => { +125c93 +< outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }] +--- +> outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }], +130,133c98,99 +< it('works with cached blocks', function() { +< expectTemplate( +< '{{#each person}}{{#with .}}{{first}} {{last}}{{/with}}{{/each}}' +< ) +--- +> it('works with cached blocks', () => { +> expectTemplate('{{#each person}}{{#with .}}{{first}} {{last}}{{/with}}{{/each}}') +138,139c104,105 +< { first: 'Alan', last: 'Johnson' } +< ] +--- +> { first: 'Alan', last: 'Johnson' }, +> ], +144,145c110,111 +< describe('inverted sections', function() { +< it('inverted sections with unset value', function() { +--- +> describe('inverted sections', () => { +> it('inverted sections with unset value', () => { +148,150c114 +< ) +< .withMessage("Inverted section rendered when value isn't set.") +< .toCompileTo('Right On!'); +--- +> ).toCompileTo('Right On!'); +153,156c117,118 +< it('inverted section with false value', function() { +< expectTemplate( +< '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}' +< ) +--- +> it('inverted section with false value', () => { +> expectTemplate('{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}') +158d119 +< .withMessage('Inverted section rendered when value is false.') +162,165c123,124 +< it('inverted section with empty set', function() { +< expectTemplate( +< '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}' +< ) +--- +> it('inverted section with empty set', () => { +> expectTemplate('{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}') +167d125 +< .withMessage('Inverted section rendered when value is empty set.') +171c129 +< it('block inverted sections', function() { +--- +> it('block inverted sections', () => { +177c135 +< it('chained inverted sections', function() { +--- +> it('chained inverted sections', () => { +188,190c146 +< expectTemplate( +< '{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}' +< ) +--- +> expectTemplate('{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}') +195,198c151,152 +< it('chained inverted sections with mismatch', function() { +< expectTemplate( +< '{{#people}}{{name}}{{else if none}}{{none}}{{/if}}' +< ).toThrow(Error); +--- +> it('chained inverted sections with mismatch', () => { +> expectTemplate('{{#people}}{{name}}{{else if none}}{{none}}{{/if}}').toThrow(Error); +201c155 +< it('block inverted sections with empty arrays', function() { +--- +> it('block inverted sections with empty arrays', () => { +205c159 +< people: [] +--- +> people: [], +211,212c165,166 +< describe('standalone sections', function() { +< it('block standalone else sections', function() { +--- +> describe('standalone sections', () => { +> it('block standalone else sections', () => { +226,241c180,181 +< it('block standalone else sections can be disabled', function() { +< expectTemplate('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n') +< .withInput({ none: 'No people' }) +< .withCompileOptions({ ignoreStandalone: true }) +< .toCompileTo('\nNo people\n\n'); +< +< expectTemplate('{{#none}}\n{{.}}\n{{^}}\nFail\n{{/none}}\n') +< .withInput({ none: 'No people' }) +< .withCompileOptions({ ignoreStandalone: true }) +< .toCompileTo('\nNo people\n\n'); +< }); +< +< it('block standalone chained else sections', function() { +< expectTemplate( +< '{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n' +< ) +--- +> it('block standalone chained else sections', () => { +> expectTemplate('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n') +245,247c185 +< expectTemplate( +< '{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n' +< ) +--- +> expectTemplate('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n') +252c190 +< it('should handle nesting', function() { +--- +> it('should handle nesting', () => { +255c193 +< data: [1, 3, 5] +--- +> data: [1, 3, 5], +260,455d197 +< +< describe('compat mode', function() { +< it('block with deep recursive lookup lookup', function() { +< expectTemplate( +< '{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}' +< ) +< .withInput({ omg: 'OMG!', outer: [{ inner: [{ text: 'goodbye' }] }] }) +< .withCompileOptions({ compat: true }) +< .toCompileTo('Goodbye cruel OMG!'); +< }); +< +< it('block with deep recursive pathed lookup', function() { +< expectTemplate( +< '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}' +< ) +< .withInput({ +< omg: { yes: 'OMG!' }, +< outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }] +< }) +< .withCompileOptions({ compat: true }) +< .toCompileTo('Goodbye cruel OMG!'); +< }); +< +< it('block with missed recursive lookup', function() { +< expectTemplate( +< '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}' +< ) +< .withInput({ +< omg: { no: 'OMG!' }, +< outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }] +< }) +< .withCompileOptions({ compat: true }) +< .toCompileTo('Goodbye cruel '); +< }); +< }); +< +< describe('decorators', function() { +< it('should apply mustache decorators', function() { +< expectTemplate('{{#helper}}{{*decorator}}{{/helper}}') +< .withHelper('helper', function(options) { +< return options.fn.run; +< }) +< .withDecorator('decorator', function(fn) { +< fn.run = 'success'; +< return fn; +< }) +< .toCompileTo('success'); +< }); +< +< it('should apply allow undefined return', function() { +< expectTemplate('{{#helper}}{{*decorator}}suc{{/helper}}') +< .withHelper('helper', function(options) { +< return options.fn() + options.fn.run; +< }) +< .withDecorator('decorator', function(fn) { +< fn.run = 'cess'; +< }) +< .toCompileTo('success'); +< }); +< +< it('should apply block decorators', function() { +< expectTemplate( +< '{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}' +< ) +< .withHelper('helper', function(options) { +< return options.fn.run; +< }) +< .withDecorator('decorator', function(fn, props, container, options) { +< fn.run = options.fn(); +< return fn; +< }) +< .toCompileTo('success'); +< }); +< +< it('should support nested decorators', function() { +< expectTemplate( +< '{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}' +< ) +< .withHelper('helper', function(options) { +< return options.fn.run; +< }) +< .withDecorators({ +< decorator: function(fn, props, container, options) { +< fn.run = options.fn.nested + options.fn(); +< return fn; +< }, +< nested: function(fn, props, container, options) { +< props.nested = options.fn(); +< } +< }) +< .toCompileTo('success'); +< }); +< +< it('should apply multiple decorators', function() { +< expectTemplate( +< '{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}' +< ) +< .withHelper('helper', function(options) { +< return options.fn.run; +< }) +< .withDecorator('decorator', function(fn, props, container, options) { +< fn.run = (fn.run || '') + options.fn(); +< return fn; +< }) +< .toCompileTo('success'); +< }); +< +< it('should access parent variables', function() { +< expectTemplate('{{#helper}}{{*decorator foo}}{{/helper}}') +< .withHelper('helper', function(options) { +< return options.fn.run; +< }) +< .withDecorator('decorator', function(fn, props, container, options) { +< fn.run = options.args; +< return fn; +< }) +< .withInput({ foo: 'success' }) +< .toCompileTo('success'); +< }); +< +< it('should work with root program', function() { +< var run; +< expectTemplate('{{*decorator "success"}}') +< .withDecorator('decorator', function(fn, props, container, options) { +< equals(options.args[0], 'success'); +< run = true; +< return fn; +< }) +< .withInput({ foo: 'success' }) +< .toCompileTo(''); +< equals(run, true); +< }); +< +< it('should fail when accessing variables from root', function() { +< var run; +< expectTemplate('{{*decorator foo}}') +< .withDecorator('decorator', function(fn, props, container, options) { +< equals(options.args[0], undefined); +< run = true; +< return fn; +< }) +< .withInput({ foo: 'fail' }) +< .toCompileTo(''); +< equals(run, true); +< }); +< +< describe('registration', function() { +< it('unregisters', function() { +< handlebarsEnv.decorators = {}; +< +< handlebarsEnv.registerDecorator('foo', function() { +< return 'fail'; +< }); +< +< equals(!!handlebarsEnv.decorators.foo, true); +< handlebarsEnv.unregisterDecorator('foo'); +< equals(handlebarsEnv.decorators.foo, undefined); +< }); +< +< it('allows multiple globals', function() { +< handlebarsEnv.decorators = {}; +< +< handlebarsEnv.registerDecorator({ +< foo: function() {}, +< bar: function() {} +< }); +< +< equals(!!handlebarsEnv.decorators.foo, true); +< equals(!!handlebarsEnv.decorators.bar, true); +< handlebarsEnv.unregisterDecorator('foo'); +< handlebarsEnv.unregisterDecorator('bar'); +< equals(handlebarsEnv.decorators.foo, undefined); +< equals(handlebarsEnv.decorators.bar, undefined); +< }); +< +< it('fails with multiple and args', function() { +< shouldThrow( +< function() { +< handlebarsEnv.registerDecorator( +< { +< world: function() { +< return 'world!'; +< }, +< testHelper: function() { +< return 'found it!'; +< } +< }, +< {} +< ); +< }, +< Error, +< 'Arg not supported with multiple decorators' +< ); +< }); +< }); +< }); diff --git a/packages/kbn-handlebars/.patches/builtins.patch b/packages/kbn-handlebars/.patches/builtins.patch new file mode 100644 index 0000000000000..64e3294009a85 --- /dev/null +++ b/packages/kbn-handlebars/.patches/builtins.patch @@ -0,0 +1,872 @@ +1,4c1,16 +< describe('builtin helpers', function() { +< describe('#if', function() { +< it('if', function() { +< var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!'; +--- +> /* +> * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/kbn-handlebars/LICENSE` for more information. +> */ +> +> /* eslint-disable max-classes-per-file */ +> +> import Handlebars from '..'; +> import { expectTemplate } from '../__jest__/test_bench'; +> +> describe('builtin helpers', () => { +> describe('#if', () => { +> it('if', () => { +> const string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!'; +9c21 +< world: 'world' +--- +> world: 'world', +11d22 +< .withMessage('if with boolean argument shows the contents when true') +17c28 +< world: 'world' +--- +> world: 'world', +19d29 +< .withMessage('if with string argument shows the contents') +25c35 +< world: 'world' +--- +> world: 'world', +27,29d36 +< .withMessage( +< 'if with boolean argument does not show the contents when false' +< ) +32,35c39 +< expectTemplate(string) +< .withInput({ world: 'world' }) +< .withMessage('if with undefined does not show the contents') +< .toCompileTo('cruel world!'); +--- +> expectTemplate(string).withInput({ world: 'world' }).toCompileTo('cruel world!'); +40c44 +< world: 'world' +--- +> world: 'world', +42d45 +< .withMessage('if with non-empty array shows the contents') +48c51 +< world: 'world' +--- +> world: 'world', +50d52 +< .withMessage('if with empty array does not show the contents') +56c58 +< world: 'world' +--- +> world: 'world', +58d59 +< .withMessage('if with zero does not show the contents') +61,63c62 +< expectTemplate( +< '{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!' +< ) +--- +> expectTemplate('{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!') +66c65 +< world: 'world' +--- +> world: 'world', +68d66 +< .withMessage('if with zero does not show the contents') +72,73c70,71 +< it('if with function argument', function() { +< var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!'; +--- +> it('if with function argument', () => { +> const string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!'; +77c75 +< goodbye: function() { +--- +> goodbye() { +80c78 +< world: 'world' +--- +> world: 'world', +82,84d79 +< .withMessage( +< 'if with function shows the contents when function returns true' +< ) +89c84 +< goodbye: function() { +--- +> goodbye() { +92c87 +< world: 'world' +--- +> world: 'world', +94,96d88 +< .withMessage( +< 'if with function shows the contents when function returns string' +< ) +101c93 +< goodbye: function() { +--- +> goodbye() { +104c96 +< world: 'world' +--- +> world: 'world', +106,108d97 +< .withMessage( +< 'if with function does not show the contents when returns false' +< ) +113c102 +< goodbye: function() { +--- +> goodbye() { +116c105 +< world: 'world' +--- +> world: 'world', +118,120d106 +< .withMessage( +< 'if with function does not show the contents when returns undefined' +< ) +124,127c110,111 +< it('should not change the depth list', function() { +< expectTemplate( +< '{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}' +< ) +--- +> it('should not change the depth list', () => { +> expectTemplate('{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}') +130c114 +< world: 'world' +--- +> world: 'world', +136,137c120,121 +< describe('#with', function() { +< it('with', function() { +--- +> describe('#with', () => { +> it('with', () => { +142,143c126,127 +< last: 'Johnson' +< } +--- +> last: 'Johnson', +> }, +148c132 +< it('with with function argument', function() { +--- +> it('with with function argument', () => { +151c135 +< person: function() { +--- +> person() { +154c138 +< last: 'Johnson' +--- +> last: 'Johnson', +156c140 +< } +--- +> }, +161c145 +< it('with with else', function() { +--- +> it('with with else', () => { +167c151 +< it('with provides block parameter', function() { +--- +> it('with provides block parameter', () => { +172,173c156,157 +< last: 'Johnson' +< } +--- +> last: 'Johnson', +> }, +178c162 +< it('works when data is disabled', function() { +--- +> it('works when data is disabled', () => { +186,194c170,172 +< describe('#each', function() { +< beforeEach(function() { +< handlebarsEnv.registerHelper('detectDataInsideEach', function(options) { +< return options.data && options.data.exclaim; +< }); +< }); +< +< it('each', function() { +< var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; +--- +> describe('#each', () => { +> it('each', () => { +> const string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; +198,203c176,177 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ], +< world: 'world' +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +> world: 'world', +205,207d178 +< .withMessage( +< 'each with array argument iterates over the contents when not empty' +< ) +213c184 +< world: 'world' +--- +> world: 'world', +215d185 +< .withMessage('each with array argument ignores the contents when empty') +219c189 +< it('each without data', function() { +--- +> it('each without data', () => { +222,227c192,193 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ], +< world: 'world' +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +> world: 'world', +240c206 +< it('each without context', function() { +--- +> it('each without context', () => { +246,248c212,213 +< it('each with an object and @key', function() { +< var string = +< '{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!'; +--- +> it('each with an object and @key', () => { +> const string = '{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!'; +250c215 +< function Clazz() { +--- +> function Clazz(this: any) { +255c220 +< var hash = { goodbyes: new Clazz(), world: 'world' }; +--- +> const hash = { goodbyes: new (Clazz as any)(), world: 'world' }; +260,270c225,233 +< var actual = compileWithPartials(string, hash); +< var expected1 = +< '<b>#1</b>. goodbye! 2. GOODBYE! cruel world!'; +< var expected2 = +< '2. GOODBYE! <b>#1</b>. goodbye! cruel world!'; +< +< equals( +< actual === expected1 || actual === expected2, +< true, +< 'each with object argument iterates over the contents when not empty' +< ); +--- +> try { +> expectTemplate(string) +> .withInput(hash) +> .toCompileTo('<b>#1</b>. goodbye! 2. GOODBYE! cruel world!'); +> } catch (e) { +> expectTemplate(string) +> .withInput(hash) +> .toCompileTo('2. GOODBYE! <b>#1</b>. goodbye! cruel world!'); +> } +275c238 +< world: 'world' +--- +> world: 'world', +280,283c243,244 +< it('each with @index', function() { +< expectTemplate( +< '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!' +< ) +--- +> it('each with @index', () => { +> expectTemplate('{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!') +285,290c246,247 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ], +< world: 'world' +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +> world: 'world', +292d248 +< .withMessage('The @index variable is used') +296c252 +< it('each with nested @index', function() { +--- +> it('each with nested @index', () => { +301,306c257,258 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ], +< world: 'world' +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +> world: 'world', +308d259 +< .withMessage('The @index variable is used') +314c265 +< it('each with block params', function() { +--- +> it('each with block params', () => { +320c271 +< world: 'world' +--- +> world: 'world', +322,324c273 +< .toCompileTo( +< '0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!' +< ); +--- +> .toCompileTo('0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!'); +327,330c276,277 +< it('each object with @index', function() { +< expectTemplate( +< '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!' +< ) +--- +> it('each object with @index', () => { +> expectTemplate('{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!') +335c282 +< c: { text: 'GOODBYE' } +--- +> c: { text: 'GOODBYE' }, +337c284 +< world: 'world' +--- +> world: 'world', +339d285 +< .withMessage('The @index variable is used') +343,346c289,290 +< it('each with @first', function() { +< expectTemplate( +< '{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!' +< ) +--- +> it('each with @first', () => { +> expectTemplate('{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!') +348,353c292,293 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ], +< world: 'world' +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +> world: 'world', +355d294 +< .withMessage('The @first variable is used') +359c298 +< it('each with nested @first', function() { +--- +> it('each with nested @first', () => { +364,369c303,304 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ], +< world: 'world' +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +> world: 'world', +371,374c306 +< .withMessage('The @first variable is used') +< .toCompileTo( +< '(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!' +< ); +--- +> .toCompileTo('(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!'); +377,380c309,310 +< it('each object with @first', function() { +< expectTemplate( +< '{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!' +< ) +--- +> it('each object with @first', () => { +> expectTemplate('{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!') +383c313 +< world: 'world' +--- +> world: 'world', +385d314 +< .withMessage('The @first variable is used') +389,392c318,319 +< it('each with @last', function() { +< expectTemplate( +< '{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!' +< ) +--- +> it('each with @last', () => { +> expectTemplate('{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!') +394,399c321,322 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ], +< world: 'world' +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +> world: 'world', +401d323 +< .withMessage('The @last variable is used') +405,408c327,328 +< it('each object with @last', function() { +< expectTemplate( +< '{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!' +< ) +--- +> it('each object with @last', () => { +> expectTemplate('{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!') +411c331 +< world: 'world' +--- +> world: 'world', +413d332 +< .withMessage('The @last variable is used') +417c336 +< it('each with nested @last', function() { +--- +> it('each with nested @last', () => { +422,427c341,342 +< goodbyes: [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ], +< world: 'world' +--- +> goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], +> world: 'world', +429,432c344 +< .withMessage('The @last variable is used') +< .toCompileTo( +< '(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!' +< ); +--- +> .toCompileTo('(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!'); +435,436c347,348 +< it('each with function argument', function() { +< var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; +--- +> it('each with function argument', () => { +> const string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; +440,445c352,353 +< goodbyes: function() { +< return [ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ]; +--- +> goodbyes() { +> return [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }]; +447c355 +< world: 'world' +--- +> world: 'world', +449,451d356 +< .withMessage( +< 'each with array function argument iterates over the contents when not empty' +< ) +457c362 +< world: 'world' +--- +> world: 'world', +459,461d363 +< .withMessage( +< 'each with array function argument ignores the contents when empty' +< ) +465,468c367,368 +< it('each object when last key is an empty string', function() { +< expectTemplate( +< '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!' +< ) +--- +> it('each object when last key is an empty string', () => { +> expectTemplate('{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!') +473c373 +< '': { text: 'GOODBYE' } +--- +> '': { text: 'GOODBYE' }, +475c375 +< world: 'world' +--- +> world: 'world', +477d376 +< .withMessage('Empty string key is not skipped') +481,484c380,381 +< it('data passed to helpers', function() { +< expectTemplate( +< '{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}' +< ) +--- +> it('data passed to helpers', () => { +> expectTemplate('{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}') +486c383,385 +< .withMessage('should output data') +--- +> .withHelper('detectDataInsideEach', function (options) { +> return options.data && options.data.exclaim; +> }) +489,490c388,389 +< exclaim: '!' +< } +--- +> exclaim: '!', +> }, +495,499c394,395 +< it('each on implicit context', function() { +< expectTemplate('{{#each}}{{text}}! {{/each}}cruel world!').toThrow( +< handlebarsEnv.Exception, +< 'Must pass iterator to #each' +< ); +--- +> it('each on implicit context', () => { +> expectTemplate('{{#each}}{{text}}! {{/each}}cruel world!').toThrow(Handlebars.Exception); +502,504c398,403 +< if (global.Symbol && global.Symbol.iterator) { +< it('each on iterable', function() { +< function Iterator(arr) { +--- +> it('each on iterable', () => { +> class Iterator { +> private arr: any[]; +> private index: number = 0; +> +> constructor(arr: any[]) { +506d404 +< this.index = 0; +508,510c406,409 +< Iterator.prototype.next = function() { +< var value = this.arr[this.index]; +< var done = this.index === this.arr.length; +--- +> +> next() { +> const value = this.arr[this.index]; +> const done = this.index === this.arr.length; +514,516c413,420 +< return { value: value, done: done }; +< }; +< function Iterable(arr) { +--- +> return { value, done }; +> } +> } +> +> class Iterable { +> private arr: any[]; +> +> constructor(arr: any[]) { +519c423,424 +< Iterable.prototype[global.Symbol.iterator] = function() { +--- +> +> [Symbol.iterator]() { +521,522c426,427 +< }; +< var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; +--- +> } +> } +524,536c429 +< expectTemplate(string) +< .withInput({ +< goodbyes: new Iterable([ +< { text: 'goodbye' }, +< { text: 'Goodbye' }, +< { text: 'GOODBYE' } +< ]), +< world: 'world' +< }) +< .withMessage( +< 'each with array argument iterates over the contents when not empty' +< ) +< .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!'); +--- +> const string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; +538,548c431,444 +< expectTemplate(string) +< .withInput({ +< goodbyes: new Iterable([]), +< world: 'world' +< }) +< .withMessage( +< 'each with array argument ignores the contents when empty' +< ) +< .toCompileTo('cruel world!'); +< }); +< } +--- +> expectTemplate(string) +> .withInput({ +> goodbyes: new Iterable([{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }]), +> world: 'world', +> }) +> .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!'); +> +> expectTemplate(string) +> .withInput({ +> goodbyes: new Iterable([]), +> world: 'world', +> }) +> .toCompileTo('cruel world!'); +> }); +551c447 +< describe('#log', function() { +--- +> describe('#log', function () { +553,555c449,451 +< if (typeof console === 'undefined') { +< return; +< } +--- +> let $log: typeof console.log; +> let $info: typeof console.info; +> let $error: typeof console.error; +557,558c453 +< var $log, $info, $error; +< beforeEach(function() { +--- +> beforeEach(function () { +561a457,458 +> +> global.kbnHandlebarsEnv = Handlebars.create(); +563c460,461 +< afterEach(function() { +--- +> +> afterEach(function () { +569,571c467,470 +< it('should call logger at default level', function() { +< var levelArg, logArg; +< handlebarsEnv.log = function(level, arg) { +--- +> it('should call logger at default level', function () { +> let levelArg; +> let logArg; +> kbnHandlebarsEnv!.log = function (level, arg) { +576,581c475,477 +< expectTemplate('{{log blah}}') +< .withInput({ blah: 'whee' }) +< .withMessage('log should not display') +< .toCompileTo(''); +< equals(1, levelArg, 'should call log with 1'); +< equals('whee', logArg, "should call log with 'whee'"); +--- +> expectTemplate('{{log blah}}').withInput({ blah: 'whee' }).toCompileTo(''); +> expect(1).toEqual(levelArg); +> expect('whee').toEqual(logArg); +584,586c480,483 +< it('should call logger at data level', function() { +< var levelArg, logArg; +< handlebarsEnv.log = function(level, arg) { +--- +> it('should call logger at data level', function () { +> let levelArg; +> let logArg; +> kbnHandlebarsEnv!.log = function (level, arg) { +596,597c493,494 +< equals('03', levelArg); +< equals('whee', logArg); +--- +> expect('03').toEqual(levelArg); +> expect('whee').toEqual(logArg); +600,601c497,498 +< it('should output to info', function() { +< var called; +--- +> it('should output to info', function () { +> let called; +603,604c500,501 +< console.info = function(info) { +< equals('whee', info); +--- +> console.info = function (info) { +> expect('whee').toEqual(info); +609,610c506,507 +< console.log = function(log) { +< equals('whee', log); +--- +> console.log = function (log) { +> expect('whee').toEqual(log); +616,619c513,514 +< expectTemplate('{{log blah}}') +< .withInput({ blah: 'whee' }) +< .toCompileTo(''); +< equals(true, called); +--- +> expectTemplate('{{log blah}}').withInput({ blah: 'whee' }).toCompileTo(''); +> expect(true).toEqual(called); +622,623c517,518 +< it('should log at data level', function() { +< var called; +--- +> it('should log at data level', function () { +> let called; +625,626c520,521 +< console.error = function(log) { +< equals('whee', log); +--- +> console.error = function (log) { +> expect('whee').toEqual(log); +636c531 +< equals(true, called); +--- +> expect(true).toEqual(called); +639,640c534,535 +< it('should handle missing logger', function() { +< var called = false; +--- +> it('should handle missing logger', function () { +> let called = false; +641a537 +> // @ts-expect-error +643,644c539,540 +< console.log = function(log) { +< equals('whee', log); +--- +> console.log = function (log) { +> expect('whee').toEqual(log); +654c550 +< equals(true, called); +--- +> expect(true).toEqual(called); +657,658c553,554 +< it('should handle string log levels', function() { +< var called; +--- +> it('should handle string log levels', function () { +> let called; +660,661c556,557 +< console.error = function(log) { +< equals('whee', log); +--- +> console.error = function (log) { +> expect('whee').toEqual(log); +670c566 +< equals(true, called); +--- +> expect(true).toEqual(called); +679c575 +< equals(true, called); +--- +> expect(true).toEqual(called); +682,683c578,579 +< it('should handle hash log levels', function() { +< var called; +--- +> it('should handle hash log levels [1]', function () { +> let called; +685,686c581,582 +< console.error = function(log) { +< equals('whee', log); +--- +> console.error = function (log) { +> expect('whee').toEqual(log); +690,693c586,587 +< expectTemplate('{{log blah level="error"}}') +< .withInput({ blah: 'whee' }) +< .toCompileTo(''); +< equals(true, called); +--- +> expectTemplate('{{log blah level="error"}}').withInput({ blah: 'whee' }).toCompileTo(''); +> expect(true).toEqual(called); +696,697c590,591 +< it('should handle hash log levels', function() { +< var called = false; +--- +> it('should handle hash log levels [2]', function () { +> let called = false; +699,702c593,600 +< console.info = console.log = console.error = console.debug = function() { +< called = true; +< console.info = console.log = console.error = console.debug = $log; +< }; +--- +> console.info = +> console.log = +> console.error = +> console.debug = +> function () { +> called = true; +> console.info = console.log = console.error = console.debug = $log; +> }; +704,707c602,603 +< expectTemplate('{{log blah level="debug"}}') +< .withInput({ blah: 'whee' }) +< .toCompileTo(''); +< equals(false, called); +--- +> expectTemplate('{{log blah level="debug"}}').withInput({ blah: 'whee' }).toCompileTo(''); +> expect(false).toEqual(called); +710,711c606,607 +< it('should pass multiple log arguments', function() { +< var called; +--- +> it('should pass multiple log arguments', function () { +> let called; +713,716c609,612 +< console.info = console.log = function(log1, log2, log3) { +< equals('whee', log1); +< equals('foo', log2); +< equals(1, log3); +--- +> console.info = console.log = function (log1, log2, log3) { +> expect('whee').toEqual(log1); +> expect('foo').toEqual(log2); +> expect(1).toEqual(log3); +721,724c617,618 +< expectTemplate('{{log blah "foo" 1}}') +< .withInput({ blah: 'whee' }) +< .toCompileTo(''); +< equals(true, called); +--- +> expectTemplate('{{log blah "foo" 1}}').withInput({ blah: 'whee' }).toCompileTo(''); +> expect(true).toEqual(called); +727,728c621,622 +< it('should pass zero log arguments', function() { +< var called; +--- +> it('should pass zero log arguments', function () { +> let called; +730,731c624,625 +< console.info = console.log = function() { +< expect(arguments.length).to.equal(0); +--- +> console.info = console.log = function () { +> expect(arguments.length).toEqual(0); +736,739c630,631 +< expectTemplate('{{log}}') +< .withInput({ blah: 'whee' }) +< .toCompileTo(''); +< expect(called).to.be.true(); +--- +> expectTemplate('{{log}}').withInput({ blah: 'whee' }).toCompileTo(''); +> expect(called).toEqual(true); +744,745c636,637 +< describe('#lookup', function() { +< it('should lookup arbitrary content', function() { +--- +> describe('#lookup', () => { +> it('should lookup arbitrary content', () => { +751c643 +< it('should not fail on undefined value', function() { +--- +> it('should not fail on undefined value', () => { diff --git a/packages/kbn-handlebars/.patches/compiler.patch b/packages/kbn-handlebars/.patches/compiler.patch new file mode 100644 index 0000000000000..0028d85e395e8 --- /dev/null +++ b/packages/kbn-handlebars/.patches/compiler.patch @@ -0,0 +1,272 @@ +1,92c1,24 +< describe('compiler', function() { +< if (!Handlebars.compile) { +< return; +< } +< +< describe('#equals', function() { +< function compile(string) { +< var ast = Handlebars.parse(string); +< return new Handlebars.Compiler().compile(ast, {}); +< } +< +< it('should treat as equal', function() { +< equal(compile('foo').equals(compile('foo')), true); +< equal(compile('{{foo}}').equals(compile('{{foo}}')), true); +< equal(compile('{{foo.bar}}').equals(compile('{{foo.bar}}')), true); +< equal( +< compile('{{foo.bar baz "foo" true false bat=1}}').equals( +< compile('{{foo.bar baz "foo" true false bat=1}}') +< ), +< true +< ); +< equal( +< compile('{{foo.bar (baz bat=1)}}').equals( +< compile('{{foo.bar (baz bat=1)}}') +< ), +< true +< ); +< equal( +< compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}')), +< true +< ); +< }); +< it('should treat as not equal', function() { +< equal(compile('foo').equals(compile('bar')), false); +< equal(compile('{{foo}}').equals(compile('{{bar}}')), false); +< equal(compile('{{foo.bar}}').equals(compile('{{bar.bar}}')), false); +< equal( +< compile('{{foo.bar baz bat=1}}').equals( +< compile('{{foo.bar bar bat=1}}') +< ), +< false +< ); +< equal( +< compile('{{foo.bar (baz bat=1)}}').equals( +< compile('{{foo.bar (bar bat=1)}}') +< ), +< false +< ); +< equal( +< compile('{{#foo}} {{/foo}}').equals(compile('{{#bar}} {{/bar}}')), +< false +< ); +< equal( +< compile('{{#foo}} {{/foo}}').equals( +< compile('{{#foo}} {{foo}}{{/foo}}') +< ), +< false +< ); +< }); +< }); +< +< describe('#compile', function() { +< it('should fail with invalid input', function() { +< shouldThrow( +< function() { +< Handlebars.compile(null); +< }, +< Error, +< 'You must pass a string or Handlebars AST to Handlebars.compile. You passed null' +< ); +< shouldThrow( +< function() { +< Handlebars.compile({}); +< }, +< Error, +< 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]' +< ); +< }); +< +< it('should include the location in the error (row and column)', function() { +< try { +< Handlebars.compile(' \n {{#if}}\n{{/def}}')(); +< equal( +< true, +< false, +< 'Statement must throw exception. This line should not be executed.' +< ); +< } catch (err) { +< equal( +< err.message, +< "if doesn't match def - 2:5", +< 'Checking error message' +--- +> /* +> * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/kbn-handlebars/LICENSE` for more information. +> */ +> +> import Handlebars from '..'; +> +> describe('compiler', () => { +> const compileFns = ['compile', 'compileAST']; +> if (process.env.AST) compileFns.splice(0, 1); +> else if (process.env.EVAL) compileFns.splice(1, 1); +> +> compileFns.forEach((compileName) => { +> // @ts-expect-error +> const compile = Handlebars[compileName]; +> +> describe(`#${compileName}`, () => { +> it('should fail with invalid input', () => { +> expect(function () { +> compile(null); +> }).toThrow( +> `You must pass a string or Handlebars AST to Handlebars.${compileName}. You passed null` +94,102d25 +< if (Object.getOwnPropertyDescriptor(err, 'column').writable) { +< // In Safari 8, the column-property is read-only. This means that even if it is set with defineProperty, +< // its value won't change (https://github.com/jquery/esprima/issues/1290#issuecomment-132455482) +< // Since this was neither working in Handlebars 3 nor in 4.0.5, we only check the column for other browsers. +< equal(err.column, 5, 'Checking error column'); +< } +< equal(err.lineNumber, 2, 'Checking error row'); +< } +< }); +104,116c27,30 +< it('should include the location as enumerable property', function() { +< try { +< Handlebars.compile(' \n {{#if}}\n{{/def}}')(); +< equal( +< true, +< false, +< 'Statement must throw exception. This line should not be executed.' +< ); +< } catch (err) { +< equal( +< Object.prototype.propertyIsEnumerable.call(err, 'column'), +< true, +< 'Checking error column' +--- +> expect(function () { +> compile({}); +> }).toThrow( +> `You must pass a string or Handlebars AST to Handlebars.${compileName}. You passed [object Object]` +118,129c32 +< } +< }); +< +< it('can utilize AST instance', function() { +< equal( +< Handlebars.compile({ +< type: 'Program', +< body: [{ type: 'ContentStatement', value: 'Hello' }] +< })(), +< 'Hello' +< ); +< }); +--- +> }); +131,152c34,48 +< it('can pass through an empty string', function() { +< equal(Handlebars.compile('')(), ''); +< }); +< +< it('should not modify the options.data property(GH-1327)', function() { +< var options = { data: [{ a: 'foo' }, { a: 'bar' }] }; +< Handlebars.compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)(); +< equal( +< JSON.stringify(options, 0, 2), +< JSON.stringify({ data: [{ a: 'foo' }, { a: 'bar' }] }, 0, 2) +< ); +< }); +< +< it('should not modify the options.knownHelpers property(GH-1327)', function() { +< var options = { knownHelpers: {} }; +< Handlebars.compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)(); +< equal( +< JSON.stringify(options, 0, 2), +< JSON.stringify({ knownHelpers: {} }, 0, 2) +< ); +< }); +< }); +--- +> it('should include the location in the error (row and column)', () => { +> try { +> compile(' \n {{#if}}\n{{/def}}')({}); +> expect(true).toEqual(false); +> } catch (err) { +> expect(err.message).toEqual("if doesn't match def - 2:5"); +> if (Object.getOwnPropertyDescriptor(err, 'column')!.writable) { +> // In Safari 8, the column-property is read-only. This means that even if it is set with defineProperty, +> // its value won't change (https://github.com/jquery/esprima/issues/1290#issuecomment-132455482) +> // Since this was neither working in Handlebars 3 nor in 4.0.5, we only check the column for other browsers. +> expect(err.column).toEqual(5); +> } +> expect(err.lineNumber).toEqual(2); +> } +> }); +154,170c50,57 +< describe('#precompile', function() { +< it('should fail with invalid input', function() { +< shouldThrow( +< function() { +< Handlebars.precompile(null); +< }, +< Error, +< 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null' +< ); +< shouldThrow( +< function() { +< Handlebars.precompile({}); +< }, +< Error, +< 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed [object Object]' +< ); +< }); +--- +> it('should include the location as enumerable property', () => { +> try { +> compile(' \n {{#if}}\n{{/def}}')({}); +> expect(true).toEqual(false); +> } catch (err) { +> expect(Object.prototype.propertyIsEnumerable.call(err, 'column')).toEqual(true); +> } +> }); +172,175c59,61 +< it('can utilize AST instance', function() { +< equal( +< /return "Hello"/.test( +< Handlebars.precompile({ +--- +> it('can utilize AST instance', () => { +> expect( +> compile({ +177,182c63,78 +< body: [{ type: 'ContentStatement', value: 'Hello' }] +< }) +< ), +< true +< ); +< }); +--- +> body: [{ type: 'ContentStatement', value: 'Hello' }], +> })({}) +> ).toEqual('Hello'); +> }); +> +> it('can pass through an empty string', () => { +> expect(compile('')({})).toEqual(''); +> }); +> +> it('should not modify the options.data property(GH-1327)', () => { +> const options = { data: [{ a: 'foo' }, { a: 'bar' }] }; +> compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)({}); +> expect(JSON.stringify(options, null, 2)).toEqual( +> JSON.stringify({ data: [{ a: 'foo' }, { a: 'bar' }] }, null, 2) +> ); +> }); +184,185c80,86 +< it('can pass through an empty string', function() { +< equal(/return ""/.test(Handlebars.precompile('')), true); +--- +> it('should not modify the options.knownHelpers property(GH-1327)', () => { +> const options = { knownHelpers: {} }; +> compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)({}); +> expect(JSON.stringify(options, null, 2)).toEqual( +> JSON.stringify({ knownHelpers: {} }, null, 2) +> ); +> }); diff --git a/packages/kbn-handlebars/.patches/data.patch b/packages/kbn-handlebars/.patches/data.patch new file mode 100644 index 0000000000000..2da3fdb2e2591 --- /dev/null +++ b/packages/kbn-handlebars/.patches/data.patch @@ -0,0 +1,273 @@ +1,2c1,12 +< describe('data', function() { +< it('passing in data to a compiled function that expects data - works with helpers', function() { +--- +> /* +> * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/kbn-handlebars/LICENSE` for more information. +> */ +> +> import Handlebars from '..'; +> import { expectTemplate } from '../__jest__/test_bench'; +> +> describe('data', () => { +> it('passing in data to a compiled function that expects data - works with helpers', () => { +5c15 +< .withHelper('hello', function(options) { +--- +> .withHelper('hello', function (this: any, options) { +10d19 +< .withMessage('Data output by helper') +14c23 +< it('data can be looked up via @foo', function() { +--- +> it('data can be looked up via @foo', () => { +17d25 +< .withMessage('@foo retrieves template data') +21,22c29,31 +< it('deep @foo triggers automatic top-level data', function() { +< var helpers = Handlebars.createFrame(handlebarsEnv.helpers); +--- +> it('deep @foo triggers automatic top-level data', () => { +> global.kbnHandlebarsEnv = Handlebars.create(); +> const helpers = Handlebars.createFrame(kbnHandlebarsEnv!.helpers); +24,25c33,34 +< helpers.let = function(options) { +< var frame = Handlebars.createFrame(options.data); +--- +> helpers.let = function (options: Handlebars.HelperOptions) { +> const frame = Handlebars.createFrame(options.data); +27c36 +< for (var prop in options.hash) { +--- +> for (const prop in options.hash) { +40d48 +< .withMessage('Automatic data was triggered') +44c52 +< it('parameter data can be looked up via @foo', function() { +--- +> it('parameter data can be looked up via @foo', () => { +47c55 +< .withHelper('hello', function(noun) { +--- +> .withHelper('hello', function (noun) { +50d57 +< .withMessage('@foo as a parameter retrieves template data') +54c61 +< it('hash values can be looked up via @foo', function() { +--- +> it('hash values can be looked up via @foo', () => { +57c64 +< .withHelper('hello', function(options) { +--- +> .withHelper('hello', function (options) { +60d66 +< .withMessage('@foo as a parameter retrieves template data') +64c70 +< it('nested parameter data can be looked up via @foo.bar', function() { +--- +> it('nested parameter data can be looked up via @foo.bar', () => { +67c73 +< .withHelper('hello', function(noun) { +--- +> .withHelper('hello', function (noun) { +70d75 +< .withMessage('@foo as a parameter retrieves template data') +74c79 +< it('nested parameter data does not fail with @world.bar', function() { +--- +> it('nested parameter data does not fail with @world.bar', () => { +77c82 +< .withHelper('hello', function(noun) { +--- +> .withHelper('hello', function (noun) { +80d84 +< .withMessage('@foo as a parameter retrieves template data') +84,87c88,89 +< it('parameter data throws when using complex scope references', function() { +< expectTemplate( +< '{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}' +< ).toThrow(Error); +--- +> it('parameter data throws when using complex scope references', () => { +> expectTemplate('{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}').toThrow(Error); +90c92 +< it('data can be functions', function() { +--- +> it('data can be functions', () => { +94c96 +< hello: function() { +--- +> hello() { +96,97c98,99 +< } +< } +--- +> }, +> }, +102c104 +< it('data can be functions with params', function() { +--- +> it('data can be functions with params', () => { +106c108 +< hello: function(arg) { +--- +> hello(arg: any) { +108,109c110,111 +< } +< } +--- +> }, +> }, +114c116 +< it('data is inherited downstream', function() { +--- +> it('data is inherited downstream', () => { +120,122c122,124 +< .withHelper('let', function(options) { +< var frame = Handlebars.createFrame(options.data); +< for (var prop in options.hash) { +--- +> .withHelper('let', function (this: any, options) { +> const frame = Handlebars.createFrame(options.data); +> for (const prop in options.hash) { +130d131 +< .withMessage('data variables are inherited downstream') +134,147c135 +< it('passing in data to a compiled function that expects data - works with helpers in partials', function() { +< expectTemplate('{{>myPartial}}') +< .withCompileOptions({ data: true }) +< .withPartial('myPartial', '{{hello}}') +< .withHelper('hello', function(options) { +< return options.data.adjective + ' ' + this.noun; +< }) +< .withInput({ noun: 'cat' }) +< .withRuntimeOptions({ data: { adjective: 'happy' } }) +< .withMessage('Data output by helper inside partial') +< .toCompileTo('happy cat'); +< }); +< +< it('passing in data to a compiled function that expects data - works with helpers and parameters', function() { +--- +> it('passing in data to a compiled function that expects data - works with helpers and parameters', () => { +150c138 +< .withHelper('hello', function(noun, options) { +--- +> .withHelper('hello', function (this: any, noun, options) { +155d142 +< .withMessage('Data output by helper') +159c146 +< it('passing in data to a compiled function that expects data - works with block helpers', function() { +--- +> it('passing in data to a compiled function that expects data - works with block helpers', () => { +162c149 +< data: true +--- +> data: true, +164c151 +< .withHelper('hello', function(options) { +--- +> .withHelper('hello', function (this: any, options) { +167c154 +< .withHelper('world', function(options) { +--- +> .withHelper('world', function (this: any, options) { +172d158 +< .withMessage('Data output by helper') +176c162 +< it('passing in data to a compiled function that expects data - works with block helpers that use ..', function() { +--- +> it('passing in data to a compiled function that expects data - works with block helpers that use ..', () => { +179c165 +< .withHelper('hello', function(options) { +--- +> .withHelper('hello', function (options) { +182c168 +< .withHelper('world', function(thing, options) { +--- +> .withHelper('world', function (this: any, thing, options) { +187d172 +< .withMessage('Data output by helper') +191c176 +< it('passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..', function() { +--- +> it('passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..', () => { +194c179 +< .withHelper('hello', function(options) { +--- +> .withHelper('hello', function (options) { +197c182 +< .withHelper('world', function(thing, options) { +--- +> .withHelper('world', function (this: any, thing, options) { +202d186 +< .withMessage('Data output by helper') +206c190 +< it('you can override inherited data when invoking a helper', function() { +--- +> it('you can override inherited data when invoking a helper', () => { +209,213c193,194 +< .withHelper('hello', function(options) { +< return options.fn( +< { exclaim: '?', zomg: 'world' }, +< { data: { adjective: 'sad' } } +< ); +--- +> .withHelper('hello', function (options) { +> return options.fn({ exclaim: '?', zomg: 'world' }, { data: { adjective: 'sad' } }); +215c196 +< .withHelper('world', function(thing, options) { +--- +> .withHelper('world', function (this: any, thing, options) { +220d200 +< .withMessage('Overriden data output by helper') +224c204 +< it('you can override inherited data when invoking a helper with depth', function() { +--- +> it('you can override inherited data when invoking a helper with depth', () => { +227c207 +< .withHelper('hello', function(options) { +--- +> .withHelper('hello', function (options) { +230c210 +< .withHelper('world', function(thing, options) { +--- +> .withHelper('world', function (this: any, thing, options) { +235d214 +< .withMessage('Overriden data output by helper') +239,240c218,219 +< describe('@root', function() { +< it('the root context can be looked up via @root', function() { +--- +> describe('@root', () => { +> it('the root context can be looked up via @root', () => { +246,248c225 +< expectTemplate('{{@root.foo}}') +< .withInput({ foo: 'hello' }) +< .toCompileTo('hello'); +--- +> expectTemplate('{{@root.foo}}').withInput({ foo: 'hello' }).toCompileTo('hello'); +251c228 +< it('passed root values take priority', function() { +--- +> it('passed root values take priority', () => { +259,260c236,237 +< describe('nesting', function() { +< it('the root context can be looked up via @root', function() { +--- +> describe('nesting', () => { +> it('the root context can be looked up via @root', () => { +265,266c242,243 +< .withHelper('helper', function(options) { +< var frame = Handlebars.createFrame(options.data); +--- +> .withHelper('helper', function (this: any, options) { +> const frame = Handlebars.createFrame(options.data); +272,273c249,250 +< depth: 0 +< } +--- +> depth: 0, +> }, diff --git a/packages/kbn-handlebars/.patches/helpers.patch b/packages/kbn-handlebars/.patches/helpers.patch new file mode 100644 index 0000000000000..f744c9cde9f52 --- /dev/null +++ b/packages/kbn-handlebars/.patches/helpers.patch @@ -0,0 +1,1096 @@ +1,2c1,16 +< describe('helpers', function() { +< it('helper with complex lookup$', function() { +--- +> /* +> * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/kbn-handlebars/LICENSE` for more information. +> */ +> +> import Handlebars from '..'; +> import { expectTemplate } from '../__jest__/test_bench'; +> +> beforeEach(() => { +> global.kbnHandlebarsEnv = Handlebars.create(); +> }); +> +> describe('helpers', () => { +> it('helper with complex lookup$', () => { +6c20 +< goodbyes: [{ text: 'Goodbye', url: 'goodbye' }] +--- +> goodbyes: [{ text: 'Goodbye', url: 'goodbye' }], +8,11c22,23 +< .withHelper('link', function(prefix) { +< return ( +< '' + this.text + '' +< ); +--- +> .withHelper('link', function (this: any, prefix) { +> return '' + this.text + ''; +16c28 +< it('helper for raw block gets raw content', function() { +--- +> it('helper for raw block gets raw content', () => { +19c31 +< .withHelper('raw', function(options) { +--- +> .withHelper('raw', function (options) { +22d33 +< .withMessage('raw block helper gets raw content') +26c37 +< it('helper for raw block gets parameters', function() { +--- +> it('helper for raw block gets parameters', () => { +29,30c40,42 +< .withHelper('raw', function(a, b, c, options) { +< return options.fn() + a + b + c; +--- +> .withHelper('raw', function (a, b, c, options) { +> const ret = options.fn() + a + b + c; +> return ret; +32d43 +< .withMessage('raw block helper gets raw content') +36,37c47,48 +< describe('raw block parsing (with identity helper-function)', function() { +< function runWithIdentityHelper(template, expected) { +--- +> describe('raw block parsing (with identity helper-function)', () => { +> function runWithIdentityHelper(template: string, expected: string) { +39c50 +< .withHelper('identity', function(options) { +--- +> .withHelper('identity', function (options) { +45c56 +< it('helper for nested raw block gets raw content', function() { +--- +> it('helper for nested raw block gets raw content', () => { +52c63 +< it('helper for nested raw block works with empty content', function() { +--- +> it('helper for nested raw block works with empty content', () => { +56c67 +< xit('helper for nested raw block works if nested raw blocks are broken', function() { +--- +> it.skip('helper for nested raw block works if nested raw blocks are broken', () => { +67c78 +< it('helper for nested raw block closes after first matching close', function() { +--- +> it('helper for nested raw block closes after first matching close', () => { +74,75c85,86 +< it('helper for nested raw block throw exception when with missing closing braces', function() { +< var string = '{{{{a}}}} {{{{/a'; +--- +> it('helper for nested raw block throw exception when with missing closing braces', () => { +> const string = '{{{{a}}}} {{{{/a'; +80c91 +< it('helper block with identical context', function() { +--- +> it('helper block with identical context', () => { +83,86c94,97 +< .withHelper('goodbyes', function(options) { +< var out = ''; +< var byes = ['Goodbye', 'goodbye', 'GOODBYE']; +< for (var i = 0, j = byes.length; i < j; i++) { +--- +> .withHelper('goodbyes', function (this: any, options) { +> let out = ''; +> const byes = ['Goodbye', 'goodbye', 'GOODBYE']; +> for (let i = 0, j = byes.length; i < j; i++) { +94c105 +< it('helper block with complex lookup expression', function() { +--- +> it('helper block with complex lookup expression', () => { +97,100c108,111 +< .withHelper('goodbyes', function(options) { +< var out = ''; +< var byes = ['Goodbye', 'goodbye', 'GOODBYE']; +< for (var i = 0, j = byes.length; i < j; i++) { +--- +> .withHelper('goodbyes', function (options) { +> let out = ''; +> const byes = ['Goodbye', 'goodbye', 'GOODBYE']; +> for (let i = 0, j = byes.length; i < j; i++) { +108,111c119,120 +< it('helper with complex lookup and nested template', function() { +< expectTemplate( +< '{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}' +< ) +--- +> it('helper with complex lookup and nested template', () => { +> expectTemplate('{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}') +114c123 +< goodbyes: [{ text: 'Goodbye', url: 'goodbye' }] +--- +> goodbyes: [{ text: 'Goodbye', url: 'goodbye' }], +116,125c125,126 +< .withHelper('link', function(prefix, options) { +< return ( +< '' + +< options.fn(this) + +< '' +< ); +--- +> .withHelper('link', function (this: any, prefix, options) { +> return '' + options.fn(this) + ''; +130,133c131,132 +< it('helper with complex lookup and nested template in VM+Compiler', function() { +< expectTemplate( +< '{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}' +< ) +--- +> it('helper with complex lookup and nested template in VM+Compiler', () => { +> expectTemplate('{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}') +136c135 +< goodbyes: [{ text: 'Goodbye', url: 'goodbye' }] +--- +> goodbyes: [{ text: 'Goodbye', url: 'goodbye' }], +138,147c137,138 +< .withHelper('link', function(prefix, options) { +< return ( +< '' + +< options.fn(this) + +< '' +< ); +--- +> .withHelper('link', function (this: any, prefix, options) { +> return '' + options.fn(this) + ''; +152c143 +< it('helper returning undefined value', function() { +--- +> it('helper returning undefined value', () => { +155c146 +< nothere: function() {} +--- +> nothere() {}, +161c152 +< nothere: function() {} +--- +> nothere() {}, +166c157 +< it('block helper', function() { +--- +> it('block helper', () => { +169c160 +< .withHelper('goodbyes', function(options) { +--- +> .withHelper('goodbyes', function (options) { +172d162 +< .withMessage('Block helper executed') +176c166 +< it('block helper staying in the same context', function() { +--- +> it('block helper staying in the same context', () => { +179c169 +< .withHelper('form', function(options) { +--- +> .withHelper('form', function (this: any, options) { +182d171 +< .withMessage('Block helper executed with current context') +186,187c175,176 +< it('block helper should have context in this', function() { +< function link(options) { +--- +> it('block helper should have context in this', () => { +> function link(this: any, options: Handlebars.HelperOptions) { +191,193c180 +< expectTemplate( +< '
    {{#people}}
  • {{#link}}{{name}}{{/link}}
  • {{/people}}
' +< ) +--- +> expectTemplate('
    {{#people}}
  • {{#link}}{{name}}{{/link}}
  • {{/people}}
') +197,198c184,185 +< { name: 'Yehuda', id: 2 } +< ] +--- +> { name: 'Yehuda', id: 2 }, +> ], +206c193 +< it('block helper for undefined value', function() { +--- +> it('block helper for undefined value', () => { +210c197 +< it('block helper passing a new context', function() { +--- +> it('block helper passing a new context', () => { +213c200 +< .withHelper('form', function(context, options) { +--- +> .withHelper('form', function (context, options) { +216d202 +< .withMessage('Context variable resolved') +220c206 +< it('block helper passing a complex path context', function() { +--- +> it('block helper passing a complex path context', () => { +223c209 +< .withHelper('form', function(context, options) { +--- +> .withHelper('form', function (context, options) { +226d211 +< .withMessage('Complex path variable resolved') +230,233c215,216 +< it('nested block helpers', function() { +< expectTemplate( +< '{{#form yehuda}}

{{name}}

{{#link}}Hello{{/link}}{{/form}}' +< ) +--- +> it('nested block helpers', () => { +> expectTemplate('{{#form yehuda}}

{{name}}

{{#link}}Hello{{/link}}{{/form}}') +235c218 +< yehuda: { name: 'Yehuda' } +--- +> yehuda: { name: 'Yehuda' }, +237c220 +< .withHelper('link', function(options) { +--- +> .withHelper('link', function (this: any, options) { +240c223 +< .withHelper('form', function(context, options) { +--- +> .withHelper('form', function (context, options) { +243d225 +< .withMessage('Both blocks executed') +247,249c229,231 +< it('block helper inverted sections', function() { +< var string = "{{#list people}}{{name}}{{^}}Nobody's here{{/list}}"; +< function list(context, options) { +--- +> it('block helper inverted sections', () => { +> const string = "{{#list people}}{{name}}{{^}}Nobody's here{{/list}}"; +> function list(this: any, context: any, options: Handlebars.HelperOptions) { +251,252c233,234 +< var out = '
    '; +< for (var i = 0, j = context.length; i < j; i++) { +--- +> let out = '
      '; +> for (let i = 0, j = context.length; i < j; i++) { +268,269c250 +< .withHelpers({ list: list }) +< .withMessage('an inverse wrapper is passed in as a new context') +--- +> .withHelpers({ list }) +274,275c255 +< .withHelpers({ list: list }) +< .withMessage('an inverse wrapper can be optionally called') +--- +> .withHelpers({ list }) +281c261 +< message: "Nobody's here" +--- +> message: "Nobody's here", +283,284c263 +< .withHelpers({ list: list }) +< .withMessage('the context of an inverse is the parent of the block') +--- +> .withHelpers({ list }) +288,292c267,269 +< it('pathed lambas with parameters', function() { +< var hash = { +< helper: function() { +< return 'winning'; +< } +--- +> it('pathed lambas with parameters', () => { +> const hash = { +> helper: () => 'winning', +293a271 +> // @ts-expect-error +295,298c273,275 +< var helpers = { +< './helper': function() { +< return 'fail'; +< } +--- +> +> const helpers = { +> './helper': () => 'fail', +301,309c278,279 +< expectTemplate('{{./helper 1}}') +< .withInput(hash) +< .withHelpers(helpers) +< .toCompileTo('winning'); +< +< expectTemplate('{{hash/helper 1}}') +< .withInput(hash) +< .withHelpers(helpers) +< .toCompileTo('winning'); +--- +> expectTemplate('{{./helper 1}}').withInput(hash).withHelpers(helpers).toCompileTo('winning'); +> expectTemplate('{{hash/helper 1}}').withInput(hash).withHelpers(helpers).toCompileTo('winning'); +312,313c282,283 +< describe('helpers hash', function() { +< it('providing a helpers hash', function() { +--- +> describe('helpers hash', () => { +> it('providing a helpers hash', () => { +317c287 +< world: function() { +--- +> world() { +319c289 +< } +--- +> }, +321d290 +< .withMessage('helpers hash is available') +327c296 +< world: function() { +--- +> world() { +329c298 +< } +--- +> }, +331d299 +< .withMessage('helpers hash is available inside other blocks') +335c303 +< it('in cases of conflict, helpers win', function() { +--- +> it('in cases of conflict, helpers win', () => { +339c307 +< lookup: function() { +--- +> lookup() { +341c309 +< } +--- +> }, +343d310 +< .withMessage('helpers hash has precedence escaped expansion') +349c316 +< lookup: function() { +--- +> lookup() { +351c318 +< } +--- +> }, +353d319 +< .withMessage('helpers hash has precedence simple expansion') +357c323 +< it('the helpers hash is available is nested contexts', function() { +--- +> it('the helpers hash is available is nested contexts', () => { +361c327 +< helper: function() { +--- +> helper() { +363c329 +< } +--- +> }, +365d330 +< .withMessage('helpers hash is available in nested contexts.') +369,370c334,335 +< it('the helper hash should augment the global hash', function() { +< handlebarsEnv.registerHelper('test_helper', function() { +--- +> it('the helper hash should augment the global hash', () => { +> kbnHandlebarsEnv!.registerHelper('test_helper', function () { +374,376c339 +< expectTemplate( +< '{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}' +< ) +--- +> expectTemplate('{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}') +379c342 +< world: function() { +--- +> world() { +381c344 +< } +--- +> }, +387,389c350,352 +< describe('registration', function() { +< it('unregisters', function() { +< handlebarsEnv.helpers = {}; +--- +> describe('registration', () => { +> it('unregisters', () => { +> deleteAllKeys(kbnHandlebarsEnv!.helpers); +391c354 +< handlebarsEnv.registerHelper('foo', function() { +--- +> kbnHandlebarsEnv!.registerHelper('foo', function () { +394,395c357,359 +< handlebarsEnv.unregisterHelper('foo'); +< equals(handlebarsEnv.helpers.foo, undefined); +--- +> expect(kbnHandlebarsEnv!.helpers.foo).toBeDefined(); +> kbnHandlebarsEnv!.unregisterHelper('foo'); +> expect(kbnHandlebarsEnv!.helpers.foo).toBeUndefined(); +398,404c362,368 +< it('allows multiple globals', function() { +< var helpers = handlebarsEnv.helpers; +< handlebarsEnv.helpers = {}; +< +< handlebarsEnv.registerHelper({ +< if: helpers['if'], +< world: function() { +--- +> it('allows multiple globals', () => { +> const ifHelper = kbnHandlebarsEnv!.helpers.if; +> deleteAllKeys(kbnHandlebarsEnv!.helpers); +> +> kbnHandlebarsEnv!.registerHelper({ +> if: ifHelper, +> world() { +407c371 +< testHelper: function() { +--- +> testHelper() { +409c373 +< } +--- +> }, +412,414c376 +< expectTemplate( +< '{{testHelper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}' +< ) +--- +> expectTemplate('{{testHelper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}') +419,429c381,387 +< it('fails with multiple and args', function() { +< shouldThrow( +< function() { +< handlebarsEnv.registerHelper( +< { +< world: function() { +< return 'world!'; +< }, +< testHelper: function() { +< return 'found it!'; +< } +--- +> it('fails with multiple and args', () => { +> expect(() => { +> kbnHandlebarsEnv!.registerHelper( +> // @ts-expect-error TypeScript is complaining about the invalid input just as the thrown error +> { +> world() { +> return 'world!'; +431,436c389,395 +< {} +< ); +< }, +< Error, +< 'Arg not supported with multiple helpers' +< ); +--- +> testHelper() { +> return 'found it!'; +> }, +> }, +> {} +> ); +> }).toThrow('Arg not supported with multiple helpers'); +440c399 +< it('decimal number literals work', function() { +--- +> it('decimal number literals work', () => { +442c401 +< .withHelper('hello', function(times, times2) { +--- +> .withHelper('hello', function (times, times2) { +451d409 +< .withMessage('template with a negative integer literal') +455c413 +< it('negative number literals work', function() { +--- +> it('negative number literals work', () => { +457c415 +< .withHelper('hello', function(times) { +--- +> .withHelper('hello', function (times) { +463d420 +< .withMessage('template with a negative integer literal') +467,468c424,425 +< describe('String literal parameters', function() { +< it('simple literals work', function() { +--- +> describe('String literal parameters', () => { +> it('simple literals work', () => { +470c427 +< .withHelper('hello', function(param, times, bool1, bool2) { +--- +> .withHelper('hello', function (param, times, bool1, bool2) { +480,482c437 +< return ( +< 'Hello ' + param + ' ' + times + ' times: ' + bool1 + ' ' + bool2 +< ); +--- +> return 'Hello ' + param + ' ' + times + ' times: ' + bool1 + ' ' + bool2; +484d438 +< .withMessage('template with a simple String literal') +488c442 +< it('using a quote in the middle of a parameter raises an error', function() { +--- +> it('using a quote in the middle of a parameter raises an error', () => { +492c446 +< it('escaping a String is possible', function() { +--- +> it('escaping a String is possible', () => { +494c448 +< .withHelper('hello', function(param) { +--- +> .withHelper('hello', function (param) { +497d450 +< .withMessage('template with an escaped String literal') +501c454 +< it("it works with ' marks", function() { +--- +> it("it works with ' marks", () => { +503c456 +< .withHelper('hello', function(param) { +--- +> .withHelper('hello', function (param) { +506d458 +< .withMessage("template with a ' mark") +511,524c463,464 +< it('negative number literals work', function() { +< expectTemplate('Message: {{hello -12}}') +< .withHelper('hello', function(times) { +< if (typeof times !== 'number') { +< times = 'NaN'; +< } +< return 'Hello ' + times + ' times'; +< }) +< .withMessage('template with a negative integer literal') +< .toCompileTo('Message: Hello -12 times'); +< }); +< +< describe('multiple parameters', function() { +< it('simple multi-params work', function() { +--- +> describe('multiple parameters', () => { +> it('simple multi-params work', () => { +527c467 +< .withHelper('goodbye', function(cruel, world) { +--- +> .withHelper('goodbye', function (cruel, world) { +530d469 +< .withMessage('regular helpers with multiple params') +534,537c473,474 +< it('block multi-params work', function() { +< expectTemplate( +< 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}' +< ) +--- +> it('block multi-params work', () => { +> expectTemplate('Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}') +539c476 +< .withHelper('goodbye', function(cruel, world, options) { +--- +> .withHelper('goodbye', function (cruel, world, options) { +542d478 +< .withMessage('block helpers with multiple params') +547,548c483,484 +< describe('hash', function() { +< it('helpers can take an optional hash', function() { +--- +> describe('hash', () => { +> it('helpers can take an optional hash', () => { +550c486 +< .withHelper('goodbye', function(options) { +--- +> .withHelper('goodbye', function (options) { +561d496 +< .withMessage('Helper output hash') +565,566c500,501 +< it('helpers can take an optional hash with booleans', function() { +< function goodbye(options) { +--- +> it('helpers can take an optional hash with booleans', () => { +> function goodbye(options: Handlebars.HelperOptions) { +578d512 +< .withMessage('Helper output hash') +583d516 +< .withMessage('Boolean helper parameter honored') +587c520 +< it('block helpers can take an optional hash', function() { +--- +> it('block helpers can take an optional hash', () => { +589c522 +< .withHelper('goodbye', function(options) { +--- +> .withHelper('goodbye', function (this: any, options) { +600d532 +< .withMessage('Hash parameters output') +604c536 +< it('block helpers can take an optional hash with single quoted stings', function() { +--- +> it('block helpers can take an optional hash with single quoted stings', () => { +606c538 +< .withHelper('goodbye', function(options) { +--- +> .withHelper('goodbye', function (this: any, options) { +617d548 +< .withMessage('Hash parameters output') +621,622c552,553 +< it('block helpers can take an optional hash with booleans', function() { +< function goodbye(options) { +--- +> it('block helpers can take an optional hash with booleans', () => { +> function goodbye(this: any, options: Handlebars.HelperOptions) { +634d564 +< .withMessage('Boolean hash parameter honored') +639d568 +< .withMessage('Boolean hash parameter honored') +644,648c573,575 +< describe('helperMissing', function() { +< it('if a context is not found, helperMissing is used', function() { +< expectTemplate('{{hello}} {{link_to world}}').toThrow( +< /Missing helper: "link_to"/ +< ); +--- +> describe('helperMissing', () => { +> it('if a context is not found, helperMissing is used', () => { +> expectTemplate('{{hello}} {{link_to world}}').toThrow(/Missing helper: "link_to"/); +651c578 +< it('if a context is not found, custom helperMissing is used', function() { +--- +> it('if a context is not found, custom helperMissing is used', () => { +654c581 +< .withHelper('helperMissing', function(mesg, options) { +--- +> .withHelper('helperMissing', function (mesg, options) { +662c589 +< it('if a value is not found, custom helperMissing is used', function() { +--- +> it('if a value is not found, custom helperMissing is used', () => { +665c592 +< .withHelper('helperMissing', function(options) { +--- +> .withHelper('helperMissing', function (options) { +674,675c601,602 +< describe('knownHelpers', function() { +< it('Known helper should render helper', function() { +--- +> describe('knownHelpers', () => { +> it('Known helper should render helper', () => { +678c605 +< knownHelpers: { hello: true } +--- +> knownHelpers: { hello: true }, +680c607 +< .withHelper('hello', function() { +--- +> .withHelper('hello', function () { +686c613 +< it('Unknown helper in knownHelpers only mode should be passed as undefined', function() { +--- +> it('Unknown helper in knownHelpers only mode should be passed as undefined', () => { +690c617 +< knownHelpersOnly: true +--- +> knownHelpersOnly: true, +692c619 +< .withHelper('typeof', function(arg) { +--- +> .withHelper('typeof', function (arg) { +695c622 +< .withHelper('hello', function() { +--- +> .withHelper('hello', function () { +701c628 +< it('Builtin helpers available in knownHelpers only mode', function() { +--- +> it('Builtin helpers available in knownHelpers only mode', () => { +704c631 +< knownHelpersOnly: true +--- +> knownHelpersOnly: true, +709c636 +< it('Field lookup works in knownHelpers only mode', function() { +--- +> it('Field lookup works in knownHelpers only mode', () => { +712c639 +< knownHelpersOnly: true +--- +> knownHelpersOnly: true, +718c645 +< it('Conditional blocks work in knownHelpers only mode', function() { +--- +> it('Conditional blocks work in knownHelpers only mode', () => { +721c648 +< knownHelpersOnly: true +--- +> knownHelpersOnly: true, +727c654 +< it('Invert blocks work in knownHelpers only mode', function() { +--- +> it('Invert blocks work in knownHelpers only mode', () => { +730c657 +< knownHelpersOnly: true +--- +> knownHelpersOnly: true, +736c663 +< it('Functions are bound to the context in knownHelpers only mode', function() { +--- +> it('Functions are bound to the context in knownHelpers only mode', () => { +739c666 +< knownHelpersOnly: true +--- +> knownHelpersOnly: true, +742c669 +< foo: function() { +--- +> foo() { +745c672 +< bar: 'bar' +--- +> bar: 'bar', +750c677 +< it('Unknown helper call in knownHelpers only mode should throw', function() { +--- +> it('Unknown helper call in knownHelpers only mode should throw', () => { +757,758c684,685 +< describe('blockHelperMissing', function() { +< it('lambdas are resolved by blockHelperMissing, not handlebars proper', function() { +--- +> describe('blockHelperMissing', () => { +> it('lambdas are resolved by blockHelperMissing, not handlebars proper', () => { +761c688 +< truthy: function() { +--- +> truthy() { +763c690 +< } +--- +> }, +768c695 +< it('lambdas resolved by blockHelperMissing are bound to the context', function() { +--- +> it('lambdas resolved by blockHelperMissing are bound to the context', () => { +771c698 +< truthy: function() { +--- +> truthy() { +774c701 +< truthiness: function() { +--- +> truthiness() { +776c703 +< } +--- +> }, +782,785c709,712 +< describe('name field', function() { +< var helpers = { +< blockHelperMissing: function() { +< return 'missing: ' + arguments[arguments.length - 1].name; +--- +> describe('name field', () => { +> const helpers = { +> blockHelperMissing(...args: any[]) { +> return 'missing: ' + args[args.length - 1].name; +787,788c714,718 +< helperMissing: function() { +< return 'helper missing: ' + arguments[arguments.length - 1].name; +--- +> helperMissing(...args: any[]) { +> return 'helper missing: ' + args[args.length - 1].name; +> }, +> helper(...args: any[]) { +> return 'ran: ' + args[args.length - 1].name; +790,792d719 +< helper: function() { +< return 'ran: ' + arguments[arguments.length - 1].name; +< } +795,798c722,723 +< it('should include in ambiguous mustache calls', function() { +< expectTemplate('{{helper}}') +< .withHelpers(helpers) +< .toCompileTo('ran: helper'); +--- +> it('should include in ambiguous mustache calls', () => { +> expectTemplate('{{helper}}').withHelpers(helpers).toCompileTo('ran: helper'); +801,804c726,727 +< it('should include in helper mustache calls', function() { +< expectTemplate('{{helper 1}}') +< .withHelpers(helpers) +< .toCompileTo('ran: helper'); +--- +> it('should include in helper mustache calls', () => { +> expectTemplate('{{helper 1}}').withHelpers(helpers).toCompileTo('ran: helper'); +807,810c730,731 +< it('should include in ambiguous block calls', function() { +< expectTemplate('{{#helper}}{{/helper}}') +< .withHelpers(helpers) +< .toCompileTo('ran: helper'); +--- +> it('should include in ambiguous block calls', () => { +> expectTemplate('{{#helper}}{{/helper}}').withHelpers(helpers).toCompileTo('ran: helper'); +813c734 +< it('should include in simple block calls', function() { +--- +> it('should include in simple block calls', () => { +819,822c740,741 +< it('should include in helper block calls', function() { +< expectTemplate('{{#helper 1}}{{/helper}}') +< .withHelpers(helpers) +< .toCompileTo('ran: helper'); +--- +> it('should include in helper block calls', () => { +> expectTemplate('{{#helper 1}}{{/helper}}').withHelpers(helpers).toCompileTo('ran: helper'); +825c744 +< it('should include in known helper calls', function() { +--- +> it('should include in known helper calls', () => { +829c748 +< knownHelpersOnly: true +--- +> knownHelpersOnly: true, +835c754 +< it('should include full id', function() { +--- +> it('should include full id', () => { +842c761 +< it('should include full id if a hash is passed', function() { +--- +> it('should include full id if a hash is passed', () => { +850,851c769,770 +< describe('name conflicts', function() { +< it('helpers take precedence over same-named context properties', function() { +--- +> describe('name conflicts', () => { +> it('helpers take precedence over same-named context properties', () => { +853c772 +< .withHelper('goodbye', function() { +--- +> .withHelper('goodbye', function (this: any) { +856c775 +< .withHelper('cruel', function(world) { +--- +> .withHelper('cruel', function (world) { +861c780 +< world: 'world' +--- +> world: 'world', +863d781 +< .withMessage('Helper executed') +867c785 +< it('helpers take precedence over same-named context properties$', function() { +--- +> it('helpers take precedence over same-named context properties$', () => { +869c787 +< .withHelper('goodbye', function(options) { +--- +> .withHelper('goodbye', function (this: any, options) { +872c790 +< .withHelper('cruel', function(world) { +--- +> .withHelper('cruel', function (world) { +877c795 +< world: 'world' +--- +> world: 'world', +879d796 +< .withMessage('Helper executed') +883c800 +< it('Scoped names take precedence over helpers', function() { +--- +> it('Scoped names take precedence over helpers', () => { +885c802 +< .withHelper('goodbye', function() { +--- +> .withHelper('goodbye', function (this: any) { +888c805 +< .withHelper('cruel', function(world) { +--- +> .withHelper('cruel', function (world) { +893c810 +< world: 'world' +--- +> world: 'world', +895d811 +< .withMessage('Helper not executed') +899,903c815,817 +< it('Scoped names take precedence over block helpers', function() { +< expectTemplate( +< '{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}' +< ) +< .withHelper('goodbye', function(options) { +--- +> it('Scoped names take precedence over block helpers', () => { +> expectTemplate('{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}') +> .withHelper('goodbye', function (this: any, options) { +906c820 +< .withHelper('cruel', function(world) { +--- +> .withHelper('cruel', function (world) { +911c825 +< world: 'world' +--- +> world: 'world', +913d826 +< .withMessage('Helper executed') +918,919c831,832 +< describe('block params', function() { +< it('should take presedence over context values', function() { +--- +> describe('block params', () => { +> it('should take presedence over context values', () => { +922,923c835,836 +< .withHelper('goodbyes', function(options) { +< equals(options.fn.blockParams, 1); +--- +> .withHelper('goodbyes', function (options) { +> expect(options.fn.blockParams).toEqual(1); +929c842 +< it('should take presedence over helper values', function() { +--- +> it('should take presedence over helper values', () => { +931c844 +< .withHelper('value', function() { +--- +> .withHelper('value', function () { +934,935c847,848 +< .withHelper('goodbyes', function(options) { +< equals(options.fn.blockParams, 1); +--- +> .withHelper('goodbyes', function (options) { +> expect(options.fn.blockParams).toEqual(1); +941,944c854,855 +< it('should not take presedence over pathed values', function() { +< expectTemplate( +< '{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}' +< ) +--- +> it('should not take presedence over pathed values', () => { +> expectTemplate('{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}') +946c857 +< .withHelper('value', function() { +--- +> .withHelper('value', function () { +949,950c860,861 +< .withHelper('goodbyes', function(options) { +< equals(options.fn.blockParams, 1); +--- +> .withHelper('goodbyes', function (this: any, options) { +> expect(options.fn.blockParams).toEqual(1); +956,957c867,868 +< it('should take presednece over parent block params', function() { +< var value = 1; +--- +> it('should take presednece over parent block params', () => { +> let value: number; +959c870,875 +< '{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}' +--- +> '{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}', +> { +> beforeEach() { +> value = 1; +> }, +> } +962c878 +< .withHelper('goodbyes', function(options) { +--- +> .withHelper('goodbyes', function (options) { +966,967c882 +< blockParams: +< options.fn.blockParams === 1 ? [value++, value++] : undefined +--- +> blockParams: options.fn.blockParams === 1 ? [value++, value++] : undefined, +974,977c889,890 +< it('should allow block params on chained helpers', function() { +< expectTemplate( +< '{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}' +< ) +--- +> it('should allow block params on chained helpers', () => { +> expectTemplate('{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}') +979,980c892,893 +< .withHelper('goodbyes', function(options) { +< equals(options.fn.blockParams, 1); +--- +> .withHelper('goodbyes', function (options) { +> expect(options.fn.blockParams).toEqual(1); +987,991c900,902 +< describe('built-in helpers malformed arguments ', function() { +< it('if helper - too few arguments', function() { +< expectTemplate('{{#if}}{{/if}}').toThrow( +< /#if requires exactly one argument/ +< ); +--- +> describe('built-in helpers malformed arguments ', () => { +> it('if helper - too few arguments', () => { +> expectTemplate('{{#if}}{{/if}}').toThrow(/#if requires exactly one argument/); +994,997c905,906 +< it('if helper - too many arguments, string', function() { +< expectTemplate('{{#if test "string"}}{{/if}}').toThrow( +< /#if requires exactly one argument/ +< ); +--- +> it('if helper - too many arguments, string', () => { +> expectTemplate('{{#if test "string"}}{{/if}}').toThrow(/#if requires exactly one argument/); +1000,1003c909,910 +< it('if helper - too many arguments, undefined', function() { +< expectTemplate('{{#if test undefined}}{{/if}}').toThrow( +< /#if requires exactly one argument/ +< ); +--- +> it('if helper - too many arguments, undefined', () => { +> expectTemplate('{{#if test undefined}}{{/if}}').toThrow(/#if requires exactly one argument/); +1006,1009c913,914 +< it('if helper - too many arguments, null', function() { +< expectTemplate('{{#if test null}}{{/if}}').toThrow( +< /#if requires exactly one argument/ +< ); +--- +> it('if helper - too many arguments, null', () => { +> expectTemplate('{{#if test null}}{{/if}}').toThrow(/#if requires exactly one argument/); +1012,1015c917,918 +< it('unless helper - too few arguments', function() { +< expectTemplate('{{#unless}}{{/unless}}').toThrow( +< /#unless requires exactly one argument/ +< ); +--- +> it('unless helper - too few arguments', () => { +> expectTemplate('{{#unless}}{{/unless}}').toThrow(/#unless requires exactly one argument/); +1018c921 +< it('unless helper - too many arguments', function() { +--- +> it('unless helper - too many arguments', () => { +1024,1027c927,928 +< it('with helper - too few arguments', function() { +< expectTemplate('{{#with}}{{/with}}').toThrow( +< /#with requires exactly one argument/ +< ); +--- +> it('with helper - too few arguments', () => { +> expectTemplate('{{#with}}{{/with}}').toThrow(/#with requires exactly one argument/); +1030c931 +< it('with helper - too many arguments', function() { +--- +> it('with helper - too many arguments', () => { +1037,1038c938,939 +< describe('the lookupProperty-option', function() { +< it('should be passed to custom helpers', function() { +--- +> describe('the lookupProperty-option', () => { +> it('should be passed to custom helpers', () => { +1040c941 +< .withHelper('testHelper', function testHelper(options) { +--- +> .withHelper('testHelper', function testHelper(this: any, options) { +1047a949,954 +> +> function deleteAllKeys(obj: { [key: string]: any }) { +> for (const key of Object.keys(obj)) { +> delete obj[key]; +> } +> } diff --git a/packages/kbn-handlebars/.patches/regressions.patch b/packages/kbn-handlebars/.patches/regressions.patch new file mode 100644 index 0000000000000..2fcd491310619 --- /dev/null +++ b/packages/kbn-handlebars/.patches/regressions.patch @@ -0,0 +1,518 @@ +1,2c1,11 +< describe('Regressions', function() { +< it('GH-94: Cannot read property of undefined', function() { +--- +> /* +> * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/kbn-handlebars/LICENSE` for more information. +> */ +> +> import { expectTemplate } from '../__jest__/test_bench'; +> +> describe('Regressions', () => { +> it('GH-94: Cannot read property of undefined', () => { +9,10c18,19 +< name: 'Charles Darwin' +< } +--- +> name: 'Charles Darwin', +> }, +13,15c22,24 +< title: 'Lazarillo de Tormes' +< } +< ] +--- +> title: 'Lazarillo de Tormes', +> }, +> ], +17d25 +< .withMessage('Renders without an undefined property error') +21,43c29,34 +< it("GH-150: Inverted sections print when they shouldn't", function() { +< var string = '{{^set}}not set{{/set}} :: {{#set}}set{{/set}}'; +< +< expectTemplate(string) +< .withMessage( +< "inverted sections run when property isn't present in context" +< ) +< .toCompileTo('not set :: '); +< +< expectTemplate(string) +< .withInput({ set: undefined }) +< .withMessage('inverted sections run when property is undefined') +< .toCompileTo('not set :: '); +< +< expectTemplate(string) +< .withInput({ set: false }) +< .withMessage('inverted sections run when property is false') +< .toCompileTo('not set :: '); +< +< expectTemplate(string) +< .withInput({ set: true }) +< .withMessage("inverted sections don't run when property is true") +< .toCompileTo(' :: set'); +--- +> it("GH-150: Inverted sections print when they shouldn't", () => { +> const string = '{{^set}}not set{{/set}} :: {{#set}}set{{/set}}'; +> expectTemplate(string).toCompileTo('not set :: '); +> expectTemplate(string).withInput({ set: undefined }).toCompileTo('not set :: '); +> expectTemplate(string).withInput({ set: false }).toCompileTo('not set :: '); +> expectTemplate(string).withInput({ set: true }).toCompileTo(' :: set'); +46c37 +< it('GH-158: Using array index twice, breaks the template', function() { +--- +> it('GH-158: Using array index twice, breaks the template', () => { +49d39 +< .withMessage('it works as expected') +53,54c43,44 +< it("bug reported by @fat where lambdas weren't being properly resolved", function() { +< var string = +--- +> it("bug reported by @fat where lambdas weren't being properly resolved", () => { +> const string = +69,70c59,60 +< var data = { +< thing: function() { +--- +> const data = { +> thing() { +76c66 +< { className: 'three', word: '@sayrer' } +--- +> { className: 'three', word: '@sayrer' }, +78c68 +< hasThings: function() { +--- +> hasThings() { +80c70 +< } +--- +> }, +83c73 +< var output = +--- +> const output = +92,94c82 +< expectTemplate(string) +< .withInput(data) +< .toCompileTo(output); +--- +> expectTemplate(string).withInput(data).toCompileTo(output); +97,100c85,86 +< it('GH-408: Multiple loops fail', function() { +< expectTemplate( +< '{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}' +< ) +--- +> it('GH-408: Multiple loops fail', () => { +> expectTemplate('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}') +103c89 +< { name: 'Jane Doe', location: { city: 'New York' } } +--- +> { name: 'Jane Doe', location: { city: 'New York' } }, +105d90 +< .withMessage('It should output multiple times') +109,110c94,95 +< it('GS-428: Nested if else rendering', function() { +< var succeedingTemplate = +--- +> it('GS-428: Nested if else rendering', () => { +> const succeedingTemplate = +112c97 +< var failingTemplate = +--- +> const failingTemplate = +115,116c100,101 +< var helpers = { +< blk: function(block) { +--- +> const helpers = { +> blk(block: Handlebars.HelperOptions) { +119c104 +< inverse: function(block) { +--- +> inverse(block: Handlebars.HelperOptions) { +121c106 +< } +--- +> }, +124,130c109,110 +< expectTemplate(succeedingTemplate) +< .withHelpers(helpers) +< .toCompileTo(' Expected '); +< +< expectTemplate(failingTemplate) +< .withHelpers(helpers) +< .toCompileTo(' Expected '); +--- +> expectTemplate(succeedingTemplate).withHelpers(helpers).toCompileTo(' Expected '); +> expectTemplate(failingTemplate).withHelpers(helpers).toCompileTo(' Expected '); +133,136c113,114 +< it('GH-458: Scoped this identifier', function() { +< expectTemplate('{{./foo}}') +< .withInput({ foo: 'bar' }) +< .toCompileTo('bar'); +--- +> it('GH-458: Scoped this identifier', () => { +> expectTemplate('{{./foo}}').withInput({ foo: 'bar' }).toCompileTo('bar'); +139c117 +< it('GH-375: Unicode line terminators', function() { +--- +> it('GH-375: Unicode line terminators', () => { +143c121 +< it('GH-534: Object prototype aliases', function() { +--- +> it('GH-534: Object prototype aliases', () => { +144a123 +> // @ts-expect-error +147,149c126 +< expectTemplate('{{foo}}') +< .withInput({ foo: 'bar' }) +< .toCompileTo('bar'); +--- +> expectTemplate('{{foo}}').withInput({ foo: 'bar' }).toCompileTo('bar'); +150a128 +> // @ts-expect-error +155,157c133,135 +< it('GH-437: Matching escaping', function() { +< expectTemplate('{{{a}}').toThrow(Error, /Parse error on/); +< expectTemplate('{{a}}}').toThrow(Error, /Parse error on/); +--- +> it('GH-437: Matching escaping', () => { +> expectTemplate('{{{a}}').toThrow(/Parse error on/); +> expectTemplate('{{a}}}').toThrow(/Parse error on/); +160,166c138,140 +< it('GH-676: Using array in escaping mustache fails', function() { +< var data = { arr: [1, 2] }; +< +< expectTemplate('{{arr}}') +< .withInput(data) +< .withMessage('it works as expected') +< .toCompileTo(data.arr.toString()); +--- +> it('GH-676: Using array in escaping mustache fails', () => { +> const data = { arr: [1, 2] }; +> expectTemplate('{{arr}}').withInput(data).toCompileTo(data.arr.toString()); +169c143 +< it('Mustache man page', function() { +--- +> it('Mustache man page', () => { +177c151 +< in_ca: true +--- +> in_ca: true, +179,182c153 +< .withMessage('the hello world mustache example works') +< .toCompileTo( +< 'Hello Chris. You have just won $10000! Well, $6000, after taxes.' +< ); +--- +> .toCompileTo('Hello Chris. You have just won $10000! Well, $6000, after taxes.'); +185c156 +< it('GH-731: zero context rendering', function() { +--- +> it('GH-731: zero context rendering', () => { +189c160 +< bar: 'OK' +--- +> bar: 'OK', +194,197c165,166 +< it('GH-820: zero pathed rendering', function() { +< expectTemplate('{{foo.bar}}') +< .withInput({ foo: 0 }) +< .toCompileTo(''); +--- +> it('GH-820: zero pathed rendering', () => { +> expectTemplate('{{foo.bar}}').withInput({ foo: 0 }).toCompileTo(''); +200c169 +< it('GH-837: undefined values for helpers', function() { +--- +> it('GH-837: undefined values for helpers', () => { +203c172 +< str: function(value) { +--- +> str(value) { +205c174 +< } +--- +> }, +210c179 +< it('GH-926: Depths and de-dupe', function() { +--- +> it('GH-926: Depths and de-dupe', () => { +217c186 +< notData: [1] +--- +> notData: [1], +222c191 +< it('GH-1021: Each empty string key', function() { +--- +> it('GH-1021: Each empty string key', () => { +228,229c197,198 +< value: 10000 +< } +--- +> value: 10000, +> }, +234,248c203,204 +< it('GH-1054: Should handle simple safe string responses', function() { +< expectTemplate('{{#wrap}}{{>partial}}{{/wrap}}') +< .withHelpers({ +< wrap: function(options) { +< return new Handlebars.SafeString(options.fn()); +< } +< }) +< .withPartials({ +< partial: '{{#wrap}}{{/wrap}}' +< }) +< .toCompileTo(''); +< }); +< +< it('GH-1065: Sparse arrays', function() { +< var array = []; +--- +> it('GH-1065: Sparse arrays', () => { +> const array = []; +252c208 +< .withInput({ array: array }) +--- +> .withInput({ array }) +256c212 +< it('GH-1093: Undefined helper context', function() { +--- +> it('GH-1093: Undefined helper context', () => { +260c216 +< helper: function() { +--- +> helper(this: any) { +263c219 +< for (var name in this) { +--- +> for (const name in this) { +270c226 +< } +--- +> }, +275,306c231 +< it('should support multiple levels of inline partials', function() { +< expectTemplate( +< '{{#> layout}}{{#*inline "subcontent"}}subcontent{{/inline}}{{/layout}}' +< ) +< .withPartials({ +< doctype: 'doctype{{> content}}', +< layout: +< '{{#> doctype}}{{#*inline "content"}}layout{{> subcontent}}{{/inline}}{{/doctype}}' +< }) +< .toCompileTo('doctypelayoutsubcontent'); +< }); +< +< it('GH-1089: should support failover content in multiple levels of inline partials', function() { +< expectTemplate('{{#> layout}}{{/layout}}') +< .withPartials({ +< doctype: 'doctype{{> content}}', +< layout: +< '{{#> doctype}}{{#*inline "content"}}layout{{#> subcontent}}subcontent{{/subcontent}}{{/inline}}{{/doctype}}' +< }) +< .toCompileTo('doctypelayoutsubcontent'); +< }); +< +< it('GH-1099: should support greater than 3 nested levels of inline partials', function() { +< expectTemplate('{{#> layout}}Outer{{/layout}}') +< .withPartials({ +< layout: '{{#> inner}}Inner{{/inner}}{{> @partial-block }}', +< inner: '' +< }) +< .toCompileTo('Outer'); +< }); +< +< it('GH-1135 : Context handling within each iteration', function() { +--- +> it('GH-1135 : Context handling within each iteration', () => { +315c240 +< myif: function(conditional, options) { +--- +> myif(conditional, options) { +321c246 +< } +--- +> }, +326,343c251,252 +< it('GH-1186: Support block params for existing programs', function() { +< expectTemplate( +< '{{#*inline "test"}}{{> @partial-block }}{{/inline}}' + +< '{{#>test }}{{#each listOne as |item|}}{{ item }}{{/each}}{{/test}}' + +< '{{#>test }}{{#each listTwo as |item|}}{{ item }}{{/each}}{{/test}}' +< ) +< .withInput({ +< listOne: ['a'], +< listTwo: ['b'] +< }) +< .withMessage('') +< .toCompileTo('ab'); +< }); +< +< it('GH-1319: "unless" breaks when "each" value equals "null"', function() { +< expectTemplate( +< '{{#each list}}{{#unless ./prop}}parent={{../value}} {{/unless}}{{/each}}' +< ) +--- +> it('GH-1319: "unless" breaks when "each" value equals "null"', () => { +> expectTemplate('{{#each list}}{{#unless ./prop}}parent={{../value}} {{/unless}}{{/each}}') +346c255 +< list: [null, 'a'] +--- +> list: [null, 'a'], +348d256 +< .withMessage('') +352,457c260 +< it('GH-1341: 4.0.7 release breaks {{#if @partial-block}} usage', function() { +< expectTemplate('template {{>partial}} template') +< .withPartials({ +< partialWithBlock: +< '{{#if @partial-block}} block {{> @partial-block}} block {{/if}}', +< partial: '{{#> partialWithBlock}} partial {{/partialWithBlock}}' +< }) +< .toCompileTo('template block partial block template'); +< }); +< +< describe('GH-1561: 4.3.x should still work with precompiled templates from 4.0.0 <= x < 4.3.0', function() { +< it('should compile and execute templates', function() { +< var newHandlebarsInstance = Handlebars.create(); +< +< registerTemplate(newHandlebarsInstance, compiledTemplateVersion7()); +< newHandlebarsInstance.registerHelper('loud', function(value) { +< return value.toUpperCase(); +< }); +< var result = newHandlebarsInstance.templates['test.hbs']({ +< name: 'yehuda' +< }); +< equals(result.trim(), 'YEHUDA'); +< }); +< +< it('should call "helperMissing" if a helper is missing', function() { +< var newHandlebarsInstance = Handlebars.create(); +< +< shouldThrow( +< function() { +< registerTemplate(newHandlebarsInstance, compiledTemplateVersion7()); +< newHandlebarsInstance.templates['test.hbs']({}); +< }, +< Handlebars.Exception, +< 'Missing helper: "loud"' +< ); +< }); +< +< it('should pass "options.lookupProperty" to "lookup"-helper, even with old templates', function() { +< var newHandlebarsInstance = Handlebars.create(); +< registerTemplate( +< newHandlebarsInstance, +< compiledTemplateVersion7_usingLookupHelper() +< ); +< +< newHandlebarsInstance.templates['test.hbs']({}); +< +< expect( +< newHandlebarsInstance.templates['test.hbs']({ +< property: 'a', +< test: { a: 'b' } +< }) +< ).to.equal('b'); +< }); +< +< function registerTemplate(Handlebars, compileTemplate) { +< var template = Handlebars.template, +< templates = (Handlebars.templates = Handlebars.templates || {}); +< templates['test.hbs'] = template(compileTemplate); +< } +< +< function compiledTemplateVersion7() { +< return { +< compiler: [7, '>= 4.0.0'], +< main: function(container, depth0, helpers, partials, data) { +< return ( +< container.escapeExpression( +< ( +< helpers.loud || +< (depth0 && depth0.loud) || +< helpers.helperMissing +< ).call( +< depth0 != null ? depth0 : container.nullContext || {}, +< depth0 != null ? depth0.name : depth0, +< { name: 'loud', hash: {}, data: data } +< ) +< ) + '\n\n' +< ); +< }, +< useData: true +< }; +< } +< +< function compiledTemplateVersion7_usingLookupHelper() { +< // This is the compiled version of "{{lookup test property}}" +< return { +< compiler: [7, '>= 4.0.0'], +< main: function(container, depth0, helpers, partials, data) { +< return container.escapeExpression( +< helpers.lookup.call( +< depth0 != null ? depth0 : container.nullContext || {}, +< depth0 != null ? depth0.test : depth0, +< depth0 != null ? depth0.property : depth0, +< { +< name: 'lookup', +< hash: {}, +< data: data +< } +< ) +< ); +< }, +< useData: true +< }; +< } +< }); +< +< it('should allow hash with protected array names', function() { +--- +> it('should allow hash with protected array names', () => { +461c264 +< helpa: function(options) { +--- +> helpa(options) { +463c266 +< } +--- +> }, +468,496c271,272 +< describe('GH-1598: Performance degradation for partials since v4.3.0', function() { +< // Do not run test for runs without compiler +< if (!Handlebars.compile) { +< return; +< } +< +< var newHandlebarsInstance; +< beforeEach(function() { +< newHandlebarsInstance = Handlebars.create(); +< }); +< afterEach(function() { +< sinon.restore(); +< }); +< +< it('should only compile global partials once', function() { +< var templateSpy = sinon.spy(newHandlebarsInstance, 'template'); +< newHandlebarsInstance.registerPartial({ +< dude: 'I am a partial' +< }); +< var string = 'Dudes: {{> dude}} {{> dude}}'; +< newHandlebarsInstance.compile(string)(); // This should compile template + partial once +< newHandlebarsInstance.compile(string)(); // This should only compile template +< equal(templateSpy.callCount, 3); +< sinon.restore(); +< }); +< }); +< +< describe("GH-1639: TypeError: Cannot read property 'apply' of undefined\" when handlebars version > 4.6.0 (undocumented, deprecated usage)", function() { +< it('should treat undefined helpers like non-existing helpers', function() { +--- +> describe("GH-1639: TypeError: Cannot read property 'apply' of undefined\" when handlebars version > 4.6.0 (undocumented, deprecated usage)", () => { +> it('should treat undefined helpers like non-existing helpers', () => { diff --git a/packages/kbn-handlebars/.patches/security.patch b/packages/kbn-handlebars/.patches/security.patch new file mode 100644 index 0000000000000..89d2060d8977b --- /dev/null +++ b/packages/kbn-handlebars/.patches/security.patch @@ -0,0 +1,443 @@ +1,10c1,15 +< describe('security issues', function() { +< describe('GH-1495: Prevent Remote Code Execution via constructor', function() { +< it('should not allow constructors to be accessed', function() { +< expectTemplate('{{lookup (lookup this "constructor") "name"}}') +< .withInput({}) +< .toCompileTo(''); +< +< expectTemplate('{{constructor.name}}') +< .withInput({}) +< .toCompileTo(''); +--- +> /* +> * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/kbn-handlebars/LICENSE` for more information. +> */ +> +> import Handlebars from '..'; +> import { expectTemplate } from '../__jest__/test_bench'; +> +> describe('security issues', () => { +> describe('GH-1495: Prevent Remote Code Execution via constructor', () => { +> it('should not allow constructors to be accessed', () => { +> expectTemplate('{{lookup (lookup this "constructor") "name"}}').withInput({}).toCompileTo(''); +> expectTemplate('{{constructor.name}}').withInput({}).toCompileTo(''); +13c18 +< it('GH-1603: should not allow constructors to be accessed (lookup via toString)', function() { +--- +> it('GH-1603: should not allow constructors to be accessed (lookup via toString)', () => { +16c21 +< .withHelper('list', function(element) { +--- +> .withHelper('list', function (element) { +22c27 +< it('should allow the "constructor" property to be accessed if it is an "ownProperty"', function() { +--- +> it('should allow the "constructor" property to be accessed if it is an "ownProperty"', () => { +32c37 +< it('should allow the "constructor" property to be accessed if it is an "own property"', function() { +--- +> it('should allow the "constructor" property to be accessed if it is an "own property"', () => { +39,45c44,46 +< describe('GH-1558: Prevent explicit call of helperMissing-helpers', function() { +< if (!Handlebars.compile) { +< return; +< } +< +< describe('without the option "allowExplicitCallOfHelperMissing"', function() { +< it('should throw an exception when calling "{{helperMissing}}" ', function() { +--- +> describe('GH-1558: Prevent explicit call of helperMissing-helpers', () => { +> describe('without the option "allowExplicitCallOfHelperMissing"', () => { +> it('should throw an exception when calling "{{helperMissing}}" ', () => { +49c50 +< it('should throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function() { +--- +> it('should throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', () => { +53,56c54,57 +< it('should throw an exception when calling "{{blockHelperMissing "abc" .}}" ', function() { +< var functionCalls = []; +< expect(function() { +< var template = Handlebars.compile('{{blockHelperMissing "abc" .}}'); +--- +> it('should throw an exception when calling "{{blockHelperMissing "abc" .}}" ', () => { +> const functionCalls = []; +> expect(() => { +> const template = Handlebars.compile('{{blockHelperMissing "abc" .}}'); +58c59 +< fn: function() { +--- +> fn() { +60c61 +< } +--- +> }, +62,63c63,64 +< }).to.throw(Error); +< expect(functionCalls.length).to.equal(0); +--- +> }).toThrow(Error); +> expect(functionCalls.length).toEqual(0); +66c67 +< it('should throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function() { +--- +> it('should throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', () => { +69c70 +< fn: function() { +--- +> fn() { +71c72 +< } +--- +> }, +76,110d76 +< +< describe('with the option "allowCallsToHelperMissing" set to true', function() { +< it('should not throw an exception when calling "{{helperMissing}}" ', function() { +< var template = Handlebars.compile('{{helperMissing}}'); +< template({}, { allowCallsToHelperMissing: true }); +< }); +< +< it('should not throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function() { +< var template = Handlebars.compile( +< '{{#helperMissing}}{{/helperMissing}}' +< ); +< template({}, { allowCallsToHelperMissing: true }); +< }); +< +< it('should not throw an exception when calling "{{blockHelperMissing "abc" .}}" ', function() { +< var functionCalls = []; +< var template = Handlebars.compile('{{blockHelperMissing "abc" .}}'); +< template( +< { +< fn: function() { +< functionCalls.push('called'); +< } +< }, +< { allowCallsToHelperMissing: true } +< ); +< equals(functionCalls.length, 1); +< }); +< +< it('should not throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function() { +< var template = Handlebars.compile( +< '{{#blockHelperMissing true}}sdads{{/blockHelperMissing}}' +< ); +< template({}, { allowCallsToHelperMissing: true }); +< }); +< }); +113,114c79,81 +< describe('GH-1563', function() { +< it('should not allow to access constructor after overriding via __defineGetter__', function() { +--- +> describe('GH-1563', () => { +> it('should not allow to access constructor after overriding via __defineGetter__', () => { +> // @ts-expect-error +116c83 +< return this.skip(); // Browser does not support this exploit anyway +--- +> return; // Browser does not support this exploit anyway +130,131c97,98 +< describe('GH-1595: dangerous properties', function() { +< var templates = [ +--- +> describe('GH-1595: dangerous properties', () => { +> const templates = [ +141c108 +< '{{lookup this "__proto__"}}' +--- +> '{{lookup this "__proto__"}}', +144,257c111,114 +< templates.forEach(function(template) { +< describe('access should be denied to ' + template, function() { +< it('by default', function() { +< expectTemplate(template) +< .withInput({}) +< .toCompileTo(''); +< }); +< it(' with proto-access enabled', function() { +< expectTemplate(template) +< .withInput({}) +< .withRuntimeOptions({ +< allowProtoPropertiesByDefault: true, +< allowProtoMethodsByDefault: true +< }) +< .toCompileTo(''); +< }); +< }); +< }); +< }); +< describe('GH-1631: disallow access to prototype functions', function() { +< function TestClass() {} +< +< TestClass.prototype.aProperty = 'propertyValue'; +< TestClass.prototype.aMethod = function() { +< return 'returnValue'; +< }; +< +< beforeEach(function() { +< handlebarsEnv.resetLoggedPropertyAccesses(); +< }); +< +< afterEach(function() { +< sinon.restore(); +< }); +< +< describe('control access to prototype methods via "allowedProtoMethods"', function() { +< checkProtoMethodAccess({}); +< +< describe('in compat mode', function() { +< checkProtoMethodAccess({ compat: true }); +< }); +< +< function checkProtoMethodAccess(compileOptions) { +< it('should be prohibited by default and log a warning', function() { +< var spy = sinon.spy(console, 'error'); +< +< expectTemplate('{{aMethod}}') +< .withInput(new TestClass()) +< .withCompileOptions(compileOptions) +< .toCompileTo(''); +< +< expect(spy.calledOnce).to.be.true(); +< expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/); +< }); +< +< it('should only log the warning once', function() { +< var spy = sinon.spy(console, 'error'); +< +< expectTemplate('{{aMethod}}') +< .withInput(new TestClass()) +< .withCompileOptions(compileOptions) +< .toCompileTo(''); +< +< expectTemplate('{{aMethod}}') +< .withInput(new TestClass()) +< .withCompileOptions(compileOptions) +< .toCompileTo(''); +< +< expect(spy.calledOnce).to.be.true(); +< expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/); +< }); +< +< it('can be allowed, which disables the warning', function() { +< var spy = sinon.spy(console, 'error'); +< +< expectTemplate('{{aMethod}}') +< .withInput(new TestClass()) +< .withCompileOptions(compileOptions) +< .withRuntimeOptions({ +< allowedProtoMethods: { +< aMethod: true +< } +< }) +< .toCompileTo('returnValue'); +< +< expect(spy.callCount).to.equal(0); +< }); +< +< it('can be turned on by default, which disables the warning', function() { +< var spy = sinon.spy(console, 'error'); +< +< expectTemplate('{{aMethod}}') +< .withInput(new TestClass()) +< .withCompileOptions(compileOptions) +< .withRuntimeOptions({ +< allowProtoMethodsByDefault: true +< }) +< .toCompileTo('returnValue'); +< +< expect(spy.callCount).to.equal(0); +< }); +< +< it('can be turned off by default, which disables the warning', function() { +< var spy = sinon.spy(console, 'error'); +< +< expectTemplate('{{aMethod}}') +< .withInput(new TestClass()) +< .withCompileOptions(compileOptions) +< .withRuntimeOptions({ +< allowProtoMethodsByDefault: false +< }) +< .toCompileTo(''); +< +< expect(spy.callCount).to.equal(0); +--- +> templates.forEach((template) => { +> describe('access should be denied to ' + template, () => { +> it('by default', () => { +> expectTemplate(template).withInput({}).toCompileTo(''); +259,399d115 +< +< it('can be turned off, if turned on by default', function() { +< expectTemplate('{{aMethod}}') +< .withInput(new TestClass()) +< .withCompileOptions(compileOptions) +< .withRuntimeOptions({ +< allowProtoMethodsByDefault: true, +< allowedProtoMethods: { +< aMethod: false +< } +< }) +< .toCompileTo(''); +< }); +< } +< +< it('should cause the recursive lookup by default (in "compat" mode)', function() { +< expectTemplate('{{#aString}}{{trim}}{{/aString}}') +< .withInput({ aString: ' abc ', trim: 'trim' }) +< .withCompileOptions({ compat: true }) +< .toCompileTo('trim'); +< }); +< +< it('should not cause the recursive lookup if allowed through options(in "compat" mode)', function() { +< expectTemplate('{{#aString}}{{trim}}{{/aString}}') +< .withInput({ aString: ' abc ', trim: 'trim' }) +< .withCompileOptions({ compat: true }) +< .withRuntimeOptions({ +< allowedProtoMethods: { +< trim: true +< } +< }) +< .toCompileTo('abc'); +< }); +< }); +< +< describe('control access to prototype non-methods via "allowedProtoProperties" and "allowProtoPropertiesByDefault', function() { +< checkProtoPropertyAccess({}); +< +< describe('in compat-mode', function() { +< checkProtoPropertyAccess({ compat: true }); +< }); +< +< describe('in strict-mode', function() { +< checkProtoPropertyAccess({ strict: true }); +< }); +< +< function checkProtoPropertyAccess(compileOptions) { +< it('should be prohibited by default and log a warning', function() { +< var spy = sinon.spy(console, 'error'); +< +< expectTemplate('{{aProperty}}') +< .withInput(new TestClass()) +< .withCompileOptions(compileOptions) +< .toCompileTo(''); +< +< expect(spy.calledOnce).to.be.true(); +< expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/); +< }); +< +< it('can be explicitly prohibited by default, which disables the warning', function() { +< var spy = sinon.spy(console, 'error'); +< +< expectTemplate('{{aProperty}}') +< .withInput(new TestClass()) +< .withCompileOptions(compileOptions) +< .withRuntimeOptions({ +< allowProtoPropertiesByDefault: false +< }) +< .toCompileTo(''); +< +< expect(spy.callCount).to.equal(0); +< }); +< +< it('can be turned on, which disables the warning', function() { +< var spy = sinon.spy(console, 'error'); +< +< expectTemplate('{{aProperty}}') +< .withInput(new TestClass()) +< .withCompileOptions(compileOptions) +< .withRuntimeOptions({ +< allowedProtoProperties: { +< aProperty: true +< } +< }) +< .toCompileTo('propertyValue'); +< +< expect(spy.callCount).to.equal(0); +< }); +< +< it('can be turned on by default, which disables the warning', function() { +< var spy = sinon.spy(console, 'error'); +< +< expectTemplate('{{aProperty}}') +< .withInput(new TestClass()) +< .withCompileOptions(compileOptions) +< .withRuntimeOptions({ +< allowProtoPropertiesByDefault: true +< }) +< .toCompileTo('propertyValue'); +< +< expect(spy.callCount).to.equal(0); +< }); +< +< it('can be turned off, if turned on by default', function() { +< expectTemplate('{{aProperty}}') +< .withInput(new TestClass()) +< .withCompileOptions(compileOptions) +< .withRuntimeOptions({ +< allowProtoPropertiesByDefault: true, +< allowedProtoProperties: { +< aProperty: false +< } +< }) +< .toCompileTo(''); +< }); +< } +< }); +< +< describe('compatibility with old runtimes, that do not provide the function "container.lookupProperty"', function() { +< beforeEach(function simulateRuntimeWithoutLookupProperty() { +< var oldTemplateMethod = handlebarsEnv.template; +< sinon.replace(handlebarsEnv, 'template', function(templateSpec) { +< templateSpec.main = wrapToAdjustContainer(templateSpec.main); +< return oldTemplateMethod.call(this, templateSpec); +< }); +< }); +< +< afterEach(function() { +< sinon.restore(); +< }); +< +< it('should work with simple properties', function() { +< expectTemplate('{{aProperty}}') +< .withInput({ aProperty: 'propertyValue' }) +< .toCompileTo('propertyValue'); +< }); +< +< it('should work with Array.prototype.length', function() { +< expectTemplate('{{anArray.length}}') +< .withInput({ anArray: ['a', 'b', 'c'] }) +< .toCompileTo('3'); +404,409c120,122 +< describe('escapes template variables', function() { +< it('in compat mode', function() { +< expectTemplate("{{'a\\b'}}") +< .withCompileOptions({ compat: true }) +< .withInput({ 'a\\b': 'c' }) +< .toCompileTo('c'); +--- +> describe('escapes template variables', () => { +> it('in default mode', () => { +> expectTemplate("{{'a\\b'}}").withCompileOptions().withInput({ 'a\\b': 'c' }).toCompileTo('c'); +412,418c125 +< it('in default mode', function() { +< expectTemplate("{{'a\\b'}}") +< .withCompileOptions() +< .withInput({ 'a\\b': 'c' }) +< .toCompileTo('c'); +< }); +< it('in default mode', function() { +--- +> it('in strict mode', () => { +426,432d132 +< +< function wrapToAdjustContainer(precompiledTemplateFunction) { +< return function templateFunctionWrapper(container /*, more args */) { +< delete container.lookupProperty; +< return precompiledTemplateFunction.apply(this, arguments); +< }; +< } diff --git a/packages/kbn-handlebars/.patches/strict.patch b/packages/kbn-handlebars/.patches/strict.patch new file mode 100644 index 0000000000000..be50113e1416d --- /dev/null +++ b/packages/kbn-handlebars/.patches/strict.patch @@ -0,0 +1,180 @@ +1,5c1,12 +< var Exception = Handlebars.Exception; +< +< describe('strict', function() { +< describe('strict mode', function() { +< it('should error on missing property lookup', function() { +--- +> /* +> * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/kbn-handlebars/LICENSE` for more information. +> */ +> +> import { expectTemplate } from '../__jest__/test_bench'; +> +> describe('strict', () => { +> describe('strict mode', () => { +> it('should error on missing property lookup', () => { +8c15 +< .toThrow(Exception, /"hello" not defined in/); +--- +> .toThrow(/"hello" not defined in/); +11c18 +< it('should error on missing child', function() { +--- +> it('should error on missing child', () => { +20c27 +< .toThrow(Exception, /"bar" not defined in/); +--- +> .toThrow(/"bar" not defined in/); +23c30 +< it('should handle explicit undefined', function() { +--- +> it('should handle explicit undefined', () => { +30c37 +< it('should error on missing property lookup in known helpers mode', function() { +--- +> it('should error on missing property lookup in known helpers mode', () => { +34c41 +< knownHelpersOnly: true +--- +> knownHelpersOnly: true, +36c43 +< .toThrow(Exception, /"hello" not defined in/); +--- +> .toThrow(/"hello" not defined in/); +39,42c46,47 +< it('should error on missing context', function() { +< expectTemplate('{{hello}}') +< .withCompileOptions({ strict: true }) +< .toThrow(Error); +--- +> it('should error on missing context', () => { +> expectTemplate('{{hello}}').withCompileOptions({ strict: true }).toThrow(Error); +45,47c50,52 +< it('should error on missing data lookup', function() { +< var xt = expectTemplate('{{@hello}}').withCompileOptions({ +< strict: true +--- +> it('should error on missing data lookup', () => { +> const xt = expectTemplate('{{@hello}}').withCompileOptions({ +> strict: true, +55c60 +< it('should not run helperMissing for helper calls', function() { +--- +> it('should not run helperMissing for helper calls', () => { +59c64 +< .toThrow(Exception, /"hello" not defined in/); +--- +> .toThrow(/"hello" not defined in/); +64c69 +< .toThrow(Exception, /"hello" not defined in/); +--- +> .toThrow(/"hello" not defined in/); +67c72 +< it('should throw on ambiguous blocks', function() { +--- +> it('should throw on ambiguous blocks', () => { +70c75 +< .toThrow(Exception, /"hello" not defined in/); +--- +> .toThrow(/"hello" not defined in/); +74c79 +< .toThrow(Exception, /"hello" not defined in/); +--- +> .toThrow(/"hello" not defined in/); +79c84 +< .toThrow(Exception, /"bar" not defined in/); +--- +> .toThrow(/"bar" not defined in/); +82c87 +< it('should allow undefined parameters when passed to helpers', function() { +--- +> it('should allow undefined parameters when passed to helpers', () => { +88c93 +< it('should allow undefined hash when passed to helpers', function() { +--- +> it('should allow undefined hash when passed to helpers', () => { +91c96 +< strict: true +--- +> strict: true, +94,96c99,101 +< helper: function(options) { +< equals('value' in options.hash, true); +< equals(options.hash.value, undefined); +--- +> helper(options) { +> expect('value' in options.hash).toEqual(true); +> expect(options.hash.value).toBeUndefined(); +98c103 +< } +--- +> }, +103c108 +< it('should show error location on missing property lookup', function() { +--- +> it('should show error location on missing property lookup', () => { +106c111 +< .toThrow(Exception, '"hello" not defined in [object Object] - 4:5'); +--- +> .toThrow('"hello" not defined in [object Object] - 4:5'); +109c114 +< it('should error contains correct location properties on missing property lookup', function() { +--- +> it('should error contains correct location properties on missing property lookup', () => { +111,114c116,118 +< var template = CompilerContext.compile('\n\n\n {{hello}}', { +< strict: true +< }); +< template({}); +--- +> expectTemplate('\n\n\n {{hello}}') +> .withCompileOptions({ strict: true }) +> .toCompileTo('throw before asserting this'); +116,119c120,123 +< equals(error.lineNumber, 4); +< equals(error.endLineNumber, 4); +< equals(error.column, 5); +< equals(error.endColumn, 10); +--- +> expect(error.lineNumber).toEqual(4); +> expect(error.endLineNumber).toEqual(4); +> expect(error.column).toEqual(5); +> expect(error.endColumn).toEqual(10); +124,128c128,130 +< describe('assume objects', function() { +< it('should ignore missing property', function() { +< expectTemplate('{{hello}}') +< .withCompileOptions({ assumeObjects: true }) +< .toCompileTo(''); +--- +> describe('assume objects', () => { +> it('should ignore missing property', () => { +> expectTemplate('{{hello}}').withCompileOptions({ assumeObjects: true }).toCompileTo(''); +131c133 +< it('should ignore missing child', function() { +--- +> it('should ignore missing child', () => { +138,141c140,141 +< it('should error on missing object', function() { +< expectTemplate('{{hello.bar}}') +< .withCompileOptions({ assumeObjects: true }) +< .toThrow(Error); +--- +> it('should error on missing object', () => { +> expectTemplate('{{hello.bar}}').withCompileOptions({ assumeObjects: true }).toThrow(Error); +144c144 +< it('should error on missing context', function() { +--- +> it('should error on missing context', () => { +151c151 +< it('should error on missing data lookup', function() { +--- +> it('should error on missing data lookup', () => { +158c158 +< it('should execute blockHelperMissing', function() { +--- +> it('should execute blockHelperMissing', () => { diff --git a/packages/kbn-handlebars/.patches/subexpressions.patch b/packages/kbn-handlebars/.patches/subexpressions.patch new file mode 100644 index 0000000000000..bc29db240f3c1 --- /dev/null +++ b/packages/kbn-handlebars/.patches/subexpressions.patch @@ -0,0 +1,318 @@ +1,2c1,12 +< describe('subexpressions', function() { +< it('arg-less helper', function() { +--- +> /* +> * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/kbn-handlebars/LICENSE` for more information. +> */ +> +> import Handlebars from '..'; +> import { expectTemplate } from '../__jest__/test_bench'; +> +> describe('subexpressions', () => { +> it('arg-less helper', () => { +5c15 +< foo: function(val) { +--- +> foo(val) { +8c18 +< bar: function() { +--- +> bar() { +10c20 +< } +--- +> }, +15c25 +< it('helper w args', function() { +--- +> it('helper w args', () => { +19c29 +< blog: function(val) { +--- +> blog(val) { +22c32 +< equal: function(x, y) { +--- +> equal(x, y) { +24c34 +< } +--- +> }, +29c39 +< it('mixed paths and helpers', function() { +--- +> it('mixed paths and helpers', () => { +33c43 +< blog: function(val, that, theOther) { +--- +> blog(val, that, theOther) { +36c46 +< equal: function(x, y) { +--- +> equal(x, y) { +38c48 +< } +--- +> }, +43c53 +< it('supports much nesting', function() { +--- +> it('supports much nesting', () => { +47c57 +< blog: function(val) { +--- +> blog(val) { +50c60 +< equal: function(x, y) { +--- +> equal(x, y) { +52c62 +< } +--- +> }, +57,60c67,70 +< it('GH-800 : Complex subexpressions', function() { +< var context = { a: 'a', b: 'b', c: { c: 'c' }, d: 'd', e: { e: 'e' } }; +< var helpers = { +< dash: function(a, b) { +--- +> it('GH-800 : Complex subexpressions', () => { +> const context = { a: 'a', b: 'b', c: { c: 'c' }, d: 'd', e: { e: 'e' } }; +> const helpers = { +> dash(a: any, b: any) { +63c73 +< concat: function(a, b) { +--- +> concat(a: any, b: any) { +65c75 +< } +--- +> }, +94,97c104,107 +< it('provides each nested helper invocation its own options hash', function() { +< var lastOptions = null; +< var helpers = { +< equal: function(x, y, options) { +--- +> it('provides each nested helper invocation its own options hash', () => { +> let lastOptions: Handlebars.HelperOptions; +> const helpers = { +> equal(x: any, y: any, options: Handlebars.HelperOptions) { +103c113 +< } +--- +> }, +105,107c115 +< expectTemplate('{{equal (equal true true) true}}') +< .withHelpers(helpers) +< .toCompileTo('true'); +--- +> expectTemplate('{{equal (equal true true) true}}').withHelpers(helpers).toCompileTo('true'); +110c118 +< it('with hashes', function() { +--- +> it('with hashes', () => { +114c122 +< blog: function(val) { +--- +> blog(val) { +117c125 +< equal: function(x, y) { +--- +> equal(x, y) { +119c127 +< } +--- +> }, +124c132 +< it('as hashes', function() { +--- +> it('as hashes', () => { +127c135 +< blog: function(options) { +--- +> blog(options) { +130c138 +< equal: function(x, y) { +--- +> equal(x, y) { +132c140 +< } +--- +> }, +137,140c145,146 +< it('multiple subexpressions in a hash', function() { +< expectTemplate( +< '{{input aria-label=(t "Name") placeholder=(t "Example User")}}' +< ) +--- +> it('multiple subexpressions in a hash', () => { +> expectTemplate('{{input aria-label=(t "Name") placeholder=(t "Example User")}}') +142,145c148,151 +< input: function(options) { +< var hash = options.hash; +< var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']); +< var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder); +--- +> input(options) { +> const hash = options.hash; +> const ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']); +> const placeholder = Handlebars.Utils.escapeExpression(hash.placeholder); +147,151c153 +< '' +--- +> '' +154c156 +< t: function(defaultString) { +--- +> t(defaultString) { +156c158 +< } +--- +> }, +161,164c163,164 +< it('multiple subexpressions in a hash with context', function() { +< expectTemplate( +< '{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}' +< ) +--- +> it('multiple subexpressions in a hash with context', () => { +> expectTemplate('{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}') +168,169c168,169 +< placeholder: 'Example User' +< } +--- +> placeholder: 'Example User', +> }, +172,175c172,175 +< input: function(options) { +< var hash = options.hash; +< var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']); +< var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder); +--- +> input(options) { +> const hash = options.hash; +> const ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']); +> const placeholder = Handlebars.Utils.escapeExpression(hash.placeholder); +177,181c177 +< '' +--- +> '' +184c180 +< t: function(defaultString) { +--- +> t(defaultString) { +186,212d181 +< } +< }) +< .toCompileTo(''); +< }); +< +< it('in string params mode,', function() { +< expectTemplate('{{snog (blorg foo x=y) yeah a=b}}') +< .withCompileOptions({ stringParams: true }) +< .withHelpers({ +< snog: function(a, b, options) { +< equals(a, 'foo'); +< equals( +< options.types.length, +< 2, +< 'string params for outer helper processed correctly' +< ); +< equals( +< options.types[0], +< 'SubExpression', +< 'string params for outer helper processed correctly' +< ); +< equals( +< options.types[1], +< 'PathExpression', +< 'string params for outer helper processed correctly' +< ); +< return a + b; +214,231d182 +< +< blorg: function(a, options) { +< equals( +< options.types.length, +< 1, +< 'string params for inner helper processed correctly' +< ); +< equals( +< options.types[0], +< 'PathExpression', +< 'string params for inner helper processed correctly' +< ); +< return a; +< } +< }) +< .withInput({ +< foo: {}, +< yeah: {} +233,248c184 +< .toCompileTo('fooyeah'); +< }); +< +< it('as hashes in string params mode', function() { +< expectTemplate('{{blog fun=(bork)}}') +< .withCompileOptions({ stringParams: true }) +< .withHelpers({ +< blog: function(options) { +< equals(options.hashTypes.fun, 'SubExpression'); +< return 'val is ' + options.hash.fun; +< }, +< bork: function() { +< return 'BORK'; +< } +< }) +< .toCompileTo('val is BORK'); +--- +> .toCompileTo(''); +251c187 +< it('subexpression functions on the context', function() { +--- +> it('subexpression functions on the context', () => { +254c190 +< bar: function() { +--- +> bar() { +256c192 +< } +--- +> }, +259c195 +< foo: function(val) { +--- +> foo(val) { +261c197 +< } +--- +> }, +266c202 +< it("subexpressions can't just be property lookups", function() { +--- +> it("subexpressions can't just be property lookups", () => { +269c205 +< bar: 'LOL' +--- +> bar: 'LOL', +272c208 +< foo: function(val) { +--- +> foo(val) { +274c210 +< } +--- +> }, diff --git a/packages/kbn-handlebars/.patches/utils.patch b/packages/kbn-handlebars/.patches/utils.patch new file mode 100644 index 0000000000000..485d69652544c --- /dev/null +++ b/packages/kbn-handlebars/.patches/utils.patch @@ -0,0 +1,109 @@ +1,86c1,21 +< describe('utils', function() { +< describe('#SafeString', function() { +< it('constructing a safestring from a string and checking its type', function() { +< var safe = new Handlebars.SafeString('testing 1, 2, 3'); +< if (!(safe instanceof Handlebars.SafeString)) { +< throw new Error('Must be instance of SafeString'); +< } +< equals( +< safe.toString(), +< 'testing 1, 2, 3', +< 'SafeString is equivalent to its underlying string' +< ); +< }); +< +< it('it should not escape SafeString properties', function() { +< var name = new Handlebars.SafeString('Sean O'Malley'); +< +< expectTemplate('{{name}}') +< .withInput({ name: name }) +< .toCompileTo('Sean O'Malley'); +< }); +< }); +< +< describe('#escapeExpression', function() { +< it('shouhld escape html', function() { +< equals( +< Handlebars.Utils.escapeExpression('foo<&"\'>'), +< 'foo<&"'>' +< ); +< equals(Handlebars.Utils.escapeExpression('foo='), 'foo='); +< }); +< it('should not escape SafeString', function() { +< var string = new Handlebars.SafeString('foo<&"\'>'); +< equals(Handlebars.Utils.escapeExpression(string), 'foo<&"\'>'); +< +< var obj = { +< toHTML: function() { +< return 'foo<&"\'>'; +< } +< }; +< equals(Handlebars.Utils.escapeExpression(obj), 'foo<&"\'>'); +< }); +< it('should handle falsy', function() { +< equals(Handlebars.Utils.escapeExpression(''), ''); +< equals(Handlebars.Utils.escapeExpression(undefined), ''); +< equals(Handlebars.Utils.escapeExpression(null), ''); +< +< equals(Handlebars.Utils.escapeExpression(false), 'false'); +< equals(Handlebars.Utils.escapeExpression(0), '0'); +< }); +< it('should handle empty objects', function() { +< equals(Handlebars.Utils.escapeExpression({}), {}.toString()); +< equals(Handlebars.Utils.escapeExpression([]), [].toString()); +< }); +< }); +< +< describe('#isEmpty', function() { +< it('should not be empty', function() { +< equals(Handlebars.Utils.isEmpty(undefined), true); +< equals(Handlebars.Utils.isEmpty(null), true); +< equals(Handlebars.Utils.isEmpty(false), true); +< equals(Handlebars.Utils.isEmpty(''), true); +< equals(Handlebars.Utils.isEmpty([]), true); +< }); +< +< it('should be empty', function() { +< equals(Handlebars.Utils.isEmpty(0), false); +< equals(Handlebars.Utils.isEmpty([1]), false); +< equals(Handlebars.Utils.isEmpty('foo'), false); +< equals(Handlebars.Utils.isEmpty({ bar: 1 }), false); +< }); +< }); +< +< describe('#extend', function() { +< it('should ignore prototype values', function() { +< function A() { +< this.a = 1; +< } +< A.prototype.b = 4; +< +< var b = { b: 2 }; +< +< Handlebars.Utils.extend(b, new A()); +< +< equals(b.a, 1); +< equals(b.b, 2); +--- +> /* +> * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/kbn-handlebars/LICENSE` for more information. +> */ +> +> import Handlebars from '..'; +> import { expectTemplate } from '../__jest__/test_bench'; +> +> describe('utils', function () { +> describe('#SafeString', function () { +> it('constructing a safestring from a string and checking its type', function () { +> const safe = new Handlebars.SafeString('testing 1, 2, 3'); +> expect(safe).toBeInstanceOf(Handlebars.SafeString); +> expect(safe.toString()).toEqual('testing 1, 2, 3'); +> }); +> +> it('it should not escape SafeString properties', function () { +> const name = new Handlebars.SafeString('Sean O'Malley'); +> expectTemplate('{{name}}').withInput({ name }).toCompileTo('Sean O'Malley'); diff --git a/packages/kbn-handlebars/.patches/whitespace-control.patch b/packages/kbn-handlebars/.patches/whitespace-control.patch new file mode 100644 index 0000000000000..f9e32bc2260ca --- /dev/null +++ b/packages/kbn-handlebars/.patches/whitespace-control.patch @@ -0,0 +1,187 @@ +1,24c1,17 +< describe('whitespace control', function() { +< it('should strip whitespace around mustache calls', function() { +< var hash = { foo: 'bar<' }; +< +< expectTemplate(' {{~foo~}} ') +< .withInput(hash) +< .toCompileTo('bar<'); +< +< expectTemplate(' {{~foo}} ') +< .withInput(hash) +< .toCompileTo('bar< '); +< +< expectTemplate(' {{foo~}} ') +< .withInput(hash) +< .toCompileTo(' bar<'); +< +< expectTemplate(' {{~&foo~}} ') +< .withInput(hash) +< .toCompileTo('bar<'); +< +< expectTemplate(' {{~{foo}~}} ') +< .withInput(hash) +< .toCompileTo('bar<'); +< +--- +> /* +> * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/kbn-handlebars/LICENSE` for more information. +> */ +> +> import { expectTemplate } from '../__jest__/test_bench'; +> +> describe('whitespace control', () => { +> it('should strip whitespace around mustache calls', () => { +> const hash = { foo: 'bar<' }; +> expectTemplate(' {{~foo~}} ').withInput(hash).toCompileTo('bar<'); +> expectTemplate(' {{~foo}} ').withInput(hash).toCompileTo('bar< '); +> expectTemplate(' {{foo~}} ').withInput(hash).toCompileTo(' bar<'); +> expectTemplate(' {{~&foo~}} ').withInput(hash).toCompileTo('bar<'); +> expectTemplate(' {{~{foo}~}} ').withInput(hash).toCompileTo('bar<'); +28,46c21,28 +< describe('blocks', function() { +< it('should strip whitespace around simple block calls', function() { +< var hash = { foo: 'bar<' }; +< +< expectTemplate(' {{~#if foo~}} bar {{~/if~}} ') +< .withInput(hash) +< .toCompileTo('bar'); +< +< expectTemplate(' {{#if foo~}} bar {{/if~}} ') +< .withInput(hash) +< .toCompileTo(' bar '); +< +< expectTemplate(' {{~#if foo}} bar {{~/if}} ') +< .withInput(hash) +< .toCompileTo(' bar '); +< +< expectTemplate(' {{#if foo}} bar {{/if}} ') +< .withInput(hash) +< .toCompileTo(' bar '); +--- +> describe('blocks', () => { +> it('should strip whitespace around simple block calls', () => { +> const hash = { foo: 'bar<' }; +> +> expectTemplate(' {{~#if foo~}} bar {{~/if~}} ').withInput(hash).toCompileTo('bar'); +> expectTemplate(' {{#if foo~}} bar {{/if~}} ').withInput(hash).toCompileTo(' bar '); +> expectTemplate(' {{~#if foo}} bar {{~/if}} ').withInput(hash).toCompileTo(' bar '); +> expectTemplate(' {{#if foo}} bar {{/if}} ').withInput(hash).toCompileTo(' bar '); +57c39 +< it('should strip whitespace around inverse block calls', function() { +--- +> it('should strip whitespace around inverse block calls', () => { +59d40 +< +61d41 +< +63d42 +< +65,68c44 +< +< expectTemplate( +< ' \n\n{{~^if foo~}} \n\nbar \n\n{{~/if~}}\n\n ' +< ).toCompileTo('bar'); +--- +> expectTemplate(' \n\n{{~^if foo~}} \n\nbar \n\n{{~/if~}}\n\n ').toCompileTo('bar'); +71,80c47,48 +< it('should strip whitespace around complex block calls', function() { +< var hash = { foo: 'bar<' }; +< +< expectTemplate('{{#if foo~}} bar {{~^~}} baz {{~/if}}') +< .withInput(hash) +< .toCompileTo('bar'); +< +< expectTemplate('{{#if foo~}} bar {{^~}} baz {{/if}}') +< .withInput(hash) +< .toCompileTo('bar '); +--- +> it('should strip whitespace around complex block calls', () => { +> const hash = { foo: 'bar<' }; +82,84c50,54 +< expectTemplate('{{#if foo}} bar {{~^~}} baz {{~/if}}') +< .withInput(hash) +< .toCompileTo(' bar'); +--- +> expectTemplate('{{#if foo~}} bar {{~^~}} baz {{~/if}}').withInput(hash).toCompileTo('bar'); +> expectTemplate('{{#if foo~}} bar {{^~}} baz {{/if}}').withInput(hash).toCompileTo('bar '); +> expectTemplate('{{#if foo}} bar {{~^~}} baz {{~/if}}').withInput(hash).toCompileTo(' bar'); +> expectTemplate('{{#if foo}} bar {{^~}} baz {{/if}}').withInput(hash).toCompileTo(' bar '); +> expectTemplate('{{#if foo~}} bar {{~else~}} baz {{~/if}}').withInput(hash).toCompileTo('bar'); +86,90c56 +< expectTemplate('{{#if foo}} bar {{^~}} baz {{/if}}') +< .withInput(hash) +< .toCompileTo(' bar '); +< +< expectTemplate('{{#if foo~}} bar {{~else~}} baz {{~/if}}') +--- +> expectTemplate('\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n') +94,102c60 +< expectTemplate( +< '\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n' +< ) +< .withInput(hash) +< .toCompileTo('bar'); +< +< expectTemplate( +< '\n\n{{~#if foo~}} \n\n{{{foo}}} \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n' +< ) +--- +> expectTemplate('\n\n{{~#if foo~}} \n\n{{{foo}}} \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n') +106,109c64 +< expectTemplate('{{#if foo~}} bar {{~^~}} baz {{~/if}}').toCompileTo( +< 'baz' +< ); +< +--- +> expectTemplate('{{#if foo~}} bar {{~^~}} baz {{~/if}}').toCompileTo('baz'); +111,120c66,69 +< +< expectTemplate('{{#if foo~}} bar {{~^}} baz {{~/if}}').toCompileTo( +< ' baz' +< ); +< +< expectTemplate('{{#if foo~}} bar {{~^}} baz {{/if}}').toCompileTo( +< ' baz ' +< ); +< +< expectTemplate('{{#if foo~}} bar {{~else~}} baz {{~/if}}').toCompileTo( +--- +> expectTemplate('{{#if foo~}} bar {{~^}} baz {{~/if}}').toCompileTo(' baz'); +> expectTemplate('{{#if foo~}} bar {{~^}} baz {{/if}}').toCompileTo(' baz '); +> expectTemplate('{{#if foo~}} bar {{~else~}} baz {{~/if}}').toCompileTo('baz'); +> expectTemplate('\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n').toCompileTo( +123,126d71 +< +< expectTemplate( +< '\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n' +< ).toCompileTo('baz'); +130,152c75 +< it('should strip whitespace around partials', function() { +< expectTemplate('foo {{~> dude~}} ') +< .withPartials({ dude: 'bar' }) +< .toCompileTo('foobar'); +< +< expectTemplate('foo {{> dude~}} ') +< .withPartials({ dude: 'bar' }) +< .toCompileTo('foo bar'); +< +< expectTemplate('foo {{> dude}} ') +< .withPartials({ dude: 'bar' }) +< .toCompileTo('foo bar '); +< +< expectTemplate('foo\n {{~> dude}} ') +< .withPartials({ dude: 'bar' }) +< .toCompileTo('foobar'); +< +< expectTemplate('foo\n {{> dude}} ') +< .withPartials({ dude: 'bar' }) +< .toCompileTo('foo\n bar'); +< }); +< +< it('should only strip whitespace once', function() { +--- +> it('should only strip whitespace once', () => { diff --git a/packages/kbn-handlebars/BUILD.bazel b/packages/kbn-handlebars/BUILD.bazel new file mode 100644 index 0000000000000..362c153fb90af --- /dev/null +++ b/packages/kbn-handlebars/BUILD.bazel @@ -0,0 +1,115 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_BASE_NAME = "kbn-handlebars" +PKG_REQUIRE_NAME = "@kbn/handlebars" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__handlebars" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__jest__/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +RUNTIME_DEPS = [ + "@npm//handlebars", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/node", + "@npm//handlebars", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-handlebars/LICENSE b/packages/kbn-handlebars/LICENSE new file mode 100644 index 0000000000000..55b4f257a1e98 --- /dev/null +++ b/packages/kbn-handlebars/LICENSE @@ -0,0 +1,29 @@ +The MIT License (MIT) + +Copyright (c) Elasticsearch BV +Copyright (c) Copyright (C) 2011-2019 by Yehuda Katz + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at the following locations: + - https://github.com/handlebars-lang/handlebars.js + - https://github.com/elastic/kibana/tree/main/packages/kbn-handlebars + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/kbn-handlebars/README.md b/packages/kbn-handlebars/README.md new file mode 100644 index 0000000000000..e9a6a8c839fd9 --- /dev/null +++ b/packages/kbn-handlebars/README.md @@ -0,0 +1,192 @@ +# @kbn/handlebars + +A custom version of the handlebars package which, to improve security, does not use `eval` or `new Function`. This means that templates can't be compiled into JavaScript functions in advance and hence, rendering the templates is a lot slower. + +## Limitations + +- Only the following compile options are supported: + - `knownHelpers` + - `knownHelpersOnly` + - `strict` + - `assumeObjects` + - `noEscape` + - `data` + +- Only the following runtime options are supported: + - `helpers` + - `blockParams` + - `data` + +The [Inline partials](https://handlebarsjs.com/guide/partials.html#inline-partials) handlebars template feature is currently not supported by `@kbn/handlebars`. + +## Implementation differences + +The standard `handlebars` implementation: + +1. When given a template string, e.g. `Hello {{x}}`, return a "render" function which takes an "input" object, e.g. `{ x: 'World' }`. +1. The first time the "render" function is called the following happens: + 1. Turn the template string into an Abstract Syntax Tree (AST). + 1. Convert the AST into a hyper optimized JavaScript function which takes the input object as an argument. + 1. Call the generate JavaScript function with the given "input" object to produce and return the final output string (`Hello World`). +1. Subsequent calls to the "render" function will re-use the already generated JavaScript function. + +The custom `@kbn/handlebars` implementation: + +1. When given a template string, e.g. `Hello {{x}}`, return a "render" function which takes an "input" object, e.g. `{ x: 'World' }`. +1. The first time the "render" function is called the following happens: + 1. Turn the template string into an Abstract Syntax Tree (AST). + 1. Process the AST with the given "input" object to produce and return the final output string (`Hello World`). +1. Subsequent calls to the "render" function will re-use the already generated AST. + +_Note: Not parsing of the template string until the first call to the "render" function is deliberate as it mimics the original `handlebars` implementation. This means that any errors that occur due to an invalid template string will not be thrown until the first call to the "render" function._ + +## Technical details + +The `handlebars` library exposes the API for both [generating the AST](https://github.com/handlebars-lang/handlebars.js/blob/master/docs/compiler-api.md#ast) and walking it by implementing the [Visitor API](https://github.com/handlebars-lang/handlebars.js/blob/master/docs/compiler-api.md#ast-visitor). We can leverage that to our advantage and create our own "render" function, which internally calls this API to generate the AST and then the API to walk the AST. + +The `@kbn/handlebars` implementation of the `Visitor` class implements all the necessary methods called by the parent `Visitor` code when instructed to walk the AST. They all start with an upppercase letter, e.g. `MustacheStatement` or `SubExpression`. We call this class `ElasticHandlebarsVisitor`. + +To parse the template string to an AST representation, we call `Handlebars.parse(templateString)`, which returns an AST object. + +The AST object contains a bunch of nodes, one for each element of the template string, all arranged in a tree-like structure. The root of the AST object is a node of type `Program`. This is a special node, which we do not need to worry about, but each of its direct children has a type named like the method which will be called when the walking algorithm reaches that node, e.g. `ContentStatement` or `BlockStatement`. These are the methods that our `Visitor` implementation implements. + +To instruct our `ElasticHandlebarsVisitor` class to start walking the AST object, we call the `accept()` method inherited from the parent `Visitor` class with the main AST object. The `Visitor` will walk each node in turn that is directly attached to the root `Program` node. For each node it traverses, it will call the matching method in our `ElasticHandlebarsVisitor` class. + +To instruct the `Visitor` code to traverse any child nodes of a given node, our implementation needs to manually call `accept(childNode)`, `acceptArray(arrayOfChildNodes)`, `acceptKey(node, childKeyName)`, or `acceptRequired(node, childKeyName)` from within any of the "node" methods, otherwise the child nodes are ignored. + +### State + +We keep state internally in the `ElasticHandlebarsVisitor` object using the following private properties: + +- `scopes`: An array (stack) of `context` objects. In a simple template this array will always only contain a single element: The main `context` object. In more complicated scenarios, new `context` objects will be pushed and popped to and from the `scopes` stack as needed. +- `output`: An array containing the "rendered" output of each node (normally just one element per node). In the most simple template, this is simply joined together into a the final output string after the AST has been traversed. In more complicated templates, we use this array temporarily to collect parameters to give to helper functions (see the `getParams` function). + +## Development + +Some of the tests have been copied from the upstream `handlebars` project and modified to fit our use-case, test-suite, and coding conventions. They are all located under the `packages/kbn-handlebars/src/upstream` directory. To check if any of the copied files have received updates upstream that we might want to include in our copies, you can run the following script: + +```sh +./packages/kbn-handlebars/scripts/check_for_test_changes.sh +``` + +If the script outputs a diff for a given file, it means that this file has been updated. + +Once all updates have been manually merged with our versions of the files, run the following script to "lock" us into the new updates: + +```sh +./packages/kbn-handlebars/scripts/update_test_patches.sh +``` + +This will update the `.patch` files inside the `packages/kbn-handlebars/.patches` directory. Make sure to commit those changes. + +_Note: If we manually make changes to our test files in the `upstream` directory, we need to run the `update_test_patches.sh` script as well._ + +## Debugging + +### Print AST + +To output the generated AST object structure in a somewhat readable form, use the following script: + +```sh +./packages/kbn-handlebars/scripts/print_ast.js +``` + +Example: + +```sh +./packages/kbn-handlebars/scripts/print_ast.js '{{value}}' +``` + +Output: + +```js +{ + type: 'Program', + body: [ + { + type: 'MustacheStatement', + path: { + type: 'PathExpression', + data: false, + depth: 0, + parts: [ 'value' ], + original: 'value' + }, + params: [], + hash: undefined, + escaped: true, + strip: { open: false, close: false } + } + ], + strip: {} +} +``` + +You can also filter which properties not to display, e.g: + +```sh +./packages/kbn-handlebars/scripts/print_ast.js '{{#myBlock}}Hello {{name}}{{/myBlock}}' params,hash,loc,strip,data,depth,parts,inverse,openStrip,inverseStrip,closeStrip,blockParams,escaped +``` + +Output: + +```js +{ + type: 'Program', + body: [ + { + type: 'BlockStatement', + path: { type: 'PathExpression', original: 'myBlock' }, + program: { + type: 'Program', + body: [ + { + type: 'ContentStatement', + original: 'Hello ', + value: 'Hello ' + }, + { + type: 'MustacheStatement', + path: { type: 'PathExpression', original: 'name' } + } + ] + } + } + ] +} +``` + +### Environment variables + +By default each test will run both the original `handlebars` code and the modified `@kbn/handlebars` code to compare if the output of the two are identical. When debugging, it can be beneficial to isolate a test run to just one or the other. To control this, you can use the following environment variables: + +- `EVAL=1` - Set to only run the original `handlebars` implementation that uses `eval`. +- `AST=1` - Set to only run the modified `@kbn/handlebars` implementation that doesn't use `eval`. + +### Print generated code + +It's possible to see the generated JavaScript code that `handlebars` create for a given template using the following command line tool: + +```sh +./node_modules/handlebars/print-script