From aad9dafb3d8f56706fba377f0b4c693695ba5f63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Tue, 5 May 2020 08:02:21 +0200 Subject: [PATCH 01/75] [ML] Amends New job from index pattern UI text (#64667) Co-authored-by: Lisa Cawley --- .../components/dedicated_index/description.tsx | 2 +- .../recognize/components/job_settings_form.tsx | 6 +++--- .../ml/public/application/routing/breadcrumbs.ts | 7 +++++++ .../routing/routes/new_job/recognize.tsx | 9 +++++++-- .../application/routing/routes/new_job/wizard.tsx | 15 ++++++--------- .../plugins/translations/translations/ja-JP.json | 1 - .../plugins/translations/translations/zh-CN.json | 1 - 7 files changed, 24 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx index 2a898843dda57..8b9dbe23f0920 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx @@ -22,7 +22,7 @@ export const Description: FC = memo(({ children }) => { description={ } > diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx index 377ec84623480..bae9c592b94c4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx @@ -109,7 +109,7 @@ export const JobSettingsForm: FC = ({ description={ } > @@ -230,7 +230,7 @@ export const JobSettingsForm: FC = ({ description={ } > @@ -263,7 +263,7 @@ export const JobSettingsForm: FC = ({ onSubmit(formState); }} aria-label={i18n.translate('xpack.ml.newJob.recognize.createJobButtonAriaLabel', { - defaultMessage: 'Create Job', + defaultMessage: 'Create job', })} > {saveState === SAVE_STATE.NOT_SAVED && ( diff --git a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts index 74dbe055fead3..588f4423c3190 100644 --- a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts +++ b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts @@ -34,3 +34,10 @@ export const DATA_VISUALIZER_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ }), href: '#/datavisualizer', }); + +export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ + text: i18n.translate('xpack.ml.createJobsBreadcrumbLabel', { + defaultMessage: 'Create jobs', + }), + href: '#/jobs/new_job', +}); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx index 12687fd71edc5..2cd40cbcd95e6 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -13,14 +13,19 @@ import { basicResolvers } from '../../resolvers'; import { Page } from '../../../jobs/new_job/recognize'; import { checkViewOrCreateJobs } from '../../../jobs/new_job/recognize/resolvers'; import { mlJobService } from '../../../services/job_service'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; +import { + ANOMALY_DETECTION_BREADCRUMB, + CREATE_JOB_BREADCRUMB, + ML_BREADCRUMB, +} from '../../breadcrumbs'; const breadcrumbs = [ ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, + CREATE_JOB_BREADCRUMB, { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { - defaultMessage: 'Select index or search', + defaultMessage: 'Recognized index', }), href: '', }, diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 1c91d7e94b241..619a27013b6dd 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -16,20 +16,17 @@ import { JOB_TYPE } from '../../../../../common/constants/new_job'; import { mlJobService } from '../../../services/job_service'; import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; +import { + ANOMALY_DETECTION_BREADCRUMB, + CREATE_JOB_BREADCRUMB, + ML_BREADCRUMB, +} from '../../breadcrumbs'; interface WizardPageProps extends PageProps { jobType: JOB_TYPE; } -const createJobBreadcrumbs = { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.createJobLabel', { - defaultMessage: 'Create job', - }), - href: '#/jobs/new_job', -}; - -const baseBreadcrumbs = [ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, createJobBreadcrumbs]; +const baseBreadcrumbs = [ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, CREATE_JOB_BREADCRUMB]; const singleMetricBreadcrumbs = [ ...baseBreadcrumbs, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8b05f973c74eb..e316ffbd3eccb 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9781,7 +9781,6 @@ "xpack.ml.jobMessages.timeLabel": "時間", "xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel": "高度な構成", "xpack.ml.jobsBreadcrumbs.categorizationLabel": "カテゴリー分け", - "xpack.ml.jobsBreadcrumbs.createJobLabel": "ジョブを作成", "xpack.ml.jobsBreadcrumbs.jobWizardLabel": "ジョブを作成", "xpack.ml.jobsBreadcrumbs.multiMetricLabel": "マルチメトリック", "xpack.ml.jobsBreadcrumbs.populationLabel": "集団", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 90794f7512fe6..09fdd46d95a04 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9787,7 +9787,6 @@ "xpack.ml.jobMessages.timeLabel": "时间", "xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel": "高级配置", "xpack.ml.jobsBreadcrumbs.categorizationLabel": "归类", - "xpack.ml.jobsBreadcrumbs.createJobLabel": "创建作业", "xpack.ml.jobsBreadcrumbs.jobWizardLabel": "创建作业", "xpack.ml.jobsBreadcrumbs.multiMetricLabel": "多指标", "xpack.ml.jobsBreadcrumbs.populationLabel": "填充", From cf64454574e675ef9c32ba82c55da295528dadc5 Mon Sep 17 00:00:00 2001 From: Andrea Del Rio Date: Mon, 4 May 2020 23:03:24 -0700 Subject: [PATCH 02/75] [Alerting] Alert Details and Alert List design improvements (#64839) * added labels * cleaned up flex elements in alert details view * replace eye icon with mute switch * removed Actions columns from Connectors list * fix keypadmenuitems layout in action selection section * fixed error * more cleanup * added back Actions column for Connectors List but with new name * fix some tests * fix another test * removed unneeded lodash from test file * fix test in details.ts * removed unused translations * removed Alerts (Actions) column from Connectors table" * removed translation * removed EuiBadge from connectors list * updated test in connectors.ts * fix function tests * fix more function tests Co-authored-by: Elastic Machine Co-authored-by: Patrick Mueller --- .../translations/translations/ja-JP.json | 6 -- .../translations/translations/zh-CN.json | 6 -- .../action_connector_form/action_form.tsx | 6 +- .../components/actions_connectors_list.tsx | 18 ---- .../components/alert_details.test.tsx | 26 +++--- .../components/alert_details.tsx | 91 ++++++++++++------- .../components/alert_instances.tsx | 39 ++------ .../alerts_list/components/alerts_list.tsx | 18 ++++ .../apps/triggers_actions_ui/connectors.ts | 2 - .../apps/triggers_actions_ui/details.ts | 35 ++++--- .../page_objects/alert_details.ts | 9 +- .../page_objects/triggers_actions_ui_page.ts | 4 - 12 files changed, 118 insertions(+), 142 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e316ffbd3eccb..8023bec0a225b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15720,7 +15720,6 @@ "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionName": "削除", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actionTypeTitle": "タイプ", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.nameTitle": "名前", - "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.referencedByCountTitle": "アクション", "xpack.triggersActionsUI.sections.actionsConnectorsList.filters.actionTypeIdName": "タイプ", "xpack.triggersActionsUI.sections.actionsConnectorsList.multipleTitle": "コネクター", "xpack.triggersActionsUI.sections.actionsConnectorsList.noPermissionToCreateTitle": "コネクターを作成するパーミッションがありません。", @@ -15775,10 +15774,6 @@ "xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.noDataTitle": "このクエリに一致するデータはありません", "xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage": "ビジュアライゼーションを読み込めません", "xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledAlert": "このアラートは無効になっていて再表示できません。[↑ を有効にする]を切り替えてアクティブにします。", - "xpack.triggersActionsUI.sections.alertDetails.alertInstances.mutedAlert": "ミュート", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.actions.mute": "ミュート", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.actions.unmute": "ミュート解除", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.actions": "アクション", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration": "期間", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.instance": "インスタンス", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.start": "開始", @@ -15786,7 +15781,6 @@ "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active": "アクティブ", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "非アクティブ", "xpack.triggersActionsUI.sections.alertDetails.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", - "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableTitle": "有効にする", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle": "ミュート", "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage": "アラートを読み込めません: {message}", "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertStateMessage": "アラートステートを読み込めません: {message}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 09fdd46d95a04..25e588c79ede6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15728,7 +15728,6 @@ "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionName": "删除", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actionTypeTitle": "类型", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.nameTitle": "名称", - "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.referencedByCountTitle": "操作", "xpack.triggersActionsUI.sections.actionsConnectorsList.filters.actionTypeIdName": "类型", "xpack.triggersActionsUI.sections.actionsConnectorsList.multipleTitle": "连接器", "xpack.triggersActionsUI.sections.actionsConnectorsList.noPermissionToCreateTitle": "无权创建连接器", @@ -15783,10 +15782,6 @@ "xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.noDataTitle": "没有数据匹配此查询", "xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage": "无法加载可视化", "xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledAlert": "此告警已禁用,无法显示。切换启用 ↑ 以激活。", - "xpack.triggersActionsUI.sections.alertDetails.alertInstances.mutedAlert": "已静音", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.actions.mute": "静音", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.actions.unmute": "取消静音", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.actions": "操作", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration": "持续时间", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.instance": "实例", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.start": "启动", @@ -15794,7 +15789,6 @@ "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active": "活动", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "非活动", "xpack.triggersActionsUI.sections.alertDetails.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", - "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableTitle": "启用", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle": "静音", "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage": "无法加载告警:{message}", "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertStateMessage": "无法加载告警状态:{message}", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 531e9e1926ff4..977881604636a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -556,14 +556,14 @@ export const ActionForm = ({ ); return ( - + {checkEnabledResult.isEnabled && keyPadItem} {checkEnabledResult.isEnabled === false && ( {keyPadItem} )} - + ); }); } @@ -656,7 +656,7 @@ export const ActionForm = ({ )} - + {isLoadingActionTypes ? ( { sortable: false, truncateText: true, }, - { - field: 'referencedByCount', - 'data-test-subj': 'connectorsTableCell-referencedByCount', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.referencedByCountTitle', - { defaultMessage: 'Actions' } - ), - sortable: false, - truncateText: true, - render: (value: number, item: ActionConnectorTableItem) => { - return ( - - {value} - - ); - }, - }, { field: 'isPreconfigured', name: '', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 230b896eeca7d..4132ed6ed2a80 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -16,7 +16,6 @@ import { EuiBetaBadge, EuiButtonEmpty, } from '@elastic/eui'; -import { times, random } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ViewInApp } from './view_in_app'; import { PLUGIN } from '../../../constants/plugin'; @@ -175,7 +174,6 @@ describe('alert_details', () => { }); it('renders a counter for multiple alert action', () => { - const actionCount = random(1, 10); const alert = mockAlert({ actions: [ { @@ -184,12 +182,12 @@ describe('alert_details', () => { params: {}, actionTypeId: '.server-log', }, - ...times(actionCount, () => ({ + { group: 'default', id: uuid.v4(), params: {}, actionTypeId: '.email', - })), + }, ], }); const alertType = { @@ -238,7 +236,7 @@ describe('alert_details', () => { expect( details.containsMatchingElement( - {`+${actionCount}`} + {actionTypes[1].name} ) ).toBeTruthy(); @@ -288,8 +286,8 @@ describe('alert_details', () => { }); }); -describe('enable button', () => { - it('should render an enable button when alert is enabled', () => { +describe('disable button', () => { + it('should render a disable button when alert is enabled', () => { const alert = mockAlert({ enabled: true, }); @@ -306,16 +304,16 @@ describe('enable button', () => { ) .find(EuiSwitch) - .find('[name="enable"]') + .find('[name="disable"]') .first(); expect(enableButton.props()).toMatchObject({ - checked: true, + checked: false, disabled: false, }); }); - it('should render an enable button when alert is disabled', () => { + it('should render a disable button when alert is disabled', () => { const alert = mockAlert({ enabled: false, }); @@ -332,11 +330,11 @@ describe('enable button', () => { ) .find(EuiSwitch) - .find('[name="enable"]') + .find('[name="disable"]') .first(); expect(enableButton.props()).toMatchObject({ - checked: false, + checked: true, disabled: false, }); }); @@ -365,7 +363,7 @@ describe('enable button', () => { /> ) .find(EuiSwitch) - .find('[name="enable"]') + .find('[name="disable"]') .first(); enableButton.simulate('click'); @@ -400,7 +398,7 @@ describe('enable button', () => { /> ) .find(EuiSwitch) - .find('[name="enable"]') + .find('[name="disable"]') .first(); enableButton.simulate('click'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 318dd28d92da1..c6038aad3807b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -13,6 +13,7 @@ import { EuiPageContentHeader, EuiPageContentHeaderSection, EuiTitle, + EuiText, EuiFlexGroup, EuiFlexItem, EuiBadge, @@ -73,8 +74,9 @@ export const AlertDetails: React.FunctionComponent = ({ const canSave = hasSaveAlertsCapability(capabilities); const actionTypesByTypeId = indexBy(actionTypes, 'id'); - const [firstAction, ...otherActions] = alert.actions; + const alertActions = alert.actions; + const uniqueActions = Array.from(new Set(alertActions.map((item: any) => item.actionTypeId))); const [isEnabled, setIsEnabled] = useState(alert.enabled); const [isMuted, setIsMuted] = useState(alert.muteAll); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); @@ -156,33 +158,50 @@ export const AlertDetails: React.FunctionComponent = ({ - - - {alertType.name} - - {firstAction && ( - - - {actionTypesByTypeId[firstAction.actionTypeId].name ?? - firstAction.actionTypeId} - - - )} - {otherActions.length ? ( - - +{otherActions.length} - - ) : null} - + +

+ +

+
+ + {alertType.name}
- - + + {uniqueActions && uniqueActions.length ? ( + + +

+ +

+
+ + + {uniqueActions.map((action, index) => ( + + + {actionTypesByTypeId[action].name ?? action} + + + ))} + +
+ ) : null} +
+ + + { if (isEnabled) { setIsEnabled(false); @@ -195,8 +214,8 @@ export const AlertDetails: React.FunctionComponent = ({ }} label={ } /> @@ -229,19 +248,21 @@ export const AlertDetails: React.FunctionComponent = ({ - {alert.enabled ? ( ) : ( - -

- -

-
+ + + +

+ +

+
+
)}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index fa4d8f66cd7bf..16c6fb092f2e5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -7,11 +7,10 @@ import React, { Fragment, useState } from 'react'; import moment, { Duration } from 'moment'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable, EuiButtonToggle, EuiBadge, EuiHealth } from '@elastic/eui'; +import { EuiBasicTable, EuiHealth, EuiSpacer, EuiSwitch } from '@elastic/eui'; // @ts-ignore import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services'; import { padLeft, difference, chunk } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n/react'; import { Alert, AlertTaskState, RawAlertInstance, Pagination } from '../../../../types'; import { ComponentOpts as AlertApis, @@ -80,40 +79,19 @@ export const alertInstancesTableColumns = ( field: '', align: RIGHT_ALIGNMENT, name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.actions', - { defaultMessage: 'Actions' } + 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.mute', + { defaultMessage: 'Mute' } ), render: (alertInstance: AlertInstanceListItem) => { return ( - {alertInstance.isMuted ? ( - - - - ) : ( - - )} - onMuteAction(alertInstance)} - isSelected={alertInstance.isMuted} - isEmpty - isIconOnly /> ); @@ -161,6 +139,7 @@ export function AlertInstances({ return ( + { sortable: false, 'data-test-subj': 'alertsTableCell-tagsText', }, + { + field: 'actionsText', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsText', + { defaultMessage: 'Actions' } + ), + render: (count: number, item: AlertTableItem) => { + return ( + + {count} + + ); + }, + sortable: false, + 'data-test-subj': 'alertsTableCell-actionsText', + }, { field: 'alertType', name: i18n.translate( @@ -437,6 +454,7 @@ function filterAlertsById(alerts: Alert[], ids: string[]): Alert[] { function convertAlertsToTableItems(alerts: Alert[], alertTypesIndex: AlertTypeIndex) { return alerts.map(alert => ({ ...alert, + actionsText: alert.actions.length, tagsText: alert.tags.join(', '), alertType: alertTypesIndex[alert.alertTypeId]?.name ?? alert.alertTypeId, })); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index 1facc05bc186d..88d52578f692e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -56,7 +56,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { { name: connectorName, actionType: 'Slack', - referencedByCount: '0', }, ]); }); @@ -100,7 +99,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { { name: updatedConnectorName, actionType: 'Slack', - referencedByCount: '0', }, ]); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 7970c9b24427e..9d547876d9b3b 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -75,29 +75,28 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const alertType = await pageObjects.alertDetailsUI.getAlertType(); expect(alertType).to.be(`Always Firing`); - const { actionType, actionCount } = await pageObjects.alertDetailsUI.getActionsLabels(); + const { actionType } = await pageObjects.alertDetailsUI.getActionsLabels(); expect(actionType).to.be(`Slack`); - expect(actionCount).to.be(`+1`); }); it('should disable the alert', async () => { - const enableSwitch = await testSubjects.find('enableSwitch'); + const disableSwitch = await testSubjects.find('disableSwitch'); - const isChecked = await enableSwitch.getAttribute('aria-checked'); - expect(isChecked).to.eql('true'); + const isChecked = await disableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); - await enableSwitch.click(); + await disableSwitch.click(); - const enabledSwitchAfterDisabling = await testSubjects.find('enableSwitch'); - const isCheckedAfterDisabling = await enabledSwitchAfterDisabling.getAttribute( + const disableSwitchAfterDisabling = await testSubjects.find('disableSwitch'); + const isCheckedAfterDisabling = await disableSwitchAfterDisabling.getAttribute( 'aria-checked' ); - expect(isCheckedAfterDisabling).to.eql('false'); + expect(isCheckedAfterDisabling).to.eql('true'); }); it('shouldnt allow you to mute a disabled alert', async () => { - const disabledEnableSwitch = await testSubjects.find('enableSwitch'); - expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false'); + const disabledDisableSwitch = await testSubjects.find('disableSwitch'); + expect(await disabledDisableSwitch.getAttribute('aria-checked')).to.eql('true'); const muteSwitch = await testSubjects.find('muteSwitch'); expect(await muteSwitch.getAttribute('aria-checked')).to.eql('false'); @@ -112,18 +111,18 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should reenable a disabled the alert', async () => { - const enableSwitch = await testSubjects.find('enableSwitch'); + const disableSwitch = await testSubjects.find('disableSwitch'); - const isChecked = await enableSwitch.getAttribute('aria-checked'); - expect(isChecked).to.eql('false'); + const isChecked = await disableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); - await enableSwitch.click(); + await disableSwitch.click(); - const enabledSwitchAfterReenabling = await testSubjects.find('enableSwitch'); - const isCheckedAfterDisabling = await enabledSwitchAfterReenabling.getAttribute( + const disableSwitchAfterReenabling = await testSubjects.find('disableSwitch'); + const isCheckedAfterDisabling = await disableSwitchAfterReenabling.getAttribute( 'aria-checked' ); - expect(isCheckedAfterDisabling).to.eql('true'); + expect(isCheckedAfterDisabling).to.eql('false'); }); it('should mute the alert', async () => { diff --git a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts index 48ad59586f793..bd82443068765 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts @@ -23,7 +23,6 @@ export function AlertDetailsPageProvider({ getService }: FtrProviderContext) { async getActionsLabels() { return { actionType: await testSubjects.getVisibleText('actionTypeLabel'), - actionCount: await testSubjects.getVisibleText('actionCountLabel'), }; }, async getAlertInstancesList() { @@ -71,12 +70,10 @@ export function AlertDetailsPageProvider({ getService }: FtrProviderContext) { const muteAlertInstanceButton = await testSubjects.find( `muteAlertInstanceButton_${instance}` ); - log.debug(`checked:${await muteAlertInstanceButton.getAttribute('checked')}`); - expect(await muteAlertInstanceButton.getAttribute('checked')).to.eql( - isMuted ? 'true' : null + log.debug(`checked:${await muteAlertInstanceButton.getAttribute('aria-checked')}`); + expect(await muteAlertInstanceButton.getAttribute('aria-checked')).to.eql( + isMuted ? 'true' : 'false' ); - - expect(await testSubjects.exists(`mutedAlertInstanceLabel_${instance}`)).to.eql(isMuted); }); }, async ensureAlertInstanceExistance(instance: string, shouldExist: boolean) { diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index 2a50c0117eae9..ca7f064e20690 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -69,10 +69,6 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) .findTestSubject('connectorsTableCell-actionType') .find('.euiTableCellContent') .text(), - referencedByCount: $(row) - .findTestSubject('connectorsTableCell-referencedByCount') - .find('.euiTableCellContent') - .text(), }; }); }, From d77d2e4b777dae9a99a9e4be375a5935650677a1 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 5 May 2020 08:35:05 +0200 Subject: [PATCH 03/75] Remove legacy "interpreter" plugin (#61263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 🤖 delete legacy interpreter registries * chore: 🤖 completely delete legacy "interpreter" plugin * chore: 🤖 remove dependencies on legacy "interpreter" plugin * chore: 🤖 revert back Canvas global registries * test: 💍 fix Lens test Co-authored-by: Elastic Machine --- src/legacy/core_plugins/interpreter/README.md | 2 - src/legacy/core_plugins/interpreter/index.ts | 44 ---------------- src/legacy/core_plugins/interpreter/init.ts | 52 ------------------- .../core_plugins/interpreter/package.json | 4 -- .../load_legacy_server_function_wrappers.ts | 33 ------------ .../interpreter/public/interpreter.ts | 49 ----------------- .../public/registries.karma_mock.ts | 44 ---------------- .../interpreter/public/registries.ts | 29 ----------- .../canvas/.storybook/storyshots.test.js | 2 - x-pack/legacy/plugins/canvas/README.md | 4 +- x-pack/legacy/plugins/canvas/index.js | 2 +- 11 files changed, 3 insertions(+), 262 deletions(-) delete mode 100644 src/legacy/core_plugins/interpreter/README.md delete mode 100644 src/legacy/core_plugins/interpreter/index.ts delete mode 100644 src/legacy/core_plugins/interpreter/init.ts delete mode 100644 src/legacy/core_plugins/interpreter/package.json delete mode 100644 src/legacy/core_plugins/interpreter/public/canvas/load_legacy_server_function_wrappers.ts delete mode 100644 src/legacy/core_plugins/interpreter/public/interpreter.ts delete mode 100644 src/legacy/core_plugins/interpreter/public/registries.karma_mock.ts delete mode 100644 src/legacy/core_plugins/interpreter/public/registries.ts diff --git a/src/legacy/core_plugins/interpreter/README.md b/src/legacy/core_plugins/interpreter/README.md deleted file mode 100644 index 6d90ce2d5e2eb..0000000000000 --- a/src/legacy/core_plugins/interpreter/README.md +++ /dev/null @@ -1,2 +0,0 @@ -Interpreter legacy plugin has been migrated to the New Platform. Use -`expressions` New Platform plugin instead. diff --git a/src/legacy/core_plugins/interpreter/index.ts b/src/legacy/core_plugins/interpreter/index.ts deleted file mode 100644 index 9427a2f8a2d0f..0000000000000 --- a/src/legacy/core_plugins/interpreter/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; -import { Legacy } from '../../../../kibana'; -import { init } from './init'; - -// eslint-disable-next-line -export default function InterpreterPlugin(kibana: any) { - const config: Legacy.PluginSpecOptions = { - id: 'interpreter', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - injectDefaultVars: server => ({ - serverBasePath: server.config().get('server.basePath'), - }), - }, - config: (Joi: any) => { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - init, - }; - - return new kibana.Plugin(config); -} diff --git a/src/legacy/core_plugins/interpreter/init.ts b/src/legacy/core_plugins/interpreter/init.ts deleted file mode 100644 index 46da1539afadb..0000000000000 --- a/src/legacy/core_plugins/interpreter/init.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* eslint-disable max-classes-per-file */ - -// @ts-ignore -import { register, registryFactory, Registry, Fn } from '@kbn/interpreter/common'; - -import { Legacy } from '../../../../kibana'; - -export async function init(server: Legacy.Server /* options */) { - server.injectUiAppVars('canvas', () => { - const config = server.config(); - const basePath = config.get('server.basePath'); - const reportingBrowserType = (() => { - const configKey = 'xpack.reporting.capture.browser.type'; - if (!config.has(configKey)) { - return null; - } - return config.get(configKey); - })(); - - return { - kbnIndex: config.get('kibana.index'), - serverFunctions: (server.newPlatform.setup.plugins.expressions as any).__LEGACY - .registries() - .serverFunctions.toArray(), - basePath, - reportingBrowserType, - }; - }); - - // Expose server.plugins.interpreter.register(specs) and - // server.plugins.interpreter.registries() (a getter). - server.expose((server.newPlatform.setup.plugins.expressions as any).__LEGACY); -} diff --git a/src/legacy/core_plugins/interpreter/package.json b/src/legacy/core_plugins/interpreter/package.json deleted file mode 100644 index 3265dadd7fbfc..0000000000000 --- a/src/legacy/core_plugins/interpreter/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "interpreter", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/interpreter/public/canvas/load_legacy_server_function_wrappers.ts b/src/legacy/core_plugins/interpreter/public/canvas/load_legacy_server_function_wrappers.ts deleted file mode 100644 index fed157846a1a1..0000000000000 --- a/src/legacy/core_plugins/interpreter/public/canvas/load_legacy_server_function_wrappers.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * This file needs to be deleted by 8.0 release. It is here to load available - * server side functions and create a wrappers around them on client side, to - * execute them from client side. This functionality is used only by Canvas - * and all server side functions are in Canvas plugin. - * - * In 8.0 there will be no server-side functions, plugins will register only - * client side functions and if they need those to execute something on the - * server side, it should be respective function's internal implementation detail. - */ - -import { npSetup } from 'ui/new_platform'; - -export const { loadLegacyServerFunctionWrappers } = npSetup.plugins.expressions.__LEGACY; diff --git a/src/legacy/core_plugins/interpreter/public/interpreter.ts b/src/legacy/core_plugins/interpreter/public/interpreter.ts deleted file mode 100644 index 319a2779010c3..0000000000000 --- a/src/legacy/core_plugins/interpreter/public/interpreter.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import 'uiExports/interpreter'; -// @ts-ignore -import { register, registryFactory } from '@kbn/interpreter/common'; -import { npSetup } from 'ui/new_platform'; -import { registries } from './registries'; -import { Executor, ExpressionExecutor } from '../../../../plugins/expressions/public'; - -// Expose kbnInterpreter.register(specs) and kbnInterpreter.registries() globally so that plugins -// can register without a transpile step. -// TODO: This will be left behind in then legacy platform? -(global as any).kbnInterpreter = Object.assign( - (global as any).kbnInterpreter || {}, - registryFactory(registries) -); - -// TODO: This function will be left behind in the legacy platform. -let executorPromise: Promise | undefined; -export const getInterpreter = async () => { - if (!executorPromise) { - const executor = npSetup.plugins.expressions.__LEGACY.getExecutor(); - executorPromise = Promise.resolve(executor); - } - return await executorPromise; -}; - -// TODO: This function will be left behind in the legacy platform. -export const interpretAst: Executor['run'] = async (ast, context, handlers) => { - const { interpreter } = await getInterpreter(); - return await interpreter.interpretAst(ast, context, handlers); -}; diff --git a/src/legacy/core_plugins/interpreter/public/registries.karma_mock.ts b/src/legacy/core_plugins/interpreter/public/registries.karma_mock.ts deleted file mode 100644 index 0f37f33cc1b13..0000000000000 --- a/src/legacy/core_plugins/interpreter/public/registries.karma_mock.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; - -export const functionsRegistry = {}; -export const renderersRegistry = {}; -export const typesRegistry = {}; -export const registries = { - browserFunctions: functionsRegistry, - renderers: renderersRegistry, - types: typesRegistry, - loadLegacyServerFunctionWrappers: () => Promise.resolve(), -}; - -const resetRegistry = (registry: any) => { - registry.wrapper = sinon.stub(); - registry.register = sinon.stub(); - registry.toJS = sinon.stub(); - registry.toArray = sinon.stub(); - registry.get = sinon.stub(); - registry.getProp = sinon.stub(); - registry.reset = sinon.stub(); -}; -const resetAll = () => Object.values(registries).forEach(resetRegistry); - -resetAll(); -afterEach(resetAll); diff --git a/src/legacy/core_plugins/interpreter/public/registries.ts b/src/legacy/core_plugins/interpreter/public/registries.ts deleted file mode 100644 index 63fd9089acf4a..0000000000000 --- a/src/legacy/core_plugins/interpreter/public/registries.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { npSetup } from 'ui/new_platform'; - -export const functionsRegistry = npSetup.plugins.expressions.__LEGACY.functions; -export const renderersRegistry = npSetup.plugins.expressions.__LEGACY.renderers; -export const typesRegistry = npSetup.plugins.expressions.__LEGACY.types; -export const registries = { - browserFunctions: functionsRegistry, - renderers: renderersRegistry, - types: typesRegistry, -}; diff --git a/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js b/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js index a81483d1e7a17..a679010c67092 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js +++ b/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js @@ -77,8 +77,6 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { }; }); -jest.mock('plugins/interpreter/registries', () => ({})); - // Disabling this test due to https://github.com/elastic/eui/issues/2242 jest.mock( '../public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories', diff --git a/x-pack/legacy/plugins/canvas/README.md b/x-pack/legacy/plugins/canvas/README.md index 8e91161c635c2..fbcd674f72181 100644 --- a/x-pack/legacy/plugins/canvas/README.md +++ b/x-pack/legacy/plugins/canvas/README.md @@ -48,7 +48,7 @@ Open your plugin's `index.js` file, and modify it to look something like this (b export default function (kibana) { return new kibana.Plugin({ // Tell Kibana that this plugin needs canvas and the Kibana interpreter - require: ['interpreter', 'canvas'], + require: ['canvas'], // The name of your plugin. Make this whatever you want. name: 'canvas_example', @@ -132,7 +132,7 @@ In your plugin's root `index.js` file, modify the `kibana.Plugin` definition to export default function (kibana) { return new kibana.Plugin({ // Tell Kibana that this plugin needs canvas and the Kibana interpreter - require: ['interpreter', 'canvas'], + require: ['canvas'], // The name of your plugin. Make this whatever you want. name: 'canvas_example', diff --git a/x-pack/legacy/plugins/canvas/index.js b/x-pack/legacy/plugins/canvas/index.js index b62d88c930d91..4c7825e5b58aa 100644 --- a/x-pack/legacy/plugins/canvas/index.js +++ b/x-pack/legacy/plugins/canvas/index.js @@ -12,7 +12,7 @@ export function canvas(kibana) { return new kibana.Plugin({ id: CANVAS_APP, configPrefix: 'xpack.canvas', - require: ['kibana', 'elasticsearch', 'xpack_main', 'interpreter'], + require: ['kibana', 'elasticsearch', 'xpack_main'], publicDir: resolve(__dirname, 'public'), uiExports: { app: { From 6349575ec1edbcea52cbd09f6f98ab4e51d57eea Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 5 May 2020 11:11:21 +0300 Subject: [PATCH 04/75] [data.search.aggs]: Expression functions for bucket agg types (#64772) * [data.search.aggs]: Expression functions for bucket agg types - ranges agg types + significant terms * new portion of changes * add geo_tile_fn * add geo_hash_fn * Update src/plugins/data/public/search/aggs/buckets/filter_fn.ts Co-authored-by: Luke Elmers * Update src/plugins/data/public/search/aggs/buckets/geo_tile_fn.ts Co-authored-by: Luke Elmers * Update src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts Co-authored-by: Luke Elmers * Update src/plugins/data/public/search/aggs/buckets/ip_range_fn.ts Co-authored-by: Luke Elmers * Update src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts Co-authored-by: Luke Elmers * Update src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts Co-authored-by: Luke Elmers * Update src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts Co-authored-by: Luke Elmers * create BaseAggParams * add filters_fn * add histogram / date_histogram expression functions * cleanup * terms - order should be optional * add custom label params Co-authored-by: Luke Elmers Co-authored-by: Elastic Machine --- src/plugins/data/public/public.api.md | 1 + .../data/public/search/aggs/agg_config.ts | 23 ++- .../data/public/search/aggs/agg_types.ts | 25 ++- .../search/aggs/buckets/date_histogram.ts | 18 +- .../aggs/buckets/date_histogram_fn.test.ts | 120 ++++++++++++++ .../search/aggs/buckets/date_histogram_fn.ts | 155 ++++++++++++++++++ .../public/search/aggs/buckets/date_range.ts | 7 + .../search/aggs/buckets/date_range_fn.test.ts | 101 ++++++++++++ .../search/aggs/buckets/date_range_fn.ts | 111 +++++++++++++ .../data/public/search/aggs/buckets/filter.ts | 6 + .../search/aggs/buckets/filter_fn.test.ts | 85 ++++++++++ .../public/search/aggs/buckets/filter_fn.ts | 99 +++++++++++ .../public/search/aggs/buckets/filters.ts | 8 + .../search/aggs/buckets/filters_fn.test.ts | 91 ++++++++++ .../public/search/aggs/buckets/filters_fn.ts | 93 +++++++++++ .../public/search/aggs/buckets/geo_hash.ts | 11 ++ .../search/aggs/buckets/geo_hash_fn.test.ts | 112 +++++++++++++ .../public/search/aggs/buckets/geo_hash_fn.ts | 129 +++++++++++++++ .../public/search/aggs/buckets/geo_tile.ts | 7 + .../search/aggs/buckets/geo_tile_fn.test.ts | 91 ++++++++++ .../public/search/aggs/buckets/geo_tile_fn.ts | 108 ++++++++++++ .../public/search/aggs/buckets/histogram.ts | 11 ++ .../search/aggs/buckets/histogram_fn.test.ts | 109 ++++++++++++ .../search/aggs/buckets/histogram_fn.ts | 132 +++++++++++++++ .../data/public/search/aggs/buckets/index.ts | 7 + .../public/search/aggs/buckets/ip_range.ts | 28 +++- .../search/aggs/buckets/ip_range_fn.test.ts | 102 ++++++++++++ .../public/search/aggs/buckets/ip_range_fn.ts | 114 +++++++++++++ .../search/aggs/buckets/lib/date_range.ts | 4 +- .../aggs/buckets/lib/extended_bounds.ts | 23 +++ .../search/aggs/buckets/lib/geo_point.ts | 86 ++++++++++ .../search/aggs/buckets/lib/ip_range.ts | 15 +- .../data/public/search/aggs/buckets/range.ts | 9 + .../search/aggs/buckets/range_fn.test.ts | 100 +++++++++++ .../public/search/aggs/buckets/range_fn.ts | 106 ++++++++++++ .../search/aggs/buckets/significant_terms.ts | 8 + .../aggs/buckets/significant_terms_fn.test.ts | 96 +++++++++++ .../aggs/buckets/significant_terms_fn.ts | 116 +++++++++++++ .../data/public/search/aggs/buckets/terms.ts | 7 +- .../search/aggs/buckets/terms_fn.test.ts | 40 +++-- .../public/search/aggs/buckets/terms_fn.ts | 52 ++---- src/plugins/data/public/search/aggs/types.ts | 29 +++- .../search/aggs/utils/get_parsed_value.ts | 34 ++++ 43 files changed, 2551 insertions(+), 78 deletions(-) create mode 100644 src/plugins/data/public/search/aggs/buckets/date_histogram_fn.test.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/date_histogram_fn.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/date_range_fn.test.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/date_range_fn.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/filter_fn.test.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/filter_fn.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/filters_fn.test.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/filters_fn.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/geo_hash_fn.test.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/geo_tile_fn.test.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/geo_tile_fn.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/histogram_fn.test.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/histogram_fn.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/ip_range_fn.test.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/ip_range_fn.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/lib/extended_bounds.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/lib/geo_point.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/range_fn.test.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/range_fn.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/significant_terms_fn.test.ts create mode 100644 src/plugins/data/public/search/aggs/buckets/significant_terms_fn.ts create mode 100644 src/plugins/data/public/search/aggs/utils/get_parsed_value.ts diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index fab9475d05dc2..088c82eb4ec65 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -14,6 +14,7 @@ import { Component } from 'react'; import { CoreSetup } from 'src/core/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; +import { Ensure } from '@kbn/utility-types'; import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiComboBoxProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; diff --git a/src/plugins/data/public/search/aggs/agg_config.ts b/src/plugins/data/public/search/aggs/agg_config.ts index 973c69e3d4f5f..86a2c3e0e82e4 100644 --- a/src/plugins/data/public/search/aggs/agg_config.ts +++ b/src/plugins/data/public/search/aggs/agg_config.ts @@ -19,7 +19,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Assign } from '@kbn/utility-types'; +import { Assign, Ensure } from '@kbn/utility-types'; import { ExpressionAstFunction, ExpressionAstArgument } from 'src/plugins/expressions/public'; import { IAggType } from './agg_type'; import { writeParams } from './agg_params'; @@ -31,17 +31,22 @@ import { FieldFormatsStart } from '../../field_formats'; type State = string | number | boolean | null | undefined | SerializableState; -interface SerializableState { +/** @internal **/ +export interface SerializableState { [key: string]: State | State[]; } -export interface AggConfigSerialized { - type: string; - enabled?: boolean; - id?: string; - params?: SerializableState; - schema?: string; -} +/** @internal **/ +export type AggConfigSerialized = Ensure< + { + type: string; + enabled?: boolean; + id?: string; + params?: SerializableState; + schema?: string; + }, + SerializableState +>; export interface AggConfigDependencies { fieldFormats: FieldFormatsStart; diff --git a/src/plugins/data/public/search/aggs/agg_types.ts b/src/plugins/data/public/search/aggs/agg_types.ts index da07f581c9274..7c7d7609cc82f 100644 --- a/src/plugins/data/public/search/aggs/agg_types.ts +++ b/src/plugins/data/public/search/aggs/agg_types.ts @@ -105,6 +105,29 @@ export const getAggTypes = ({ ], }); +/** Buckets: **/ +import { aggFilter } from './buckets/filter_fn'; +import { aggFilters } from './buckets/filters_fn'; +import { aggSignificantTerms } from './buckets/significant_terms_fn'; +import { aggIpRange } from './buckets/ip_range_fn'; +import { aggDateRange } from './buckets/date_range_fn'; +import { aggRange } from './buckets/range_fn'; +import { aggGeoTile } from './buckets/geo_tile_fn'; +import { aggGeoHash } from './buckets/geo_hash_fn'; +import { aggHistogram } from './buckets/histogram_fn'; +import { aggDateHistogram } from './buckets/date_histogram_fn'; import { aggTerms } from './buckets/terms_fn'; -export const getAggTypesFunctions = () => [aggTerms]; +export const getAggTypesFunctions = () => [ + aggFilter, + aggFilters, + aggSignificantTerms, + aggIpRange, + aggDateRange, + aggRange, + aggGeoTile, + aggGeoHash, + aggDateHistogram, + aggHistogram, + aggTerms, +]; diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts index 3ecdc17cb57f3..219bb5440c8da 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -27,7 +27,7 @@ import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { createFilterDateHistogram } from './create_filter/date_histogram'; import { intervalOptions } from './_interval_options'; -import { dateHistogramInterval } from '../../../../common'; +import { dateHistogramInterval, TimeRange } from '../../../../common'; import { writeParams } from '../agg_params'; import { isMetricAggType } from '../metrics/metric_agg_type'; @@ -35,6 +35,8 @@ import { FIELD_FORMAT_IDS, KBN_FIELD_TYPES } from '../../../../common'; import { TimefilterContract } from '../../../query'; import { QuerySetup } from '../../../query/query_service'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; +import { ExtendedBounds } from './lib/extended_bounds'; const detectedTimezone = moment.tz.guess(); const tzOffset = moment().format('Z'); @@ -67,6 +69,19 @@ export function isDateHistogramBucketAggConfig(agg: any): agg is IBucketDateHist return Boolean(agg.buckets); } +export interface AggParamsDateHistogram extends BaseAggParams { + field?: string; + timeRange?: TimeRange; + useNormalizedEsInterval?: boolean; + scaleMetricValues?: boolean; + interval?: string; + time_zone?: string; + drop_partials?: boolean; + format?: string; + min_doc_count?: number; + extended_bounds?: ExtendedBounds; +} + export const getDateHistogramBucketAgg = ({ uiSettings, query, @@ -89,6 +104,7 @@ export const getDateHistogramBucketAgg = ({ } const field = agg.getFieldDisplayName(); + return i18n.translate('data.search.aggs.buckets.dateHistogramLabel', { defaultMessage: '{fieldName} per {intervalDescription}', values: { diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram_fn.test.ts new file mode 100644 index 0000000000000..bd3c4f8dd58cf --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram_fn.test.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggDateHistogram } from './date_histogram_fn'; + +describe('agg_expression_functions', () => { + describe('aggDateHistogram', () => { + const fn = functionWrapper(aggDateHistogram()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({}); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "drop_partials": undefined, + "extended_bounds": undefined, + "field": undefined, + "format": undefined, + "interval": undefined, + "json": undefined, + "min_doc_count": undefined, + "scaleMetricValues": undefined, + "timeRange": undefined, + "time_zone": undefined, + "useNormalizedEsInterval": undefined, + }, + "schema": undefined, + "type": "date_histogram", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + field: 'field', + timeRange: JSON.stringify({ + from: 'from', + to: 'to', + }), + useNormalizedEsInterval: true, + scaleMetricValues: true, + interval: 'interval', + time_zone: 'time_zone', + drop_partials: false, + format: 'format', + min_doc_count: 1, + extended_bounds: JSON.stringify({ + min: 1, + max: 2, + }), + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "drop_partials": false, + "extended_bounds": Object { + "max": 2, + "min": 1, + }, + "field": "field", + "format": "format", + "interval": "interval", + "json": undefined, + "min_doc_count": 1, + "scaleMetricValues": true, + "timeRange": Object { + "from": "from", + "to": "to", + }, + "time_zone": "time_zone", + "useNormalizedEsInterval": true, + }, + "schema": undefined, + "type": "date_histogram", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + + expect(() => { + fn({ + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram_fn.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram_fn.ts new file mode 100644 index 0000000000000..033b44da0880f --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram_fn.ts @@ -0,0 +1,155 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggDateHistogram'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; + +type Arguments = Assign; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggDateHistogram = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.dateHistogram.help', { + defaultMessage: 'Generates a serialized agg config for a Histogram agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.dateHistogram.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + useNormalizedEsInterval: { + types: ['boolean'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.useNormalizedEsInterval.help', { + defaultMessage: 'Specifies whether to use useNormalizedEsInterval for this aggregation', + }), + }, + time_zone: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.timeZone.help', { + defaultMessage: 'Time zone to use for this aggregation', + }), + }, + format: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.format.help', { + defaultMessage: 'Format to use for this aggregation', + }), + }, + scaleMetricValues: { + types: ['boolean'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.scaleMetricValues.help', { + defaultMessage: 'Specifies whether to use scaleMetricValues for this aggregation', + }), + }, + interval: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.interval.help', { + defaultMessage: 'Interval to use for this aggregation', + }), + }, + timeRange: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.timeRange.help', { + defaultMessage: 'Time Range to use for this aggregation', + }), + }, + min_doc_count: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.minDocCount.help', { + defaultMessage: 'Minimum document count to use for this aggregation', + }), + }, + drop_partials: { + types: ['boolean'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.dropPartials.help', { + defaultMessage: 'Specifies whether to use drop_partials for this aggregation', + }), + }, + extended_bounds: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.extendedBounds.help', { + defaultMessage: + 'With extended_bounds setting, you now can "force" the histogram aggregation to start building buckets on a specific min value and also keep on building buckets up to a max value ', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateHistogram.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: BUCKET_TYPES.DATE_HISTOGRAM, + params: { + ...rest, + timeRange: getParsedValue(args, 'timeRange'), + extended_bounds: getParsedValue(args, 'extended_bounds'), + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/buckets/date_range.ts b/src/plugins/data/public/search/aggs/buckets/date_range.ts index 07d927e64a943..504958854cad4 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_range.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_range.ts @@ -29,6 +29,7 @@ import { convertDateRangeToString, DateRangeKey } from './lib/date_range'; import { KBN_FIELD_TYPES, FieldFormat, TEXT_CONTEXT_TYPE } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; const dateRangeTitle = i18n.translate('data.search.aggs.buckets.dateRangeTitle', { defaultMessage: 'Date Range', @@ -39,6 +40,12 @@ export interface DateRangeBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } +export interface AggParamsDateRange extends BaseAggParams { + field?: string; + ranges?: DateRangeKey[]; + time_zone?: string; +} + export const getDateRangeBucketAgg = ({ uiSettings, getInternalStartServices, diff --git a/src/plugins/data/public/search/aggs/buckets/date_range_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/date_range_fn.test.ts new file mode 100644 index 0000000000000..93bb791874e67 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/date_range_fn.test.ts @@ -0,0 +1,101 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggDateRange } from './date_range_fn'; + +describe('agg_expression_functions', () => { + describe('aggDateRange', () => { + const fn = functionWrapper(aggDateRange()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({}); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": undefined, + "json": undefined, + "ranges": undefined, + "time_zone": undefined, + }, + "schema": undefined, + "type": "date_range", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + field: 'date_field', + time_zone: 'UTC +3', + ranges: JSON.stringify([ + { from: 'now-1w/w', to: 'now' }, + { from: 1588163532470, to: 1588163532481 }, + ]), + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "date_field", + "json": undefined, + "ranges": Array [ + Object { + "from": "now-1w/w", + "to": "now", + }, + Object { + "from": 1588163532470, + "to": 1588163532481, + }, + ], + "time_zone": "UTC +3", + }, + "schema": undefined, + "type": "date_range", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'date_field', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + + expect(() => { + fn({ + field: 'date_field', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/date_range_fn.ts b/src/plugins/data/public/search/aggs/buckets/date_range_fn.ts new file mode 100644 index 0000000000000..1fe42ce63d815 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/date_range_fn.ts @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggDateRange'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; + +type Arguments = Assign; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggDateRange = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.dateRange.help', { + defaultMessage: 'Generates a serialized agg config for a Date Range agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateRange.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.dateRange.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateRange.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateRange.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + ranges: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateRange.ranges.help', { + defaultMessage: 'Serialized ranges to use for this aggregation.', + }), + }, + time_zone: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateRange.timeZone.help', { + defaultMessage: 'Time zone to use for this aggregation.', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateRange.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.dateRange.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: BUCKET_TYPES.DATE_RANGE, + params: { + ...rest, + json: getParsedValue(args, 'json'), + ranges: getParsedValue(args, 'ranges'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/buckets/filter.ts b/src/plugins/data/public/search/aggs/buckets/filter.ts index accbdf4dd783d..69157edad4f68 100644 --- a/src/plugins/data/public/search/aggs/buckets/filter.ts +++ b/src/plugins/data/public/search/aggs/buckets/filter.ts @@ -21,6 +21,8 @@ import { i18n } from '@kbn/i18n'; import { BucketAggType } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { GetInternalStartServicesFn } from '../../../types'; +import { GeoBoundingBox } from './lib/geo_point'; +import { BaseAggParams } from '../types'; const filterTitle = i18n.translate('data.search.aggs.buckets.filterTitle', { defaultMessage: 'Filter', @@ -30,6 +32,10 @@ export interface FilterBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } +export interface AggParamsFilter extends BaseAggParams { + geo_bounding_box?: GeoBoundingBox; +} + export const getFilterBucketAgg = ({ getInternalStartServices }: FilterBucketAggDependencies) => new BucketAggType( { diff --git a/src/plugins/data/public/search/aggs/buckets/filter_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/filter_fn.test.ts new file mode 100644 index 0000000000000..c820a73b0a894 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/filter_fn.test.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggFilter } from './filter_fn'; + +describe('agg_expression_functions', () => { + describe('aggFilter', () => { + const fn = functionWrapper(aggFilter()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({}); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "geo_bounding_box": undefined, + "json": undefined, + }, + "schema": undefined, + "type": "filter", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + geo_bounding_box: JSON.stringify({ + wkt: 'BBOX (-74.1, -71.12, 40.73, 40.01)', + }), + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "geo_bounding_box": Object { + "wkt": "BBOX (-74.1, -71.12, 40.73, 40.01)", + }, + "json": undefined, + }, + "schema": undefined, + "type": "filter", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + + expect(() => { + fn({ + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/filter_fn.ts b/src/plugins/data/public/search/aggs/buckets/filter_fn.ts new file mode 100644 index 0000000000000..4a7180fc86c71 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/filter_fn.ts @@ -0,0 +1,99 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggFilter'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; + +type Arguments = Assign; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggFilter = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.filter.help', { + defaultMessage: 'Generates a serialized agg config for a Filter agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.filter.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.filter.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.filter.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + geo_bounding_box: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.filter.geoBoundingBox.help', { + defaultMessage: 'Filter results based on a point location within a bounding box', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.filter.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.filter.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: BUCKET_TYPES.FILTER, + params: { + ...rest, + json: getParsedValue(args, 'json'), + geo_bounding_box: getParsedValue(args, 'geo_bounding_box'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/buckets/filters.ts b/src/plugins/data/public/search/aggs/buckets/filters.ts index fe013928bba65..c993c0fc13800 100644 --- a/src/plugins/data/public/search/aggs/buckets/filters.ts +++ b/src/plugins/data/public/search/aggs/buckets/filters.ts @@ -29,6 +29,7 @@ import { Storage } from '../../../../../../plugins/kibana_utils/public'; import { getEsQueryConfig, buildEsQuery, Query } from '../../../../common'; import { getQueryLog } from '../../../query'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; const filtersTitle = i18n.translate('data.search.aggs.buckets.filtersTitle', { defaultMessage: 'Filters', @@ -47,6 +48,13 @@ export interface FiltersBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } +export interface AggParamsFilters extends BaseAggParams { + filters?: Array<{ + input: Query; + label: string; + }>; +} + export const getFiltersBucketAgg = ({ uiSettings, getInternalStartServices, diff --git a/src/plugins/data/public/search/aggs/buckets/filters_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/filters_fn.test.ts new file mode 100644 index 0000000000000..99c4f7d8c2b65 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/filters_fn.test.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggFilters } from './filters_fn'; + +describe('agg_expression_functions', () => { + describe('aggFilters', () => { + const fn = functionWrapper(aggFilters()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({}); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "filters": undefined, + "json": undefined, + }, + "schema": undefined, + "type": "filters", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + filters: JSON.stringify([ + { + query: 'query', + language: 'lucene', + label: 'test', + }, + ]), + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": undefined, + "params": Object { + "filters": Array [ + Object { + "label": "test", + "language": "lucene", + "query": "query", + }, + ], + "json": undefined, + }, + "schema": undefined, + "type": "filters", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + + expect(() => { + fn({ + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/filters_fn.ts b/src/plugins/data/public/search/aggs/buckets/filters_fn.ts new file mode 100644 index 0000000000000..6ffd5369d7087 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/filters_fn.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggFilters'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; + +type Arguments = Assign; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggFilters = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.filters.help', { + defaultMessage: 'Generates a serialized agg config for a Filter agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.filters.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.filters.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.filters.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + filters: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.filters.filters.help', { + defaultMessage: 'Filters to use for this aggregation', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.filters.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: BUCKET_TYPES.FILTERS, + params: { + ...rest, + filters: getParsedValue(args, 'filters'), + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash.ts b/src/plugins/data/public/search/aggs/buckets/geo_hash.ts index eab10edad60f6..be339de5d7fae 100644 --- a/src/plugins/data/public/search/aggs/buckets/geo_hash.ts +++ b/src/plugins/data/public/search/aggs/buckets/geo_hash.ts @@ -22,6 +22,8 @@ import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { KBN_FIELD_TYPES } from '../../../../common'; import { BUCKET_TYPES } from './bucket_agg_types'; import { GetInternalStartServicesFn } from '../../../types'; +import { GeoBoundingBox } from './lib/geo_point'; +import { BaseAggParams } from '../types'; const defaultBoundingBox = { top_left: { lat: 1, lon: 1 }, @@ -38,6 +40,15 @@ export interface GeoHashBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } +export interface AggParamsGeoHash extends BaseAggParams { + field: string; + autoPrecision?: boolean; + precision?: number; + useGeocentroid?: boolean; + isFilteredByCollar?: boolean; + boundingBox?: GeoBoundingBox; +} + export const getGeoHashBucketAgg = ({ getInternalStartServices }: GeoHashBucketAggDependencies) => new BucketAggType( { diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/geo_hash_fn.test.ts new file mode 100644 index 0000000000000..07ab8e66f1def --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/geo_hash_fn.test.ts @@ -0,0 +1,112 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggGeoHash } from './geo_hash_fn'; + +describe('agg_expression_functions', () => { + describe('aggGeoHash', () => { + const fn = functionWrapper(aggGeoHash()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'geo_field', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "autoPrecision": undefined, + "boundingBox": undefined, + "customLabel": undefined, + "field": "geo_field", + "isFilteredByCollar": undefined, + "json": undefined, + "precision": undefined, + "useGeocentroid": undefined, + }, + "schema": undefined, + "type": "geohash_grid", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + field: 'geo_field', + autoPrecision: false, + precision: 10, + useGeocentroid: true, + isFilteredByCollar: false, + boundingBox: JSON.stringify({ + top_left: [-74.1, 40.73], + bottom_right: [-71.12, 40.01], + }), + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": undefined, + "params": Object { + "autoPrecision": false, + "boundingBox": Object { + "bottom_right": Array [ + -71.12, + 40.01, + ], + "top_left": Array [ + -74.1, + 40.73, + ], + }, + "customLabel": undefined, + "field": "geo_field", + "isFilteredByCollar": false, + "json": undefined, + "precision": 10, + "useGeocentroid": true, + }, + "schema": undefined, + "type": "geohash_grid", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'geo_field', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + + expect(() => { + fn({ + field: 'geo_field', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts b/src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts new file mode 100644 index 0000000000000..bbfa8575d486c --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts @@ -0,0 +1,129 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggGeoHash'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; + +type Arguments = Assign; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggGeoHash = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.geoHash.help', { + defaultMessage: 'Generates a serialized agg config for a Geo Hash agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.geoHash.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.geoHash.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.geoHash.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.geoHash.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + useGeocentroid: { + types: ['boolean'], + help: i18n.translate('data.search.aggs.buckets.geoHash.useGeocentroid.help', { + defaultMessage: 'Specifies whether to use geocentroid for this aggregation', + }), + }, + autoPrecision: { + types: ['boolean'], + help: i18n.translate('data.search.aggs.buckets.geoHash.autoPrecision.help', { + defaultMessage: 'Specifies whether to use auto precision for this aggregation', + }), + }, + isFilteredByCollar: { + types: ['boolean'], + help: i18n.translate('data.search.aggs.buckets.geoHash.isFilteredByCollar.help', { + defaultMessage: 'Specifies whether to filter by collar', + }), + }, + boundingBox: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.geoHash.boundingBox.help', { + defaultMessage: 'Filter results based on a point location within a bounding box', + }), + }, + precision: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.geoHash.precision.help', { + defaultMessage: 'Precision to use for this aggregation.', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.geoHash.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.geoHash.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: BUCKET_TYPES.GEOHASH_GRID, + params: { + ...rest, + boundingBox: getParsedValue(args, 'boundingBox'), + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/buckets/geo_tile.ts b/src/plugins/data/public/search/aggs/buckets/geo_tile.ts index c981e8400f9a1..1212bba23a93a 100644 --- a/src/plugins/data/public/search/aggs/buckets/geo_tile.ts +++ b/src/plugins/data/public/search/aggs/buckets/geo_tile.ts @@ -25,6 +25,7 @@ import { BUCKET_TYPES } from './bucket_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { METRIC_TYPES } from '../metrics/metric_agg_types'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; export interface GeoTitleBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; @@ -34,6 +35,12 @@ const geotileGridTitle = i18n.translate('data.search.aggs.buckets.geotileGridTit defaultMessage: 'Geotile', }); +export interface AggParamsGeoTile extends BaseAggParams { + field: string; + useGeocentroid?: boolean; + precision?: number; +} + export const getGeoTitleBucketAgg = ({ getInternalStartServices }: GeoTitleBucketAggDependencies) => new BucketAggType( { diff --git a/src/plugins/data/public/search/aggs/buckets/geo_tile_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/geo_tile_fn.test.ts new file mode 100644 index 0000000000000..bfaf47ede8734 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/geo_tile_fn.test.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggGeoTile } from './geo_tile_fn'; + +describe('agg_expression_functions', () => { + describe('aggGeoTile', () => { + const fn = functionWrapper(aggGeoTile()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'geo_field', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "geo_field", + "json": undefined, + "precision": undefined, + "useGeocentroid": undefined, + }, + "schema": undefined, + "type": "geotile_grid", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + field: 'geo_field', + useGeocentroid: false, + precision: 10, + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "geo_field", + "json": undefined, + "precision": 10, + "useGeocentroid": false, + }, + "schema": undefined, + "type": "geotile_grid", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'geo_field', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + + expect(() => { + fn({ + field: 'geo_field', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/geo_tile_fn.ts b/src/plugins/data/public/search/aggs/buckets/geo_tile_fn.ts new file mode 100644 index 0000000000000..9c33ef45762af --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/geo_tile_fn.ts @@ -0,0 +1,108 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggGeoTile'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggGeoTile = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.geoTile.help', { + defaultMessage: 'Generates a serialized agg config for a Geo Tile agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.geoTile.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.geoTile.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.geoTile.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.geoTile.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + useGeocentroid: { + types: ['boolean'], + help: i18n.translate('data.search.aggs.buckets.geoTile.useGeocentroid.help', { + defaultMessage: 'Specifies whether to use geocentroid for this aggregation', + }), + }, + precision: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.geoTile.precision.help', { + defaultMessage: 'Precision to use for this aggregation.', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.geoTile.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.geoTile.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: BUCKET_TYPES.GEOTILE_GRID, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/buckets/histogram.ts b/src/plugins/data/public/search/aggs/buckets/histogram.ts index f8e8720d24ea9..d04df4f8aac6b 100644 --- a/src/plugins/data/public/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/histogram.ts @@ -26,6 +26,8 @@ import { createFilterHistogram } from './create_filter/histogram'; import { BUCKET_TYPES } from './bucket_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; +import { ExtendedBounds } from './lib/extended_bounds'; export interface AutoBounds { min: number; @@ -42,6 +44,15 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig { getAutoBounds: () => AutoBounds; } +export interface AggParamsHistogram extends BaseAggParams { + field: string; + interval: string; + intervalBase?: number; + min_doc_count?: boolean; + has_extended_bounds?: boolean; + extended_bounds?: ExtendedBounds; +} + export const getHistogramBucketAgg = ({ uiSettings, getInternalStartServices, diff --git a/src/plugins/data/public/search/aggs/buckets/histogram_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/histogram_fn.test.ts new file mode 100644 index 0000000000000..34b6fa1a6dcd6 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/histogram_fn.test.ts @@ -0,0 +1,109 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggHistogram } from './histogram_fn'; + +describe('agg_expression_functions', () => { + describe('aggHistogram', () => { + const fn = functionWrapper(aggHistogram()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'field', + interval: '10', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "extended_bounds": undefined, + "field": "field", + "has_extended_bounds": undefined, + "interval": "10", + "intervalBase": undefined, + "json": undefined, + "min_doc_count": undefined, + }, + "schema": undefined, + "type": "histogram", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + field: 'field', + interval: '10', + intervalBase: 1, + min_doc_count: false, + has_extended_bounds: false, + extended_bounds: JSON.stringify({ + min: 1, + max: 2, + }), + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "extended_bounds": Object { + "max": 2, + "min": 1, + }, + "field": "field", + "has_extended_bounds": false, + "interval": "10", + "intervalBase": 1, + "json": undefined, + "min_doc_count": false, + }, + "schema": undefined, + "type": "histogram", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'field', + interval: '10', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + + expect(() => { + fn({ + field: 'field', + interval: '10', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/histogram_fn.ts b/src/plugins/data/public/search/aggs/buckets/histogram_fn.ts new file mode 100644 index 0000000000000..1e5a5a72c0ecb --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/histogram_fn.ts @@ -0,0 +1,132 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggHistogram'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; + +type Arguments = Assign; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggHistogram = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.histogram.help', { + defaultMessage: 'Generates a serialized agg config for a Histogram agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.histogram.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.histogram.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.histogram.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.histogram.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + interval: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.histogram.interval.help', { + defaultMessage: 'Interval to use for this aggregation', + }), + }, + intervalBase: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.histogram.intervalBase.help', { + defaultMessage: 'IntervalBase to use for this aggregation', + }), + }, + min_doc_count: { + types: ['boolean'], + help: i18n.translate('data.search.aggs.buckets.histogram.minDocCount.help', { + defaultMessage: 'Specifies whether to use min_doc_count for this aggregation', + }), + }, + has_extended_bounds: { + types: ['boolean'], + help: i18n.translate('data.search.aggs.buckets.histogram.hasExtendedBounds.help', { + defaultMessage: 'Specifies whether to use has_extended_bounds for this aggregation', + }), + }, + extended_bounds: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.histogram.extendedBounds.help', { + defaultMessage: + 'With extended_bounds setting, you now can "force" the histogram aggregation to start building buckets on a specific min value and also keep on building buckets up to a max value ', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.histogram.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.histogram.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: BUCKET_TYPES.HISTOGRAM, + params: { + ...rest, + extended_bounds: getParsedValue(args, 'extended_bounds'), + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/buckets/index.ts b/src/plugins/data/public/search/aggs/buckets/index.ts index 3a402b1498a77..7036cc7785db7 100644 --- a/src/plugins/data/public/search/aggs/buckets/index.ts +++ b/src/plugins/data/public/search/aggs/buckets/index.ts @@ -19,11 +19,18 @@ export * from './_interval_options'; export * from './bucket_agg_types'; +export * from './histogram'; export * from './date_histogram'; export * from './date_range'; +export * from './range'; +export * from './filter'; +export * from './filters'; +export * from './geo_tile'; +export * from './geo_hash'; export * from './ip_range'; export * from './lib/cidr_mask'; export * from './lib/date_range'; export * from './lib/ip_range'; export * from './migrate_include_exclude_format'; +export * from './significant_terms'; export * from './terms'; diff --git a/src/plugins/data/public/search/aggs/buckets/ip_range.ts b/src/plugins/data/public/search/aggs/buckets/ip_range.ts index bde347d6e673d..029fd864154be 100644 --- a/src/plugins/data/public/search/aggs/buckets/ip_range.ts +++ b/src/plugins/data/public/search/aggs/buckets/ip_range.ts @@ -23,18 +23,38 @@ import { BucketAggType } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { createFilterIpRange } from './create_filter/ip_range'; -import { IpRangeKey, convertIPRangeToString } from './lib/ip_range'; +import { + convertIPRangeToString, + IpRangeKey, + RangeIpRangeAggKey, + CidrMaskIpRangeAggKey, +} from './lib/ip_range'; import { KBN_FIELD_TYPES, FieldFormat, TEXT_CONTEXT_TYPE } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; const ipRangeTitle = i18n.translate('data.search.aggs.buckets.ipRangeTitle', { defaultMessage: 'IPv4 Range', }); +export enum IP_RANGE_TYPES { + FROM_TO = 'fromTo', + MASK = 'mask', +} + export interface IpRangeBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } +export interface AggParamsIpRange extends BaseAggParams { + field: string; + ipRangeType?: IP_RANGE_TYPES; + ranges?: Partial<{ + [IP_RANGE_TYPES.FROM_TO]: RangeIpRangeAggKey[]; + [IP_RANGE_TYPES.MASK]: CidrMaskIpRangeAggKey[]; + }>; +} + export const getIpRangeBucketAgg = ({ getInternalStartServices }: IpRangeBucketAggDependencies) => new BucketAggType( { @@ -42,7 +62,7 @@ export const getIpRangeBucketAgg = ({ getInternalStartServices }: IpRangeBucketA title: ipRangeTitle, createFilter: createFilterIpRange, getKey(bucket, key, agg): IpRangeKey { - if (agg.params.ipRangeType === 'mask') { + if (agg.params.ipRangeType === IP_RANGE_TYPES.MASK) { return { type: 'mask', mask: key }; } return { type: 'range', from: bucket.from, to: bucket.to }; @@ -74,7 +94,7 @@ export const getIpRangeBucketAgg = ({ getInternalStartServices }: IpRangeBucketA }, { name: 'ipRangeType', - default: 'fromTo', + default: IP_RANGE_TYPES.FROM_TO, write: noop, }, { @@ -90,7 +110,7 @@ export const getIpRangeBucketAgg = ({ getInternalStartServices }: IpRangeBucketA const ipRangeType = aggConfig.params.ipRangeType; let ranges = aggConfig.params.ranges[ipRangeType]; - if (ipRangeType === 'fromTo') { + if (ipRangeType === IP_RANGE_TYPES.FROM_TO) { ranges = map(ranges, (range: any) => omit(range, isNull)); } diff --git a/src/plugins/data/public/search/aggs/buckets/ip_range_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/ip_range_fn.test.ts new file mode 100644 index 0000000000000..5940345b25890 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/ip_range_fn.test.ts @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { IP_RANGE_TYPES } from './ip_range'; +import { aggIpRange } from './ip_range_fn'; + +describe('agg_expression_functions', () => { + describe('aggIpRange', () => { + const fn = functionWrapper(aggIpRange()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'ip_field', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "ip_field", + "ipRangeType": undefined, + "json": undefined, + "ranges": undefined, + }, + "schema": undefined, + "type": "ip_range", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + field: 'ip_field', + ipRangeType: IP_RANGE_TYPES.MASK, + ranges: JSON.stringify({ + mask: [{ mask: '10.0.0.0/25' }], + }), + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "ip_field", + "ipRangeType": "mask", + "json": undefined, + "ranges": Object { + "mask": Array [ + Object { + "mask": "10.0.0.0/25", + }, + ], + }, + }, + "schema": undefined, + "type": "ip_range", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'ip_field', + ipRangeType: IP_RANGE_TYPES.MASK, + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + + expect(() => { + fn({ + field: 'ip_field', + ipRangeType: IP_RANGE_TYPES.FROM_TO, + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/ip_range_fn.ts b/src/plugins/data/public/search/aggs/buckets/ip_range_fn.ts new file mode 100644 index 0000000000000..554a8708d9164 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/ip_range_fn.ts @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggIpRange'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; + +type Arguments = Assign; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggIpRange = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.ipRange.help', { + defaultMessage: 'Generates a serialized agg config for a Ip Range agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.ipRange.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.ipRange.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.ipRange.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.ipRange.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + ipRangeType: { + types: ['string'], + options: ['mask', 'fromTo'], + help: i18n.translate('data.search.aggs.buckets.ipRange.ipRangeType.help', { + defaultMessage: + 'IP range type to use for this aggregation. Takes one of the following values: mask, fromTo.', + }), + }, + ranges: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.ipRange.ranges.help', { + defaultMessage: 'Serialized ranges to use for this aggregation.', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.ipRange.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.ipRange.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: BUCKET_TYPES.IP_RANGE, + params: { + ...rest, + json: getParsedValue(args, 'json'), + ranges: getParsedValue(args, 'ranges'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/buckets/lib/date_range.ts b/src/plugins/data/public/search/aggs/buckets/lib/date_range.ts index 6eb9fe8414ec8..d52bdff993a2b 100644 --- a/src/plugins/data/public/search/aggs/buckets/lib/date_range.ts +++ b/src/plugins/data/public/search/aggs/buckets/lib/date_range.ts @@ -18,8 +18,8 @@ */ export interface DateRangeKey { - from: number; - to: number; + from: number | string; + to: number | string; } export function convertDateRangeToString({ from, to }: DateRangeKey, format: (val: any) => string) { diff --git a/src/plugins/data/public/search/aggs/buckets/lib/extended_bounds.ts b/src/plugins/data/public/search/aggs/buckets/lib/extended_bounds.ts new file mode 100644 index 0000000000000..7a249a9daca91 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/lib/extended_bounds.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface ExtendedBounds { + min: number; + max: number; +} diff --git a/src/plugins/data/public/search/aggs/buckets/lib/geo_point.ts b/src/plugins/data/public/search/aggs/buckets/lib/geo_point.ts new file mode 100644 index 0000000000000..8ff4493e286cf --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/lib/geo_point.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +type GeoPoint = + | { + lat: number; + lon: number; + } + | string + | [number, number]; + +interface GeoBox { + top: number; + left: number; + bottom: number; + right: number; +} + +/** GeoBoundingBox Accepted Formats: + * Lat Lon As Properties: + * "top_left" : { + * "lat" : 40.73, "lon" : -74.1 + * }, + * "bottom_right" : { + * "lat" : 40.01, "lon" : -71.12 + * } + * + * Lat Lon As Array: + * { + * "top_left" : [-74.1, 40.73], + * "bottom_right" : [-71.12, 40.01] + * } + * + * Lat Lon As String: + * { + * "top_left" : "40.73, -74.1", + * "bottom_right" : "40.01, -71.12" + * } + * + * Bounding Box as Well-Known Text (WKT): + * { + * "wkt" : "BBOX (-74.1, -71.12, 40.73, 40.01)" + * } + * + * Geohash: + * { + * "top_right" : "dr5r9ydj2y73", + * "bottom_left" : "drj7teegpus6" + * } + * + * Vertices: + * { + * "top" : 40.73, + * "left" : -74.1, + * "bottom" : 40.01, + * "right" : -71.12 + * } + * + * **/ +export type GeoBoundingBox = + | Partial<{ + top_left: GeoPoint; + top_right: GeoPoint; + bottom_right: GeoPoint; + bottom_left: GeoPoint; + }> + | { + wkt: string; + } + | GeoBox; diff --git a/src/plugins/data/public/search/aggs/buckets/lib/ip_range.ts b/src/plugins/data/public/search/aggs/buckets/lib/ip_range.ts index be1ac28934c7c..57e5337d4c365 100644 --- a/src/plugins/data/public/search/aggs/buckets/lib/ip_range.ts +++ b/src/plugins/data/public/search/aggs/buckets/lib/ip_range.ts @@ -17,9 +17,18 @@ * under the License. */ -export type IpRangeKey = - | { type: 'mask'; mask: string } - | { type: 'range'; from: string; to: string }; +export interface CidrMaskIpRangeAggKey { + type: 'mask'; + mask: string; +} + +export interface RangeIpRangeAggKey { + type: 'range'; + from: string; + to: string; +} + +export type IpRangeKey = CidrMaskIpRangeAggKey | RangeIpRangeAggKey; export const convertIPRangeToString = (range: IpRangeKey, format: (val: any) => string) => { if (range.type === 'mask') { diff --git a/src/plugins/data/public/search/aggs/buckets/range.ts b/src/plugins/data/public/search/aggs/buckets/range.ts index 2c1303814a88a..02aad3bd5fed1 100644 --- a/src/plugins/data/public/search/aggs/buckets/range.ts +++ b/src/plugins/data/public/search/aggs/buckets/range.ts @@ -24,6 +24,7 @@ import { RangeKey } from './range_key'; import { createFilterRange } from './create_filter/range'; import { BUCKET_TYPES } from './bucket_agg_types'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; const keyCaches = new WeakMap(); const formats = new WeakMap(); @@ -36,6 +37,14 @@ export interface RangeBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } +export interface AggParamsRange extends BaseAggParams { + field: string; + ranges?: Array<{ + from: number; + to: number; + }>; +} + export const getRangeBucketAgg = ({ getInternalStartServices }: RangeBucketAggDependencies) => new BucketAggType( { diff --git a/src/plugins/data/public/search/aggs/buckets/range_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/range_fn.test.ts new file mode 100644 index 0000000000000..93ae4490196a8 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/range_fn.test.ts @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggRange } from './range_fn'; + +describe('agg_expression_functions', () => { + describe('aggRange', () => { + const fn = functionWrapper(aggRange()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'number_field', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "number_field", + "json": undefined, + "ranges": undefined, + }, + "schema": undefined, + "type": "range", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + field: 'number_field', + ranges: JSON.stringify([ + { from: 1, to: 2 }, + { from: 5, to: 100 }, + ]), + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "number_field", + "json": undefined, + "ranges": Array [ + Object { + "from": 1, + "to": 2, + }, + Object { + "from": 5, + "to": 100, + }, + ], + }, + "schema": undefined, + "type": "range", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'number_field', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + + expect(() => { + fn({ + field: 'number_field', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/range_fn.ts b/src/plugins/data/public/search/aggs/buckets/range_fn.ts new file mode 100644 index 0000000000000..48686e7061de9 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/range_fn.ts @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggRange'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; + +type Arguments = Assign; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggRange = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.range.help', { + defaultMessage: 'Generates a serialized agg config for a Range agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.range.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.range.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.range.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.range.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + ranges: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.range.ranges.help', { + defaultMessage: 'Serialized ranges to use for this aggregation.', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.range.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.range.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: BUCKET_TYPES.RANGE, + params: { + ...rest, + json: getParsedValue(args, 'json'), + ranges: getParsedValue(args, 'ranges'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms.ts b/src/plugins/data/public/search/aggs/buckets/significant_terms.ts index 49d797f3afbc9..e6afc56dfd31c 100644 --- a/src/plugins/data/public/search/aggs/buckets/significant_terms.ts +++ b/src/plugins/data/public/search/aggs/buckets/significant_terms.ts @@ -24,6 +24,7 @@ import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exc import { BUCKET_TYPES } from './bucket_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; const significantTermsTitle = i18n.translate('data.search.aggs.buckets.significantTermsTitle', { defaultMessage: 'Significant Terms', @@ -33,6 +34,13 @@ export interface SignificantTermsBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } +export interface AggParamsSignificantTerms extends BaseAggParams { + field: string; + size?: number; + exclude?: string; + include?: string; +} + export const getSignificantTermsBucketAgg = ({ getInternalStartServices, }: SignificantTermsBucketAggDependencies) => diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/significant_terms_fn.test.ts new file mode 100644 index 0000000000000..71be4e9cfa9ac --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/significant_terms_fn.test.ts @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggSignificantTerms } from './significant_terms_fn'; + +describe('agg_expression_functions', () => { + describe('aggSignificantTerms', () => { + const fn = functionWrapper(aggSignificantTerms()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "exclude": undefined, + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "size": undefined, + }, + "schema": undefined, + "type": "significant_terms", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: '1', + enabled: false, + schema: 'whatever', + field: 'machine.os.keyword', + size: 6, + include: 'win', + exclude: 'ios', + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": false, + "id": "1", + "params": Object { + "customLabel": undefined, + "exclude": "ios", + "field": "machine.os.keyword", + "include": "win", + "json": undefined, + "size": 6, + }, + "schema": "whatever", + "type": "significant_terms", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms_fn.ts b/src/plugins/data/public/search/aggs/buckets/significant_terms_fn.ts new file mode 100644 index 0000000000000..83583070bddfe --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/significant_terms_fn.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggSignificantTerms'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; + +type Arguments = AggArgs; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggSignificantTerms = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.significantTerms.help', { + defaultMessage: 'Generates a serialized agg config for a Significant Terms agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.significantTerms.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.significantTerms.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.significantTerms.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.significantTerms.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + size: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.significantTerms.size.help', { + defaultMessage: 'Max number of buckets to retrieve', + }), + }, + exclude: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.significantTerms.exclude.help', { + defaultMessage: 'Specific bucket values to exclude from results', + }), + }, + include: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.significantTerms.include.help', { + defaultMessage: 'Specific bucket values to include in results', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.significantTerms.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.significantTerms.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: BUCKET_TYPES.SIGNIFICANT_TERMS, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/buckets/terms.ts b/src/plugins/data/public/search/aggs/buckets/terms.ts index a12a1d7ac2d3d..1bfc508dc3871 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.ts @@ -26,7 +26,7 @@ import { isStringOrNumberType, migrateIncludeExcludeFormat, } from './migrate_include_exclude_format'; -import { AggConfigSerialized, IAggConfigs } from '../types'; +import { AggConfigSerialized, BaseAggParams, IAggConfigs } from '../types'; import { Adapters } from '../../../../../inspector/public'; import { ISearchSource } from '../../search_source'; @@ -63,11 +63,11 @@ export interface TermsBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } -export interface AggParamsTerms { +export interface AggParamsTerms extends BaseAggParams { field: string; - order: 'asc' | 'desc'; orderBy: string; orderAgg?: AggConfigSerialized; + order?: 'asc' | 'desc'; size?: number; missingBucket?: boolean; missingBucketLabel?: string; @@ -76,7 +76,6 @@ export interface AggParamsTerms { // advanced exclude?: string; include?: string; - json?: string; } export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDependencies) => diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts index f55f1de796013..1384a9f17e4b6 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts @@ -27,7 +27,6 @@ describe('agg_expression_functions', () => { test('fills in defaults when only required args are provided', () => { const actual = fn({ field: 'machine.os.keyword', - order: 'asc', orderBy: '1', }); expect(actual).toMatchInlineSnapshot(` @@ -37,18 +36,19 @@ describe('agg_expression_functions', () => { "enabled": true, "id": undefined, "params": Object { + "customLabel": undefined, "exclude": undefined, "field": "machine.os.keyword", "include": undefined, "json": undefined, - "missingBucket": false, - "missingBucketLabel": "Missing", - "order": "asc", + "missingBucket": undefined, + "missingBucketLabel": undefined, + "order": undefined, "orderAgg": undefined, "orderBy": "1", - "otherBucket": false, - "otherBucketLabel": "Other", - "size": 5, + "otherBucket": undefined, + "otherBucketLabel": undefined, + "size": undefined, }, "schema": undefined, "type": "terms", @@ -70,6 +70,7 @@ describe('agg_expression_functions', () => { missingBucketLabel: 'missing', otherBucket: true, otherBucketLabel: 'other', + include: 'win', exclude: 'ios', }); @@ -78,9 +79,10 @@ describe('agg_expression_functions', () => { "enabled": false, "id": "1", "params": Object { + "customLabel": undefined, "exclude": "ios", "field": "machine.os.keyword", - "include": undefined, + "include": "win", "json": undefined, "missingBucket": true, "missingBucketLabel": "missing", @@ -107,37 +109,39 @@ describe('agg_expression_functions', () => { expect(actual.value.params).toMatchInlineSnapshot(` Object { + "customLabel": undefined, "exclude": undefined, "field": "machine.os.keyword", "include": undefined, "json": undefined, - "missingBucket": false, - "missingBucketLabel": "Missing", + "missingBucket": undefined, + "missingBucketLabel": undefined, "order": "asc", "orderAgg": Object { "enabled": true, "id": undefined, "params": Object { + "customLabel": undefined, "exclude": undefined, "field": "name", "include": undefined, "json": undefined, - "missingBucket": false, - "missingBucketLabel": "Missing", + "missingBucket": undefined, + "missingBucketLabel": undefined, "order": "asc", "orderAgg": undefined, "orderBy": "1", - "otherBucket": false, - "otherBucketLabel": "Other", - "size": 5, + "otherBucket": undefined, + "otherBucketLabel": undefined, + "size": undefined, }, "schema": undefined, "type": "terms", }, "orderBy": "1", - "otherBucket": false, - "otherBucketLabel": "Other", - "size": 5, + "otherBucket": undefined, + "otherBucketLabel": undefined, + "size": undefined, } `); }); diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts index 7980bfabe79fb..49520863fe1cc 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms_fn.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts @@ -20,27 +20,25 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; -import { AggExpressionType, AggExpressionFunctionArgs } from '../'; +import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; -const aggName = 'terms'; const fnName = 'aggTerms'; type Input = any; -type AggArgs = AggExpressionFunctionArgs; +type AggArgs = AggExpressionFunctionArgs; + // Since the orderAgg param is an agg nested in a subexpression, we need to // overwrite the param type to expect a value of type AggExpressionType. -type Arguments = AggArgs & - Assign< - AggArgs, - { orderAgg?: AggArgs['orderAgg'] extends undefined ? undefined : AggExpressionType } - >; +type Arguments = Assign; + type Output = AggExpressionType; type FunctionDefinition = ExpressionFunctionDefinition; export const aggTerms = (): FunctionDefinition => ({ name: fnName, help: i18n.translate('data.search.aggs.function.buckets.terms.help', { - defaultMessage: 'Generates a serialized agg config for a terms agg', + defaultMessage: 'Generates a serialized agg config for a Terms agg', }), type: 'agg_type', args: { @@ -72,7 +70,7 @@ export const aggTerms = (): FunctionDefinition => ({ }, order: { types: ['string'], - required: true, + options: ['asc', 'desc'], help: i18n.translate('data.search.aggs.buckets.terms.order.help', { defaultMessage: 'Order in which to return the results: asc or desc', }), @@ -91,41 +89,30 @@ export const aggTerms = (): FunctionDefinition => ({ }, size: { types: ['number'], - default: 5, help: i18n.translate('data.search.aggs.buckets.terms.size.help', { defaultMessage: 'Max number of buckets to retrieve', }), }, missingBucket: { types: ['boolean'], - default: false, help: i18n.translate('data.search.aggs.buckets.terms.missingBucket.help', { defaultMessage: 'When set to true, groups together any buckets with missing fields', }), }, missingBucketLabel: { types: ['string'], - default: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel', { - defaultMessage: 'Missing', - description: `Default label used in charts when documents are missing a field. - Visible when you create a chart with a terms aggregation and enable "Show missing values"`, - }), help: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel.help', { defaultMessage: 'Default label used in charts when documents are missing a field.', }), }, otherBucket: { types: ['boolean'], - default: false, help: i18n.translate('data.search.aggs.buckets.terms.otherBucket.help', { defaultMessage: 'When set to true, groups together any buckets beyond the allowed size', }), }, otherBucketLabel: { types: ['string'], - default: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel', { - defaultMessage: 'Other', - }), help: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel.help', { defaultMessage: 'Default label used in charts for documents in the Other bucket', }), @@ -148,32 +135,27 @@ export const aggTerms = (): FunctionDefinition => ({ defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', }), }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, }, fn: (input, args) => { const { id, enabled, schema, ...rest } = args; - let json; - try { - json = args.json ? JSON.parse(args.json) : undefined; - } catch (e) { - throw new Error('Unable to parse json argument string'); - } - - // Need to spread this object to work around TS bug: - // https://github.com/microsoft/TypeScript/issues/15300#issuecomment-436793742 - const orderAgg = args.orderAgg?.value ? { ...args.orderAgg.value } : undefined; - return { type: 'agg_type', value: { id, enabled, schema, - type: aggName, + type: BUCKET_TYPES.TERMS, params: { ...rest, - orderAgg, - json, + orderAgg: args.orderAgg?.value, + json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/public/search/aggs/types.ts b/src/plugins/data/public/search/aggs/types.ts index 1c5b5b458ce90..8ad264f59cc27 100644 --- a/src/plugins/data/public/search/aggs/types.ts +++ b/src/plugins/data/public/search/aggs/types.ts @@ -21,11 +21,22 @@ import { IndexPattern } from '../../index_patterns'; import { AggConfigSerialized, AggConfigs, + AggParamsRange, + AggParamsIpRange, + AggParamsDateRange, + AggParamsFilter, + AggParamsFilters, + AggParamsSignificantTerms, + AggParamsGeoTile, + AggParamsGeoHash, AggParamsTerms, + AggParamsHistogram, + AggParamsDateHistogram, AggTypesRegistrySetup, AggTypesRegistryStart, CreateAggConfigParams, getCalculateAutoTimeExpression, + BUCKET_TYPES, } from './'; export { IAggConfig, AggConfigSerialized } from './agg_config'; @@ -55,6 +66,12 @@ export interface SearchAggsStart { types: AggTypesRegistryStart; } +/** @internal */ +export interface BaseAggParams { + json?: string; + customLabel?: string; +} + /** @internal */ export interface AggExpressionType { type: 'agg_type'; @@ -74,5 +91,15 @@ export type AggExpressionFunctionArgs< * @internal */ export interface AggParamsMapping { - terms: AggParamsTerms; + [BUCKET_TYPES.RANGE]: AggParamsRange; + [BUCKET_TYPES.IP_RANGE]: AggParamsIpRange; + [BUCKET_TYPES.DATE_RANGE]: AggParamsDateRange; + [BUCKET_TYPES.FILTER]: AggParamsFilter; + [BUCKET_TYPES.FILTERS]: AggParamsFilters; + [BUCKET_TYPES.SIGNIFICANT_TERMS]: AggParamsSignificantTerms; + [BUCKET_TYPES.GEOTILE_GRID]: AggParamsGeoTile; + [BUCKET_TYPES.GEOHASH_GRID]: AggParamsGeoHash; + [BUCKET_TYPES.HISTOGRAM]: AggParamsHistogram; + [BUCKET_TYPES.DATE_HISTOGRAM]: AggParamsDateHistogram; + [BUCKET_TYPES.TERMS]: AggParamsTerms; } diff --git a/src/plugins/data/public/search/aggs/utils/get_parsed_value.ts b/src/plugins/data/public/search/aggs/utils/get_parsed_value.ts new file mode 100644 index 0000000000000..48e752369d1d3 --- /dev/null +++ b/src/plugins/data/public/search/aggs/utils/get_parsed_value.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * This method parses a JSON string and constructs the Object or object described by the string. + * If the given string is not valid JSON, you will get a syntax error. + * @param data { Object } - an object that contains the required for parsing field + * @param key { string} - name of the field to be parsed + * + * @internal + */ +export const getParsedValue = (data: any, key: string) => { + try { + return data[key] ? JSON.parse(data[key]) : undefined; + } catch (e) { + throw new Error(`Unable to parse ${key} argument string`); + } +}; From 4f66dfd661b5bf5e279d07486703a793dbed9724 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 5 May 2020 10:20:38 +0200 Subject: [PATCH 05/75] [ML] Transforms: Single Column Wizard. (#64436) Rearranges the layout of the transform wizard pivot configuration step into a single-column. This allows us to have the data grids for source index and pivot preview having the full width. The advanced editors for source query and pivot configuration also cover a wider width. --- .../components/data_grid/data_grid.tsx | 269 ++-- .../public/app/common/request.test.ts | 2 +- .../transform/public/app/common/request.ts | 2 +- .../public/app/hooks/use_pivot_data.ts | 3 +- .../advanced_pivot_editor.tsx | 69 + .../components/advanced_pivot_editor/index.ts | 7 + .../advanced_pivot_editor_switch.tsx | 69 + .../advanced_pivot_editor_switch/index.ts | 7 + .../advanced_query_editor_switch.tsx | 76 ++ .../advanced_query_editor_switch/index.ts | 7 + .../advanced_source_editor.tsx | 59 + .../advanced_source_editor/index.ts | 7 + .../aggregation_dropdown/dropdown.tsx | 1 + .../components/pivot_configuration/index.ts | 7 + .../pivot_configuration.tsx | 89 ++ .../components/source_search_bar/index.ts | 7 + .../source_search_bar/source_search_bar.tsx | 68 + .../apply_transform_config_to_define_state.ts | 71 + .../step_define/{ => common}/common.test.ts | 4 +- .../step_define/common/constants.ts | 11 + .../get_agg_name_conflict_toast_messages.ts | 100 ++ .../common/get_default_aggregation_config.ts | 37 + .../common/get_default_group_by_config.ts | 44 + .../common/get_default_step_define_state.ts | 25 + .../get_pivot_dropdown_options.ts} | 79 +- .../components/step_define/common/index.ts | 19 + .../components/step_define/common/types.ts | 34 + .../hooks/use_advanced_pivot_editor.ts | 76 ++ .../hooks/use_advanced_source_editor.ts | 87 ++ .../step_define/hooks/use_pivot_config.ts | 144 ++ .../step_define/hooks/use_search_bar.ts | 90 ++ .../step_define/hooks/use_step_define_form.ts | 87 ++ .../components/step_define/index.ts | 7 +- .../step_define/step_define_form.test.tsx | 3 +- .../step_define/step_define_form.tsx | 1160 ++++------------- .../step_define/step_define_summary.test.tsx | 2 +- .../step_define/step_define_summary.tsx | 152 +-- .../components/switch_modal/index.ts | 7 + .../switch_modal.tsx | 0 .../components/wizard/wizard.tsx | 4 +- .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - x-pack/run_functional_tests.sh | 3 + .../apps/transform/creation_index_pattern.ts | 4 +- .../services/transform_ui/wizard.ts | 11 +- 45 files changed, 1823 insertions(+), 1197 deletions(-) create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor_switch/advanced_pivot_editor_switch.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor_switch/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_query_editor_switch/advanced_query_editor_switch.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_query_editor_switch/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/advanced_source_editor.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts rename x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/{ => common}/common.test.ts (95%) create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/constants.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_name_conflict_toast_messages.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_group_by_config.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts rename x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/{common.ts => common/get_pivot_dropdown_options.ts} (64%) create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_source_editor.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/switch_modal/index.ts rename x-pack/plugins/transform/public/app/sections/create_transform/components/{step_define => switch_modal}/switch_modal.tsx (100%) create mode 100755 x-pack/run_functional_tests.sh diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index a5b301902cc75..aeb774a224021 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, FC } from 'react'; +import { isEqual } from 'lodash'; +import React, { memo, useEffect, FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -50,132 +51,154 @@ function isWithHeader(arg: any): arg is PropsWithHeader { type Props = PropsWithHeader | PropsWithoutHeader; -export const DataGrid: FC = props => { - const { - columns, - dataTestSubj, - errorMessage, - invalidSortingColumnns, - noDataMessage, - onChangeItemsPerPage, - onChangePage, - onSort, - pagination, - setVisibleColumns, - renderCellValue, - rowCount, - sortingColumns, - status, - tableItems: data, - toastNotifications, - visibleColumns, - } = props; - - useEffect(() => { - if (invalidSortingColumnns.length > 0) { - invalidSortingColumnns.forEach(columnId => { - toastNotifications.addDanger( - i18n.translate('xpack.ml.dataGrid.invalidSortingColumnError', { - defaultMessage: `The column '{columnId}' cannot be used for sorting.`, - values: { columnId }, - }) - ); - }); - } - }, [invalidSortingColumnns, toastNotifications]); - - if (status === INDEX_STATUS.LOADED && data.length === 0) { - return ( -
- {isWithHeader(props) && } - -

- {i18n.translate('xpack.ml.dataGrid.IndexNoDataCalloutBody', { - defaultMessage: - 'The query for the index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', +export const DataGrid: FC = memo( + props => { + const { + columns, + dataTestSubj, + errorMessage, + invalidSortingColumnns, + noDataMessage, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + setVisibleColumns, + renderCellValue, + rowCount, + sortingColumns, + status, + tableItems: data, + toastNotifications, + visibleColumns, + } = props; + + useEffect(() => { + if (invalidSortingColumnns.length > 0) { + invalidSortingColumnns.forEach(columnId => { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataGrid.invalidSortingColumnError', { + defaultMessage: `The column '{columnId}' cannot be used for sorting.`, + values: { columnId }, + }) + ); + }); + } + }, [invalidSortingColumnns, toastNotifications]); + + if (status === INDEX_STATUS.LOADED && data.length === 0) { + return ( +

+ {isWithHeader(props) && } + - -
- ); - } + color="primary" + > +

+ {i18n.translate('xpack.ml.dataGrid.IndexNoDataCalloutBody', { + defaultMessage: + 'The query for the index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', + })} +

+
+
+ ); + } - if (noDataMessage !== '') { - return ( -
- {isWithHeader(props) && } - -

{noDataMessage}

-
-
- ); - } - - return ( -
- {isWithHeader(props) && ( - - - - - - - {(copy: () => void) => ( - - )} - - - - )} - {status === INDEX_STATUS.ERROR && ( -
+ if (noDataMessage !== '') { + return ( +
+ {isWithHeader(props) && } - - {errorMessage} - +

{noDataMessage}

-
- )} - -
- ); -}; + ); + } + + return ( +
+ {isWithHeader(props) && ( + + + + + + + {(copy: () => void) => ( + + )} + + + + )} + {status === INDEX_STATUS.ERROR && ( +
+ + + {errorMessage} + + + +
+ )} + +
+ ); + }, + (prevProps, nextProps) => isEqual(pickProps(prevProps), pickProps(nextProps)) +); + +function pickProps(props: Props) { + return [ + props.columns, + props.dataTestSubj, + props.errorMessage, + props.invalidSortingColumnns, + props.noDataMessage, + props.pagination, + props.rowCount, + props.sortingColumns, + props.status, + props.tableItems, + props.visibleColumns, + ...(isWithHeader(props) + ? [props.copyToClipboard, props.copyToClipboardDescription, props.title] + : []), + ]; +} diff --git a/x-pack/plugins/transform/public/app/common/request.test.ts b/x-pack/plugins/transform/public/app/common/request.test.ts index 4c3fba3bbf8dd..63f1f8b10ad44 100644 --- a/x-pack/plugins/transform/public/app/common/request.test.ts +++ b/x-pack/plugins/transform/public/app/common/request.test.ts @@ -6,7 +6,7 @@ import { PivotGroupByConfig } from '../common'; -import { StepDefineExposedState } from '../sections/create_transform/components/step_define/step_define_form'; +import { StepDefineExposedState } from '../sections/create_transform/components/step_define'; import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form'; import { PIVOT_SUPPORTED_GROUP_BY_AGGS } from './pivot_group_by'; diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts index 7e965dbe802c0..1a69e9f6476b9 100644 --- a/x-pack/plugins/transform/public/app/common/request.ts +++ b/x-pack/plugins/transform/public/app/common/request.ts @@ -9,7 +9,7 @@ import { DefaultOperator } from 'elasticsearch'; import { dictionaryToArray } from '../../../common/types/common'; import { SavedSearchQuery } from '../hooks/use_search_items'; -import { StepDefineExposedState } from '../sections/create_transform/components/step_define/step_define_form'; +import { StepDefineExposedState } from '../sections/create_transform/components/step_define'; import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form'; import { IndexPattern } from '../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts index ff7ca5d42b5f7..853540e19ea6f 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -123,8 +123,6 @@ export const usePivotData = ( tableItems, } = dataGrid; - const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); - const getPreviewData = async () => { if (aggsArr.length === 0 || groupByArr.length === 0) { setTableItems([]); @@ -142,6 +140,7 @@ export const usePivotData = ( setStatus(INDEX_STATUS.LOADING); try { + const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); const resp = await api.getTransformsPreview(previewRequest); setTableItems(resp.preview); setRowCount(resp.preview.length); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx new file mode 100644 index 0000000000000..983d36a20e87f --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual } from 'lodash'; +import React, { memo, FC } from 'react'; + +import { EuiCodeEditor, EuiFormRow } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { StepDefineFormHook } from '../step_define'; + +export const AdvancedPivotEditor: FC = memo( + ({ + actions: { convertToJson, setAdvancedEditorConfig, setAdvancedPivotEditorApplyButtonEnabled }, + state: { advancedEditorConfigLastApplied, advancedEditorConfig, xJsonMode }, + }) => { + return ( + + { + setAdvancedEditorConfig(d); + + // Disable the "Apply"-Button if the config hasn't changed. + if (advancedEditorConfigLastApplied === d) { + setAdvancedPivotEditorApplyButtonEnabled(false); + return; + } + + // Try to parse the string passed on from the editor. + // If parsing fails, the "Apply"-Button will be disabled + try { + JSON.parse(convertToJson(d)); + setAdvancedPivotEditorApplyButtonEnabled(true); + } catch (e) { + setAdvancedPivotEditorApplyButtonEnabled(false); + } + }} + setOptions={{ + fontSize: '12px', + }} + theme="textmate" + aria-label={i18n.translate('xpack.transform.stepDefineForm.advancedEditorAriaLabel', { + defaultMessage: 'Advanced pivot editor', + })} + /> + + ); + }, + (prevProps, nextProps) => isEqual(pickProps(prevProps), pickProps(nextProps)) +); + +function pickProps(props: StepDefineFormHook['advancedPivotEditor']) { + return [props.state.advancedEditorConfigLastApplied, props.state.advancedEditorConfig]; +} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/index.ts new file mode 100644 index 0000000000000..340f7d37ba93b --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AdvancedPivotEditor } from './advanced_pivot_editor'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor_switch/advanced_pivot_editor_switch.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor_switch/advanced_pivot_editor_switch.tsx new file mode 100644 index 0000000000000..ce155c58bc37c --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor_switch/advanced_pivot_editor_switch.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSwitch } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { SwitchModal } from '../switch_modal'; + +import { StepDefineFormHook } from '../step_define'; + +export const AdvancedPivotEditorSwitch: FC = ({ + advancedPivotEditor: { + actions: { setAdvancedEditorSwitchModalVisible, toggleAdvancedEditor }, + state: { + advancedEditorConfig, + advancedEditorConfigLastApplied, + isAdvancedEditorSwitchModalVisible, + isAdvancedPivotEditorEnabled, + isAdvancedPivotEditorApplyButtonEnabled, + }, + }, + pivotConfig: { + actions: { setAggList, setGroupByList }, + }, +}) => { + return ( + + + + { + if ( + isAdvancedPivotEditorEnabled && + (isAdvancedPivotEditorApplyButtonEnabled || + advancedEditorConfig !== advancedEditorConfigLastApplied) + ) { + setAdvancedEditorSwitchModalVisible(true); + return; + } + + toggleAdvancedEditor(); + }} + data-test-subj="transformAdvancedPivotEditorSwitch" + /> + {isAdvancedEditorSwitchModalVisible && ( + setAdvancedEditorSwitchModalVisible(false)} + onConfirm={() => { + setAdvancedEditorSwitchModalVisible(false); + toggleAdvancedEditor(); + }} + type={'pivot'} + /> + )} + + + + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor_switch/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor_switch/index.ts new file mode 100644 index 0000000000000..377f54e12c03b --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor_switch/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AdvancedPivotEditorSwitch } from './advanced_pivot_editor_switch'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_query_editor_switch/advanced_query_editor_switch.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_query_editor_switch/advanced_query_editor_switch.tsx new file mode 100644 index 0000000000000..66234b8cc2007 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_query_editor_switch/advanced_query_editor_switch.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiSwitch } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { SwitchModal } from '../switch_modal'; +import { defaultSearch } from '../step_define'; + +import { StepDefineFormHook } from '../step_define'; + +export const AdvancedQueryEditorSwitch: FC = ({ + advancedSourceEditor: { + actions: { + setAdvancedSourceEditorSwitchModalVisible, + setSourceConfigUpdated, + toggleAdvancedSourceEditor, + }, + state: { + isAdvancedSourceEditorEnabled, + isAdvancedSourceEditorSwitchModalVisible, + sourceConfigUpdated, + }, + }, + searchBar: { + actions: { setSearchQuery }, + }, +}) => { + // If switching to KQL after updating via editor - reset search + const toggleEditorHandler = (reset = false) => { + if (reset === true) { + setSearchQuery(defaultSearch); + setSourceConfigUpdated(false); + } + toggleAdvancedSourceEditor(reset); + }; + + return ( + <> + { + if (isAdvancedSourceEditorEnabled && sourceConfigUpdated) { + setAdvancedSourceEditorSwitchModalVisible(true); + return; + } + + toggleEditorHandler(); + }} + data-test-subj="transformAdvancedQueryEditorSwitch" + /> + {isAdvancedSourceEditorSwitchModalVisible && ( + setAdvancedSourceEditorSwitchModalVisible(false)} + onConfirm={() => { + setAdvancedSourceEditorSwitchModalVisible(false); + toggleEditorHandler(true); + }} + type={'source'} + /> + )} + + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_query_editor_switch/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_query_editor_switch/index.ts new file mode 100644 index 0000000000000..36474e99c66ba --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_query_editor_switch/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AdvancedQueryEditorSwitch } from './advanced_query_editor_switch'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/advanced_source_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/advanced_source_editor.tsx new file mode 100644 index 0000000000000..fecf4972330e1 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/advanced_source_editor.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiCodeEditor } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { StepDefineFormHook } from '../step_define'; + +export const AdvancedSourceEditor: FC = ({ + searchBar: { + actions: { setSearchString }, + }, + advancedSourceEditor: { + actions: { setAdvancedEditorSourceConfig, setAdvancedSourceEditorApplyButtonEnabled }, + state: { advancedEditorSourceConfig, advancedEditorSourceConfigLastApplied }, + }, +}) => { + return ( + { + setSearchString(undefined); + setAdvancedEditorSourceConfig(d); + + // Disable the "Apply"-Button if the config hasn't changed. + if (advancedEditorSourceConfigLastApplied === d) { + setAdvancedSourceEditorApplyButtonEnabled(false); + return; + } + + // Try to parse the string passed on from the editor. + // If parsing fails, the "Apply"-Button will be disabled + try { + JSON.parse(d); + setAdvancedSourceEditorApplyButtonEnabled(true); + } catch (e) { + setAdvancedSourceEditorApplyButtonEnabled(false); + } + }} + setOptions={{ + fontSize: '12px', + }} + theme="textmate" + aria-label={i18n.translate('xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel', { + defaultMessage: 'Advanced query editor', + })} + /> + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/index.ts new file mode 100644 index 0000000000000..8f5c88c5b44eb --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AdvancedSourceEditor } from './advanced_source_editor'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx index 157e0f76856c8..e5381f09713b5 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx @@ -23,6 +23,7 @@ export const DropDown: React.FC = ({ }) => { return ( = memo( + ({ + actions: { + addAggregation, + addGroupBy, + deleteAggregation, + deleteGroupBy, + updateAggregation, + updateGroupBy, + }, + state: { aggList, aggOptions, aggOptionsData, groupByList, groupByOptions, groupByOptionsData }, + }) => { + return ( + <> + + <> + + + + + + + <> + + + + + + ); + }, + (prevProps, nextProps) => { + return isEqual(prevProps.state, nextProps.state); + } +); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/index.ts new file mode 100644 index 0000000000000..4e1cf81eef98e --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SourceSearchBar } from './source_search_bar'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx new file mode 100644 index 0000000000000..a8e1bf3552c80 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiCode, EuiInputPopover } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { QueryStringInput } from '../../../../../../../../../src/plugins/data/public'; + +import { SearchItems } from '../../../../hooks/use_search_items'; + +import { StepDefineFormHook, QUERY_LANGUAGE_KUERY } from '../step_define'; + +interface SourceSearchBarProps { + indexPattern: SearchItems['indexPattern']; + searchBar: StepDefineFormHook['searchBar']; +} +export const SourceSearchBar: FC = ({ indexPattern, searchBar }) => { + const { + actions: { searchChangeHandler, searchSubmitHandler, setErrorMessage }, + state: { errorMessage, searchInput }, + } = searchBar; + + return ( + setErrorMessage(undefined)} + input={ + + } + isOpen={errorMessage?.query === searchInput.query && errorMessage?.message !== ''} + > + + {i18n.translate('xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar', { + defaultMessage: 'Invalid query: {errorMessage}', + values: { + errorMessage: errorMessage?.message.split('\n')[0], + }, + })} + + + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts new file mode 100644 index 0000000000000..bda1efe97837f --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual } from 'lodash'; + +import { + matchAllQuery, + PivotAggsConfig, + PivotAggsConfigDict, + PivotGroupByConfig, + PivotGroupByConfigDict, + TransformPivotConfig, + PIVOT_SUPPORTED_AGGS, + PIVOT_SUPPORTED_GROUP_BY_AGGS, +} from '../../../../../common'; +import { Dictionary } from '../../../../../../../common/types/common'; + +import { StepDefineExposedState } from './types'; + +export function applyTransformConfigToDefineState( + state: StepDefineExposedState, + transformConfig?: TransformPivotConfig +): StepDefineExposedState { + // apply the transform configuration to wizard DEFINE state + if (transformConfig !== undefined) { + // transform aggregations config to wizard state + state.aggList = Object.keys(transformConfig.pivot.aggregations).reduce((aggList, aggName) => { + const aggConfig = transformConfig.pivot.aggregations[aggName] as Dictionary; + const agg = Object.keys(aggConfig)[0]; + aggList[aggName] = { + ...aggConfig[agg], + agg: agg as PIVOT_SUPPORTED_AGGS, + aggName, + dropDownName: aggName, + } as PivotAggsConfig; + return aggList; + }, {} as PivotAggsConfigDict); + + // transform group by config to wizard state + state.groupByList = Object.keys(transformConfig.pivot.group_by).reduce( + (groupByList, groupByName) => { + const groupByConfig = transformConfig.pivot.group_by[groupByName] as Dictionary; + const groupBy = Object.keys(groupByConfig)[0]; + groupByList[groupByName] = { + agg: groupBy as PIVOT_SUPPORTED_GROUP_BY_AGGS, + aggName: groupByName, + dropDownName: groupByName, + ...groupByConfig[groupBy], + } as PivotGroupByConfig; + return groupByList; + }, + {} as PivotGroupByConfigDict + ); + + // only apply the query from the transform config to wizard state if it's not the default query + const query = transformConfig.source.query; + if (query !== undefined && !isEqual(query, matchAllQuery)) { + state.isAdvancedSourceEditorEnabled = true; + state.searchQuery = query; + state.sourceConfigUpdated = true; + } + + // applying a transform config to wizard state will always result in a valid configuration + state.valid = true; + } + + return state; +} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts similarity index 95% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts index 58ab4a1b8ac33..4fac3dce3de44 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getPivotDropdownOptions } from './common'; -import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { getPivotDropdownOptions } from '../common'; +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; describe('Transform: Define Pivot Common', () => { test('getPivotDropdownOptions()', () => { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/constants.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/constants.ts new file mode 100644 index 0000000000000..4eefa7c94464d --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/constants.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const defaultSearch = '*'; + +export const QUERY_LANGUAGE_KUERY = 'kuery'; +export const QUERY_LANGUAGE_LUCENE = 'lucene'; +export type QUERY_LANGUAGE = 'kuery' | 'lucene'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_name_conflict_toast_messages.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_name_conflict_toast_messages.ts new file mode 100644 index 0000000000000..cad3ab8c71a22 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_name_conflict_toast_messages.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { AggName, PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common'; + +export function getAggNameConflictToastMessages( + aggName: AggName, + aggList: PivotAggsConfigDict, + groupByList: PivotGroupByConfigDict +): string[] { + if (aggList[aggName] !== undefined) { + return [ + i18n.translate('xpack.transform.stepDefineForm.aggExistsErrorMessage', { + defaultMessage: `An aggregation configuration with the name '{aggName}' already exists.`, + values: { aggName }, + }), + ]; + } + + if (groupByList[aggName] !== undefined) { + return [ + i18n.translate('xpack.transform.stepDefineForm.groupByExistsErrorMessage', { + defaultMessage: `A group by configuration with the name '{aggName}' already exists.`, + values: { aggName }, + }), + ]; + } + + const conflicts: string[] = []; + + // check the new aggName against existing aggs and groupbys + const aggNameSplit = aggName.split('.'); + let aggNameCheck: string; + aggNameSplit.forEach(aggNamePart => { + aggNameCheck = aggNameCheck === undefined ? aggNamePart : `${aggNameCheck}.${aggNamePart}`; + if (aggList[aggNameCheck] !== undefined || groupByList[aggNameCheck] !== undefined) { + conflicts.push( + i18n.translate('xpack.transform.stepDefineForm.nestedConflictErrorMessage', { + defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{aggNameCheck}'.`, + values: { aggName, aggNameCheck }, + }) + ); + } + }); + + if (conflicts.length > 0) { + return conflicts; + } + + // check all aggs against new aggName + aggListNameLoop: for (const aggListName of Object.keys(aggList)) { + const aggListNameSplit = aggListName.split('.'); + let aggListNameCheck: string | undefined; + for (const aggListNamePart of aggListNameSplit) { + aggListNameCheck = + aggListNameCheck === undefined ? aggListNamePart : `${aggListNameCheck}.${aggListNamePart}`; + if (aggListNameCheck === aggName) { + conflicts.push( + i18n.translate('xpack.transform.stepDefineForm.nestedAggListConflictErrorMessage', { + defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{aggListName}'.`, + values: { aggName, aggListName }, + }) + ); + break aggListNameLoop; + } + } + } + + if (conflicts.length > 0) { + return conflicts; + } + + // check all group-bys against new aggName + groupByListNameLoop: for (const groupByListName of Object.keys(groupByList)) { + const groupByListNameSplit = groupByListName.split('.'); + let groupByListNameCheck: string | undefined; + for (const groupByListNamePart of groupByListNameSplit) { + groupByListNameCheck = + groupByListNameCheck === undefined + ? groupByListNamePart + : `${groupByListNameCheck}.${groupByListNamePart}`; + if (groupByListNameCheck === aggName) { + conflicts.push( + i18n.translate('xpack.transform.stepDefineForm.nestedGroupByListConflictErrorMessage', { + defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{groupByListName}'.`, + values: { aggName, groupByListName }, + }) + ); + break groupByListNameLoop; + } + } + } + + return conflicts; +} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts new file mode 100644 index 0000000000000..263a8954c96eb --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EsFieldName, + PERCENTILES_AGG_DEFAULT_PERCENTS, + PivotAggsConfigWithUiSupport, + PIVOT_SUPPORTED_AGGS, +} from '../../../../../common'; + +export function getDefaultAggregationConfig( + aggName: string, + dropDownName: string, + fieldName: EsFieldName, + agg: PIVOT_SUPPORTED_AGGS +): PivotAggsConfigWithUiSupport { + switch (agg) { + case PIVOT_SUPPORTED_AGGS.PERCENTILES: + return { + agg, + aggName, + dropDownName, + field: fieldName, + percents: PERCENTILES_AGG_DEFAULT_PERCENTS, + }; + default: + return { + agg, + aggName, + dropDownName, + field: fieldName, + }; + } +} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_group_by_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_group_by_config.ts new file mode 100644 index 0000000000000..712a745ff6e77 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_group_by_config.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EsFieldName, + GroupByConfigWithUiSupport, + PIVOT_SUPPORTED_GROUP_BY_AGGS, +} from '../../../../../common'; + +export function getDefaultGroupByConfig( + aggName: string, + dropDownName: string, + fieldName: EsFieldName, + groupByAgg: PIVOT_SUPPORTED_GROUP_BY_AGGS +): GroupByConfigWithUiSupport { + switch (groupByAgg) { + case PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS: + return { + agg: groupByAgg, + aggName, + dropDownName, + field: fieldName, + }; + case PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM: + return { + agg: groupByAgg, + aggName, + dropDownName, + field: fieldName, + interval: '10', + }; + case PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM: + return { + agg: groupByAgg, + aggName, + dropDownName, + field: fieldName, + calendar_interval: '1m', + }; + } +} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts new file mode 100644 index 0000000000000..30e1659e9e6ab --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common'; +import { SearchItems } from '../../../../../hooks/use_search_items'; + +import { defaultSearch, QUERY_LANGUAGE_KUERY } from './constants'; +import { StepDefineExposedState } from './types'; + +export function getDefaultStepDefineState(searchItems: SearchItems): StepDefineExposedState { + return { + aggList: {} as PivotAggsConfigDict, + groupByList: {} as PivotGroupByConfigDict, + isAdvancedPivotEditorEnabled: false, + isAdvancedSourceEditorEnabled: false, + searchLanguage: QUERY_LANGUAGE_KUERY, + searchString: undefined, + searchQuery: searchItems.savedSearch !== undefined ? searchItems.combinedQuery : defaultSearch, + sourceConfigUpdated: false, + valid: false, + }; +} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts similarity index 64% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts index 65cea40276da9..f916afa921c12 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts @@ -3,87 +3,26 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { get } from 'lodash'; import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { IndexPattern, KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/public'; +import { + IndexPattern, + KBN_FIELD_TYPES, +} from '../../../../../../../../../../src/plugins/data/public'; import { DropDownLabel, DropDownOption, - EsFieldName, - GroupByConfigWithUiSupport, - PERCENTILES_AGG_DEFAULT_PERCENTS, - PivotAggsConfigWithUiSupport, PivotAggsConfigWithUiSupportDict, pivotAggsFieldSupport, PivotGroupByConfigWithUiSupportDict, pivotGroupByFieldSupport, - PIVOT_SUPPORTED_AGGS, - PIVOT_SUPPORTED_GROUP_BY_AGGS, -} from '../../../../common'; - -export interface Field { - name: EsFieldName; - type: KBN_FIELD_TYPES; -} +} from '../../../../../common'; -function getDefaultGroupByConfig( - aggName: string, - dropDownName: string, - fieldName: EsFieldName, - groupByAgg: PIVOT_SUPPORTED_GROUP_BY_AGGS -): GroupByConfigWithUiSupport { - switch (groupByAgg) { - case PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS: - return { - agg: groupByAgg, - aggName, - dropDownName, - field: fieldName, - }; - case PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM: - return { - agg: groupByAgg, - aggName, - dropDownName, - field: fieldName, - interval: '10', - }; - case PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM: - return { - agg: groupByAgg, - aggName, - dropDownName, - field: fieldName, - calendar_interval: '1m', - }; - } -} - -function getDefaultAggregationConfig( - aggName: string, - dropDownName: string, - fieldName: EsFieldName, - agg: PIVOT_SUPPORTED_AGGS -): PivotAggsConfigWithUiSupport { - switch (agg) { - case PIVOT_SUPPORTED_AGGS.PERCENTILES: - return { - agg, - aggName, - dropDownName, - field: fieldName, - percents: PERCENTILES_AGG_DEFAULT_PERCENTS, - }; - default: - return { - agg, - aggName, - dropDownName, - field: fieldName, - }; - } -} +import { getDefaultAggregationConfig } from './get_default_aggregation_config'; +import { getDefaultGroupByConfig } from './get_default_group_by_config'; +import { Field } from './types'; const illegalEsAggNameChars = /[[\]>]/g; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/index.ts new file mode 100644 index 0000000000000..af351759c84d1 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + defaultSearch, + QUERY_LANGUAGE, + QUERY_LANGUAGE_KUERY, + QUERY_LANGUAGE_LUCENE, +} from './constants'; +export { applyTransformConfigToDefineState } from './apply_transform_config_to_define_state'; +export { getAggNameConflictToastMessages } from './get_agg_name_conflict_toast_messages'; +export { getDefaultAggregationConfig } from './get_default_aggregation_config'; +export { getDefaultGroupByConfig } from './get_default_group_by_config'; +export { getDefaultStepDefineState } from './get_default_step_define_state'; +export { getPivotDropdownOptions } from './get_pivot_dropdown_options'; +export { ErrorMessage, Field, StepDefineExposedState } from './types'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts new file mode 100644 index 0000000000000..56fde98cd4c71 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KBN_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; + +import { EsFieldName, PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common'; +import { SavedSearchQuery } from '../../../../../hooks/use_search_items'; + +import { QUERY_LANGUAGE } from './constants'; + +export interface ErrorMessage { + query: string; + message: string; +} + +export interface Field { + name: EsFieldName; + type: KBN_FIELD_TYPES; +} + +export interface StepDefineExposedState { + aggList: PivotAggsConfigDict; + groupByList: PivotGroupByConfigDict; + isAdvancedPivotEditorEnabled: boolean; + isAdvancedSourceEditorEnabled: boolean; + searchLanguage: QUERY_LANGUAGE; + searchString: string | undefined; + searchQuery: string | SavedSearchQuery; + sourceConfigUpdated: boolean; + valid: boolean; +} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts new file mode 100644 index 0000000000000..2e92114286599 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { useXJsonMode } from '../../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; + +import { PreviewRequestBody } from '../../../../../common'; + +import { StepDefineExposedState } from '../common'; + +export const useAdvancedPivotEditor = ( + defaults: StepDefineExposedState, + previewRequest: PreviewRequestBody +) => { + const stringifiedPivotConfig = JSON.stringify(previewRequest.pivot, null, 2); + + // Advanced editor for pivot config state + const [isAdvancedEditorSwitchModalVisible, setAdvancedEditorSwitchModalVisible] = useState(false); + + const [ + isAdvancedPivotEditorApplyButtonEnabled, + setAdvancedPivotEditorApplyButtonEnabled, + ] = useState(false); + + const [isAdvancedPivotEditorEnabled, setAdvancedPivotEditorEnabled] = useState( + defaults.isAdvancedPivotEditorEnabled + ); + + const [advancedEditorConfigLastApplied, setAdvancedEditorConfigLastApplied] = useState( + stringifiedPivotConfig + ); + + const { + convertToJson, + setXJson: setAdvancedEditorConfig, + xJson: advancedEditorConfig, + xJsonMode, + } = useXJsonMode(stringifiedPivotConfig); + + useEffect(() => { + setAdvancedEditorConfig(stringifiedPivotConfig); + }, [setAdvancedEditorConfig, stringifiedPivotConfig]); + + const toggleAdvancedEditor = () => { + setAdvancedEditorConfig(advancedEditorConfig); + setAdvancedPivotEditorEnabled(!isAdvancedPivotEditorEnabled); + setAdvancedPivotEditorApplyButtonEnabled(false); + if (isAdvancedPivotEditorEnabled === false) { + setAdvancedEditorConfigLastApplied(advancedEditorConfig); + } + }; + + return { + actions: { + convertToJson, + setAdvancedEditorConfig, + setAdvancedEditorConfigLastApplied, + setAdvancedEditorSwitchModalVisible, + setAdvancedPivotEditorApplyButtonEnabled, + setAdvancedPivotEditorEnabled, + toggleAdvancedEditor, + }, + state: { + advancedEditorConfig, + advancedEditorConfigLastApplied, + isAdvancedEditorSwitchModalVisible, + isAdvancedPivotEditorApplyButtonEnabled, + isAdvancedPivotEditorEnabled, + xJsonMode, + }, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_source_editor.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_source_editor.ts new file mode 100644 index 0000000000000..1ea8a45248fb9 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_source_editor.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +import { PreviewRequestBody } from '../../../../../common'; + +import { StepDefineExposedState } from '../common'; + +export const useAdvancedSourceEditor = ( + defaults: StepDefineExposedState, + previewRequest: PreviewRequestBody +) => { + const stringifiedSourceConfig = JSON.stringify(previewRequest.source.query, null, 2); + + // Advanced editor for source config state + const [sourceConfigUpdated, setSourceConfigUpdated] = useState(defaults.sourceConfigUpdated); + + const [ + isAdvancedSourceEditorSwitchModalVisible, + setAdvancedSourceEditorSwitchModalVisible, + ] = useState(false); + + const [isAdvancedSourceEditorEnabled, setAdvancedSourceEditorEnabled] = useState( + defaults.isAdvancedSourceEditorEnabled + ); + + const [ + isAdvancedSourceEditorApplyButtonEnabled, + setAdvancedSourceEditorApplyButtonEnabled, + ] = useState(false); + + const [ + advancedEditorSourceConfigLastApplied, + setAdvancedEditorSourceConfigLastApplied, + ] = useState(stringifiedSourceConfig); + + const [advancedEditorSourceConfig, setAdvancedEditorSourceConfig] = useState( + stringifiedSourceConfig + ); + + const applyAdvancedSourceEditorChanges = () => { + const sourceConfig = JSON.parse(advancedEditorSourceConfig); + const prettySourceConfig = JSON.stringify(sourceConfig, null, 2); + setSourceConfigUpdated(true); + setAdvancedEditorSourceConfig(prettySourceConfig); + setAdvancedEditorSourceConfigLastApplied(prettySourceConfig); + setAdvancedSourceEditorApplyButtonEnabled(false); + }; + + // If switching to KQL after updating via editor - reset search + const toggleAdvancedSourceEditor = (reset = false) => { + if (reset === true) { + setSourceConfigUpdated(false); + } + if (isAdvancedSourceEditorEnabled === false) { + setAdvancedEditorSourceConfigLastApplied(advancedEditorSourceConfig); + } + + setAdvancedSourceEditorEnabled(!isAdvancedSourceEditorEnabled); + setAdvancedSourceEditorApplyButtonEnabled(false); + }; + + return { + actions: { + applyAdvancedSourceEditorChanges, + setAdvancedSourceEditorApplyButtonEnabled, + setAdvancedSourceEditorEnabled, + setAdvancedEditorSourceConfig, + setAdvancedEditorSourceConfigLastApplied, + setAdvancedSourceEditorSwitchModalVisible, + setSourceConfigUpdated, + toggleAdvancedSourceEditor, + }, + state: { + advancedEditorSourceConfig, + advancedEditorSourceConfigLastApplied, + isAdvancedSourceEditorApplyButtonEnabled, + isAdvancedSourceEditorEnabled, + isAdvancedSourceEditorSwitchModalVisible, + sourceConfigUpdated, + }, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts new file mode 100644 index 0000000000000..70886e41fef4e --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +import { dictionaryToArray } from '../../../../../../../common/types/common'; + +import { useToastNotifications } from '../../../../../app_dependencies'; +import { AggName, DropDownLabel, PivotAggsConfig, PivotGroupByConfig } from '../../../../../common'; + +import { + getAggNameConflictToastMessages, + getPivotDropdownOptions, + StepDefineExposedState, +} from '../common'; +import { StepDefineFormProps } from '../step_define_form'; + +export const usePivotConfig = ( + defaults: StepDefineExposedState, + indexPattern: StepDefineFormProps['searchItems']['indexPattern'] +) => { + const toastNotifications = useToastNotifications(); + + const { + aggOptions, + aggOptionsData, + groupByOptions, + groupByOptionsData, + } = getPivotDropdownOptions(indexPattern); + + // The list of selected group by fields + const [groupByList, setGroupByList] = useState(defaults.groupByList); + + const addGroupBy = (d: DropDownLabel[]) => { + const label: AggName = d[0].label; + const config: PivotGroupByConfig = groupByOptionsData[label]; + const aggName: AggName = config.aggName; + + const aggNameConflictMessages = getAggNameConflictToastMessages(aggName, aggList, groupByList); + if (aggNameConflictMessages.length > 0) { + aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); + return; + } + + groupByList[aggName] = config; + setGroupByList({ ...groupByList }); + }; + + const updateGroupBy = (previousAggName: AggName, item: PivotGroupByConfig) => { + const groupByListWithoutPrevious = { ...groupByList }; + delete groupByListWithoutPrevious[previousAggName]; + + const aggNameConflictMessages = getAggNameConflictToastMessages( + item.aggName, + aggList, + groupByListWithoutPrevious + ); + if (aggNameConflictMessages.length > 0) { + aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); + return; + } + + groupByListWithoutPrevious[item.aggName] = item; + setGroupByList(groupByListWithoutPrevious); + }; + + const deleteGroupBy = (aggName: AggName) => { + delete groupByList[aggName]; + setGroupByList({ ...groupByList }); + }; + + // The list of selected aggregations + const [aggList, setAggList] = useState(defaults.aggList); + + const addAggregation = (d: DropDownLabel[]) => { + const label: AggName = d[0].label; + const config: PivotAggsConfig = aggOptionsData[label]; + const aggName: AggName = config.aggName; + + const aggNameConflictMessages = getAggNameConflictToastMessages(aggName, aggList, groupByList); + if (aggNameConflictMessages.length > 0) { + aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); + return; + } + + aggList[aggName] = config; + setAggList({ ...aggList }); + }; + + const updateAggregation = (previousAggName: AggName, item: PivotAggsConfig) => { + const aggListWithoutPrevious = { ...aggList }; + delete aggListWithoutPrevious[previousAggName]; + + const aggNameConflictMessages = getAggNameConflictToastMessages( + item.aggName, + aggListWithoutPrevious, + groupByList + ); + if (aggNameConflictMessages.length > 0) { + aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); + return; + } + + aggListWithoutPrevious[item.aggName] = item; + setAggList(aggListWithoutPrevious); + }; + + const deleteAggregation = (aggName: AggName) => { + delete aggList[aggName]; + setAggList({ ...aggList }); + }; + + const pivotAggsArr = dictionaryToArray(aggList); + const pivotGroupByArr = dictionaryToArray(groupByList); + + const valid = pivotGroupByArr.length > 0 && pivotAggsArr.length > 0; + + return { + actions: { + addAggregation, + addGroupBy, + deleteAggregation, + deleteGroupBy, + setAggList, + setGroupByList, + updateAggregation, + updateGroupBy, + }, + state: { + aggList, + aggOptions, + aggOptionsData, + groupByList, + groupByOptions, + groupByOptionsData, + pivotAggsArr, + pivotGroupByArr, + valid, + }, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts new file mode 100644 index 0000000000000..9fff49d300575 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +import { esKuery, esQuery, Query } from '../../../../../../../../../../src/plugins/data/public'; + +import { getPivotQuery } from '../../../../../common'; + +import { + ErrorMessage, + StepDefineExposedState, + QUERY_LANGUAGE_KUERY, + QUERY_LANGUAGE_LUCENE, + QUERY_LANGUAGE, +} from '../common'; + +import { StepDefineFormProps } from '../step_define_form'; + +export const useSearchBar = ( + defaults: StepDefineExposedState, + indexPattern: StepDefineFormProps['searchItems']['indexPattern'] +) => { + // The internal state of the input query bar updated on every key stroke. + const [searchInput, setSearchInput] = useState({ + query: defaults.searchString || '', + language: defaults.searchLanguage, + }); + + // The state of the input query bar updated on every submit and to be exposed. + const [searchLanguage, setSearchLanguage] = useState( + defaults.searchLanguage + ); + + const [searchString, setSearchString] = useState( + defaults.searchString + ); + + const [searchQuery, setSearchQuery] = useState(defaults.searchQuery); + + const [errorMessage, setErrorMessage] = useState(undefined); + + const searchChangeHandler = (query: Query) => setSearchInput(query); + const searchSubmitHandler = (query: Query) => { + setSearchLanguage(query.language as QUERY_LANGUAGE); + setSearchString(query.query !== '' ? (query.query as string) : undefined); + try { + switch (query.language) { + case QUERY_LANGUAGE_KUERY: + setSearchQuery( + esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(query.query as string), + indexPattern + ) + ); + return; + case QUERY_LANGUAGE_LUCENE: + setSearchQuery(esQuery.luceneStringToDsl(query.query as string)); + return; + } + } catch (e) { + setErrorMessage({ query: query.query as string, message: e.message }); + } + }; + + const pivotQuery = getPivotQuery(searchQuery); + + return { + actions: { + searchChangeHandler, + searchSubmitHandler, + setErrorMessage, + setSearchInput, + setSearchLanguage, + setSearchQuery, + setSearchString, + }, + state: { + errorMessage, + pivotQuery, + searchInput, + searchLanguage, + searchQuery, + searchString, + }, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts new file mode 100644 index 0000000000000..fc47a9e3d3477 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect } from 'react'; + +import { getPreviewRequestBody } from '../../../../../common'; + +import { getDefaultStepDefineState } from '../common'; + +import { StepDefineFormProps } from '../step_define_form'; + +import { useAdvancedPivotEditor } from './use_advanced_pivot_editor'; +import { useAdvancedSourceEditor } from './use_advanced_source_editor'; +import { usePivotConfig } from './use_pivot_config'; +import { useSearchBar } from './use_search_bar'; + +export type StepDefineFormHook = ReturnType; + +export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefineFormProps) => { + const defaults = { ...getDefaultStepDefineState(searchItems), ...overrides }; + const { indexPattern } = searchItems; + + const searchBar = useSearchBar(defaults, indexPattern); + const pivotConfig = usePivotConfig(defaults, indexPattern); + + const previewRequest = getPreviewRequestBody( + indexPattern.title, + searchBar.state.pivotQuery, + pivotConfig.state.pivotGroupByArr, + pivotConfig.state.pivotAggsArr + ); + + // pivot config hook + const advancedPivotEditor = useAdvancedPivotEditor(defaults, previewRequest); + + // source config hook + const advancedSourceEditor = useAdvancedSourceEditor(defaults, previewRequest); + + useEffect(() => { + if (!advancedSourceEditor.state.isAdvancedSourceEditorEnabled) { + const previewRequestUpdate = getPreviewRequestBody( + indexPattern.title, + searchBar.state.pivotQuery, + pivotConfig.state.pivotGroupByArr, + pivotConfig.state.pivotAggsArr + ); + + const stringifiedSourceConfigUpdate = JSON.stringify( + previewRequestUpdate.source.query, + null, + 2 + ); + + advancedSourceEditor.actions.setAdvancedEditorSourceConfig(stringifiedSourceConfigUpdate); + } + + onChange({ + aggList: pivotConfig.state.aggList, + groupByList: pivotConfig.state.groupByList, + isAdvancedPivotEditorEnabled: advancedPivotEditor.state.isAdvancedPivotEditorEnabled, + isAdvancedSourceEditorEnabled: advancedSourceEditor.state.isAdvancedSourceEditorEnabled, + searchLanguage: searchBar.state.searchLanguage, + searchString: searchBar.state.searchString, + searchQuery: searchBar.state.searchQuery, + sourceConfigUpdated: advancedSourceEditor.state.sourceConfigUpdated, + valid: pivotConfig.state.valid, + }); + // custom comparison + /* eslint-disable react-hooks/exhaustive-deps */ + }, [ + JSON.stringify(advancedPivotEditor.state), + JSON.stringify(advancedSourceEditor.state), + JSON.stringify(pivotConfig.state), + JSON.stringify(searchBar.state), + /* eslint-enable react-hooks/exhaustive-deps */ + ]); + + return { + advancedPivotEditor, + advancedSourceEditor, + pivotConfig, + searchBar, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts index 881e8c6b26658..b73e281b057e9 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts @@ -5,9 +5,12 @@ */ export { + defaultSearch, applyTransformConfigToDefineState, getDefaultStepDefineState, StepDefineExposedState, - StepDefineForm, -} from './step_define_form'; + QUERY_LANGUAGE_KUERY, +} from './common'; +export { StepDefineFormHook } from './hooks/use_step_define_form'; +export { StepDefineForm } from './step_define_form'; export { StepDefineSummary } from './step_define_summary'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index a15e958c16b73..bcd2900621a59 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -24,7 +24,8 @@ import { } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; -import { StepDefineForm, getAggNameConflictToastMessages } from './step_define_form'; +import { getAggNameConflictToastMessages } from './common'; +import { StepDefineForm } from './step_define_form'; jest.mock('../../../../../shared_imports'); jest.mock('../../../../../app/app_dependencies'); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 0e6e2c1a38d0e..33adc6781c158 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -4,37 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash'; -import React, { Fragment, FC, useEffect, useState } from 'react'; +import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, - EuiCodeEditor, - EuiCode, - EuiInputPopover, + EuiButtonIcon, + EuiCopy, EuiFlexGroup, EuiFlexItem, EuiForm, - EuiFormHelpText, EuiFormRow, EuiHorizontalRule, EuiLink, - EuiPanel, EuiSpacer, - EuiSwitch, + EuiText, } from '@elastic/eui'; -import { - esKuery, - esQuery, - Query, - QueryStringInput, -} from '../../../../../../../../../src/plugins/data/public'; - -import { useXJsonMode } from '../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; - import { DataGrid } from '../../../../../shared_imports'; import { @@ -42,385 +29,62 @@ import { getPivotPreviewDevConsoleStatement, } from '../../../../common/data_grid'; -import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; -import { SavedSearchQuery, SearchItems } from '../../../../hooks/use_search_items'; -import { useIndexData } from '../../../../hooks/use_index_data'; -import { usePivotData } from '../../../../hooks/use_pivot_data'; -import { useToastNotifications } from '../../../../app_dependencies'; -import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common'; import { - getPivotQuery, getPreviewRequestBody, - matchAllQuery, - AggName, - DropDownLabel, PivotAggDict, - PivotAggsConfig, PivotAggsConfigDict, PivotGroupByDict, - PivotGroupByConfig, PivotGroupByConfigDict, PivotSupportedGroupByAggs, - TransformPivotConfig, PIVOT_SUPPORTED_AGGS, - PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; +import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; +import { useIndexData } from '../../../../hooks/use_index_data'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; +import { useToastNotifications } from '../../../../app_dependencies'; +import { SearchItems } from '../../../../hooks/use_search_items'; -import { DropDown } from '../aggregation_dropdown'; -import { AggListForm } from '../aggregation_list'; -import { GroupByListForm } from '../group_by_list'; - -import { getPivotDropdownOptions } from './common'; -import { SwitchModal } from './switch_modal'; - -export interface StepDefineExposedState { - aggList: PivotAggsConfigDict; - groupByList: PivotGroupByConfigDict; - isAdvancedPivotEditorEnabled: boolean; - isAdvancedSourceEditorEnabled: boolean; - searchLanguage: QUERY_LANGUAGE; - searchString: string | undefined; - searchQuery: string | SavedSearchQuery; - sourceConfigUpdated: boolean; - valid: boolean; -} - -interface ErrorMessage { - query: string; - message: string; -} - -const defaultSearch = '*'; - -const QUERY_LANGUAGE_KUERY = 'kuery'; -const QUERY_LANGUAGE_LUCENE = 'lucene'; -type QUERY_LANGUAGE = 'kuery' | 'lucene'; - -export function getDefaultStepDefineState(searchItems: SearchItems): StepDefineExposedState { - return { - aggList: {} as PivotAggsConfigDict, - groupByList: {} as PivotGroupByConfigDict, - isAdvancedPivotEditorEnabled: false, - isAdvancedSourceEditorEnabled: false, - searchLanguage: QUERY_LANGUAGE_KUERY, - searchString: undefined, - searchQuery: searchItems.savedSearch !== undefined ? searchItems.combinedQuery : defaultSearch, - sourceConfigUpdated: false, - valid: false, - }; -} - -export function applyTransformConfigToDefineState( - state: StepDefineExposedState, - transformConfig?: TransformPivotConfig -): StepDefineExposedState { - // apply the transform configuration to wizard DEFINE state - if (transformConfig !== undefined) { - // transform aggregations config to wizard state - state.aggList = Object.keys(transformConfig.pivot.aggregations).reduce((aggList, aggName) => { - const aggConfig = transformConfig.pivot.aggregations[aggName] as Dictionary; - const agg = Object.keys(aggConfig)[0]; - aggList[aggName] = { - ...aggConfig[agg], - agg: agg as PIVOT_SUPPORTED_AGGS, - aggName, - dropDownName: aggName, - } as PivotAggsConfig; - return aggList; - }, {} as PivotAggsConfigDict); - - // transform group by config to wizard state - state.groupByList = Object.keys(transformConfig.pivot.group_by).reduce( - (groupByList, groupByName) => { - const groupByConfig = transformConfig.pivot.group_by[groupByName] as Dictionary; - const groupBy = Object.keys(groupByConfig)[0]; - groupByList[groupByName] = { - agg: groupBy as PIVOT_SUPPORTED_GROUP_BY_AGGS, - aggName: groupByName, - dropDownName: groupByName, - ...groupByConfig[groupBy], - } as PivotGroupByConfig; - return groupByList; - }, - {} as PivotGroupByConfigDict - ); - - // only apply the query from the transform config to wizard state if it's not the default query - const query = transformConfig.source.query; - if (query !== undefined && !isEqual(query, matchAllQuery)) { - state.isAdvancedSourceEditorEnabled = true; - state.searchQuery = query; - state.sourceConfigUpdated = true; - } - - // applying a transform config to wizard state will always result in a valid configuration - state.valid = true; - } - - return state; -} - -export function getAggNameConflictToastMessages( - aggName: AggName, - aggList: PivotAggsConfigDict, - groupByList: PivotGroupByConfigDict -): string[] { - if (aggList[aggName] !== undefined) { - return [ - i18n.translate('xpack.transform.stepDefineForm.aggExistsErrorMessage', { - defaultMessage: `An aggregation configuration with the name '{aggName}' already exists.`, - values: { aggName }, - }), - ]; - } - - if (groupByList[aggName] !== undefined) { - return [ - i18n.translate('xpack.transform.stepDefineForm.groupByExistsErrorMessage', { - defaultMessage: `A group by configuration with the name '{aggName}' already exists.`, - values: { aggName }, - }), - ]; - } - - const conflicts: string[] = []; - - // check the new aggName against existing aggs and groupbys - const aggNameSplit = aggName.split('.'); - let aggNameCheck: string; - aggNameSplit.forEach(aggNamePart => { - aggNameCheck = aggNameCheck === undefined ? aggNamePart : `${aggNameCheck}.${aggNamePart}`; - if (aggList[aggNameCheck] !== undefined || groupByList[aggNameCheck] !== undefined) { - conflicts.push( - i18n.translate('xpack.transform.stepDefineForm.nestedConflictErrorMessage', { - defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{aggNameCheck}'.`, - values: { aggName, aggNameCheck }, - }) - ); - } - }); - - if (conflicts.length > 0) { - return conflicts; - } - - // check all aggs against new aggName - Object.keys(aggList).some(aggListName => { - const aggListNameSplit = aggListName.split('.'); - let aggListNameCheck: string; - return aggListNameSplit.some(aggListNamePart => { - aggListNameCheck = - aggListNameCheck === undefined ? aggListNamePart : `${aggListNameCheck}.${aggListNamePart}`; - if (aggListNameCheck === aggName) { - conflicts.push( - i18n.translate('xpack.transform.stepDefineForm.nestedAggListConflictErrorMessage', { - defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{aggListName}'.`, - values: { aggName, aggListName }, - }) - ); - return true; - } - return false; - }); - }); - - if (conflicts.length > 0) { - return conflicts; - } - - // check all group-bys against new aggName - Object.keys(groupByList).some(groupByListName => { - const groupByListNameSplit = groupByListName.split('.'); - let groupByListNameCheck: string; - return groupByListNameSplit.some(groupByListNamePart => { - groupByListNameCheck = - groupByListNameCheck === undefined - ? groupByListNamePart - : `${groupByListNameCheck}.${groupByListNamePart}`; - if (groupByListNameCheck === aggName) { - conflicts.push( - i18n.translate('xpack.transform.stepDefineForm.nestedGroupByListConflictErrorMessage', { - defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{groupByListName}'.`, - values: { aggName, groupByListName }, - }) - ); - return true; - } - return false; - }); - }); +import { AdvancedPivotEditor } from '../advanced_pivot_editor'; +import { AdvancedPivotEditorSwitch } from '../advanced_pivot_editor_switch'; +import { AdvancedQueryEditorSwitch } from '../advanced_query_editor_switch'; +import { AdvancedSourceEditor } from '../advanced_source_editor'; +import { PivotConfiguration } from '../pivot_configuration'; +import { SourceSearchBar } from '../source_search_bar'; - return conflicts; -} +import { StepDefineExposedState } from './common'; +import { useStepDefineForm } from './hooks/use_step_define_form'; -interface Props { +export interface StepDefineFormProps { overrides?: StepDefineExposedState; onChange(s: StepDefineExposedState): void; searchItems: SearchItems; } -export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, searchItems }) => { - const toastNotifications = useToastNotifications(); - const { esQueryDsl, esTransformPivot } = useDocumentationLinks(); - - const defaults = { ...getDefaultStepDefineState(searchItems), ...overrides }; - - // The internal state of the input query bar updated on every key stroke. - const [searchInput, setSearchInput] = useState({ - query: defaults.searchString || '', - language: defaults.searchLanguage, - }); - const [errorMessage, setErrorMessage] = useState(undefined); - - // The state of the input query bar updated on every submit and to be exposed. - const [searchLanguage, setSearchLanguage] = useState( - defaults.searchLanguage - ); - const [searchString, setSearchString] = useState( - defaults.searchString - ); - const [searchQuery, setSearchQuery] = useState(defaults.searchQuery); - +export const StepDefineForm: FC = React.memo(props => { + const { searchItems } = props; const { indexPattern } = searchItems; - const searchChangeHandler = (query: Query) => setSearchInput(query); - const searchSubmitHandler = (query: Query) => { - setSearchLanguage(query.language as QUERY_LANGUAGE); - setSearchString(query.query !== '' ? (query.query as string) : undefined); - try { - switch (query.language) { - case QUERY_LANGUAGE_KUERY: - setSearchQuery( - esKuery.toElasticsearchQuery( - esKuery.fromKueryExpression(query.query as string), - indexPattern - ) - ); - return; - case QUERY_LANGUAGE_LUCENE: - setSearchQuery(esQuery.luceneStringToDsl(query.query as string)); - return; - } - } catch (e) { - setErrorMessage({ query: query.query as string, message: e.message }); - } - }; - - // The list of selected group by fields - const [groupByList, setGroupByList] = useState(defaults.groupByList); + const toastNotifications = useToastNotifications(); + const stepDefineForm = useStepDefineForm(props); const { - groupByOptions, - groupByOptionsData, - aggOptions, - aggOptionsData, - } = getPivotDropdownOptions(indexPattern); - - const addGroupBy = (d: DropDownLabel[]) => { - const label: AggName = d[0].label; - const config: PivotGroupByConfig = groupByOptionsData[label]; - const aggName: AggName = config.aggName; - - const aggNameConflictMessages = getAggNameConflictToastMessages(aggName, aggList, groupByList); - if (aggNameConflictMessages.length > 0) { - aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); - return; - } - - groupByList[aggName] = config; - setGroupByList({ ...groupByList }); - }; - - const updateGroupBy = (previousAggName: AggName, item: PivotGroupByConfig) => { - const groupByListWithoutPrevious = { ...groupByList }; - delete groupByListWithoutPrevious[previousAggName]; - - const aggNameConflictMessages = getAggNameConflictToastMessages( - item.aggName, - aggList, - groupByListWithoutPrevious - ); - if (aggNameConflictMessages.length > 0) { - aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); - return; - } - - groupByListWithoutPrevious[item.aggName] = item; - setGroupByList({ ...groupByListWithoutPrevious }); - }; - - const deleteGroupBy = (aggName: AggName) => { - delete groupByList[aggName]; - setGroupByList({ ...groupByList }); - }; - - // The list of selected aggregations - const [aggList, setAggList] = useState(defaults.aggList); - - const addAggregation = (d: DropDownLabel[]) => { - const label: AggName = d[0].label; - const config: PivotAggsConfig = aggOptionsData[label]; - const aggName: AggName = config.aggName; - - const aggNameConflictMessages = getAggNameConflictToastMessages(aggName, aggList, groupByList); - if (aggNameConflictMessages.length > 0) { - aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); - return; - } - - aggList[aggName] = config; - setAggList({ ...aggList }); - }; - - const updateAggregation = (previousAggName: AggName, item: PivotAggsConfig) => { - const aggListWithoutPrevious = { ...aggList }; - delete aggListWithoutPrevious[previousAggName]; - - const aggNameConflictMessages = getAggNameConflictToastMessages( - item.aggName, - aggListWithoutPrevious, - groupByList - ); - if (aggNameConflictMessages.length > 0) { - aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); - return; - } - - aggListWithoutPrevious[item.aggName] = item; - setAggList({ ...aggListWithoutPrevious }); - }; - - const deleteAggregation = (aggName: AggName) => { - delete aggList[aggName]; - setAggList({ ...aggList }); - }; - - const pivotAggsArr = dictionaryToArray(aggList); - const pivotGroupByArr = dictionaryToArray(groupByList); - const pivotQuery = getPivotQuery(searchQuery); - - // Advanced editor for pivot config state - const [isAdvancedEditorSwitchModalVisible, setAdvancedEditorSwitchModalVisible] = useState(false); - const [ + advancedEditorConfig, + isAdvancedPivotEditorEnabled, isAdvancedPivotEditorApplyButtonEnabled, - setAdvancedPivotEditorApplyButtonEnabled, - ] = useState(false); - const [isAdvancedPivotEditorEnabled, setAdvancedPivotEditorEnabled] = useState( - defaults.isAdvancedPivotEditorEnabled - ); - // Advanced editor for source config state - const [sourceConfigUpdated, setSourceConfigUpdated] = useState(defaults.sourceConfigUpdated); - const [ - isAdvancedSourceEditorSwitchModalVisible, - setAdvancedSourceEditorSwitchModalVisible, - ] = useState(false); - const [isAdvancedSourceEditorEnabled, setAdvancedSourceEditorEnabled] = useState( - defaults.isAdvancedSourceEditorEnabled - ); - const [ + } = stepDefineForm.advancedPivotEditor.state; + const { + advancedEditorSourceConfig, + isAdvancedSourceEditorEnabled, isAdvancedSourceEditorApplyButtonEnabled, - setAdvancedSourceEditorApplyButtonEnabled, - ] = useState(false); + } = stepDefineForm.advancedSourceEditor.state; + const { aggList, groupByList, pivotGroupByArr, pivotAggsArr } = stepDefineForm.pivotConfig.state; + const pivotQuery = stepDefineForm.searchBar.state.pivotQuery; + + const indexPreviewProps = { + ...useIndexData(indexPattern, stepDefineForm.searchBar.state.pivotQuery), + dataTestSubj: 'transformIndexPreview', + toastNotifications, + }; const previewRequest = getPreviewRequestBody( indexPattern.title, @@ -428,49 +92,48 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, pivotGroupByArr, pivotAggsArr ); - // pivot config - const stringifiedPivotConfig = JSON.stringify(previewRequest.pivot, null, 2); - const [advancedEditorConfigLastApplied, setAdvancedEditorConfigLastApplied] = useState( - stringifiedPivotConfig - ); - const { - convertToJson, - setXJson: setAdvancedEditorConfig, - xJson: advancedEditorConfig, - xJsonMode, - } = useXJsonMode(stringifiedPivotConfig); + const pivotPreviewProps = { + ...usePivotData(indexPattern.title, pivotQuery, aggList, groupByList), + dataTestSubj: 'transformPivotPreview', + toastNotifications, + }; - useEffect(() => { - setAdvancedEditorConfig(stringifiedPivotConfig); - }, [setAdvancedEditorConfig, stringifiedPivotConfig]); + // TODO This should use the actual value of `indices.query.bool.max_clause_count` + const maxIndexFields = 1024; + const numIndexFields = indexPattern.fields.length; + const disabledQuery = numIndexFields > maxIndexFields; - // source config - const stringifiedSourceConfig = JSON.stringify(previewRequest.source.query, null, 2); - const [ - advancedEditorSourceConfigLastApplied, - setAdvancedEditorSourceConfigLastApplied, - ] = useState(stringifiedSourceConfig); - const [advancedEditorSourceConfig, setAdvancedEditorSourceConfig] = useState( - stringifiedSourceConfig + const copyToClipboardSource = getIndexDevConsoleStatement(pivotQuery, indexPattern.title); + const copyToClipboardSourceDescription = i18n.translate( + 'xpack.transform.indexPreview.copyClipboardTooltip', + { + defaultMessage: 'Copy Dev Console statement of the index preview to the clipboard.', + } ); - const applyAdvancedSourceEditorChanges = () => { + const copyToClipboardPivot = getPivotPreviewDevConsoleStatement(previewRequest); + const copyToClipboardPivotDescription = i18n.translate( + 'xpack.transform.pivotPreview.copyClipboardTooltip', + { + defaultMessage: 'Copy Dev Console statement of the pivot preview to the clipboard.', + } + ); + + const applySourceChangesHandler = () => { const sourceConfig = JSON.parse(advancedEditorSourceConfig); - const prettySourceConfig = JSON.stringify(sourceConfig, null, 2); - setSearchQuery(sourceConfig); - setSourceConfigUpdated(true); - setAdvancedEditorSourceConfig(prettySourceConfig); - setAdvancedEditorSourceConfigLastApplied(prettySourceConfig); - setAdvancedSourceEditorApplyButtonEnabled(false); + stepDefineForm.searchBar.actions.setSearchQuery(sourceConfig); + stepDefineForm.advancedSourceEditor.actions.applyAdvancedSourceEditorChanges(); }; - const applyAdvancedPivotEditorChanges = () => { - const pivotConfig = JSON.parse(convertToJson(advancedEditorConfig)); + const applyPivotChangesHandler = () => { + const pivot = JSON.parse( + stepDefineForm.advancedPivotEditor.actions.convertToJson(advancedEditorConfig) + ); const newGroupByList: PivotGroupByConfigDict = {}; - if (pivotConfig !== undefined && pivotConfig.group_by !== undefined) { - Object.entries(pivotConfig.group_by).forEach(d => { + if (pivot !== undefined && pivot.group_by !== undefined) { + Object.entries(pivot.group_by).forEach(d => { const aggName = d[0]; const aggConfig = d[1] as PivotGroupByDict; const aggConfigKeys = Object.keys(aggConfig); @@ -483,11 +146,11 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, }; }); } - setGroupByList(newGroupByList); + stepDefineForm.pivotConfig.actions.setGroupByList(newGroupByList); const newAggList: PivotAggsConfigDict = {}; - if (pivotConfig !== undefined && pivotConfig.aggregations !== undefined) { - Object.entries(pivotConfig.aggregations).forEach(d => { + if (pivot !== undefined && pivot.aggregations !== undefined) { + Object.entries(pivot.aggregations).forEach(d => { const aggName = d[0]; const aggConfig = d[1] as PivotAggDict; const aggConfigKeys = Object.keys(aggConfig); @@ -500,459 +163,207 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, }; }); } - setAggList(newAggList); - - setAdvancedEditorConfigLastApplied(advancedEditorConfig); - setAdvancedPivotEditorApplyButtonEnabled(false); - }; - - const toggleAdvancedEditor = () => { - setAdvancedEditorConfig(advancedEditorConfig); - setAdvancedPivotEditorEnabled(!isAdvancedPivotEditorEnabled); - setAdvancedPivotEditorApplyButtonEnabled(false); - if (isAdvancedPivotEditorEnabled === false) { - setAdvancedEditorConfigLastApplied(advancedEditorConfig); - } - }; - // If switching to KQL after updating via editor - reset search - const toggleAdvancedSourceEditor = (reset = false) => { - if (reset === true) { - setSearchQuery(defaultSearch); - setSourceConfigUpdated(false); - } - if (isAdvancedSourceEditorEnabled === false) { - setAdvancedEditorSourceConfigLastApplied(advancedEditorSourceConfig); - } - - setAdvancedSourceEditorEnabled(!isAdvancedSourceEditorEnabled); - setAdvancedSourceEditorApplyButtonEnabled(false); - }; - - const advancedEditorHelpText = ( - - {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpText', { - defaultMessage: - 'The advanced editor allows you to edit the pivot configuration of the transform.', - })}{' '} - - {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpTextLink', { - defaultMessage: 'Learn more about available options.', - })} - - - ); - - const advancedSourceEditorHelpText = ( - - {i18n.translate('xpack.transform.stepDefineForm.advancedSourceEditorHelpText', { - defaultMessage: - 'The advanced editor allows you to edit the source query clause of the transform.', - })}{' '} - - {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpTextLink', { - defaultMessage: 'Learn more about available options.', - })} - - - ); + stepDefineForm.pivotConfig.actions.setAggList(newAggList); - const valid = pivotGroupByArr.length > 0 && pivotAggsArr.length > 0; - - useEffect(() => { - const previewRequestUpdate = getPreviewRequestBody( - indexPattern.title, - pivotQuery, - pivotGroupByArr, - pivotAggsArr - ); - - const stringifiedSourceConfigUpdate = JSON.stringify( - previewRequestUpdate.source.query, - null, - 2 + stepDefineForm.advancedPivotEditor.actions.setAdvancedEditorConfigLastApplied( + advancedEditorConfig ); - setAdvancedEditorSourceConfig(stringifiedSourceConfigUpdate); - - onChange({ - aggList, - groupByList, - isAdvancedPivotEditorEnabled, - isAdvancedSourceEditorEnabled, - searchLanguage, - searchString, - searchQuery, - sourceConfigUpdated, - valid, - }); - // custom comparison - /* eslint-disable react-hooks/exhaustive-deps */ - }, [ - JSON.stringify(pivotAggsArr), - JSON.stringify(pivotGroupByArr), - isAdvancedPivotEditorEnabled, - isAdvancedSourceEditorEnabled, - searchLanguage, - searchString, - searchQuery, - valid, - /* eslint-enable react-hooks/exhaustive-deps */ - ]); + stepDefineForm.advancedPivotEditor.actions.setAdvancedPivotEditorApplyButtonEnabled(false); + }; - const indexPreviewProps = useIndexData(indexPattern, pivotQuery); - const pivotPreviewProps = usePivotData(indexPattern.title, pivotQuery, aggList, groupByList); + const { esQueryDsl } = useDocumentationLinks(); + const { esTransformPivot } = useDocumentationLinks(); - // TODO This should use the actual value of `indices.query.bool.max_clause_count` - const maxIndexFields = 1024; - const numIndexFields = indexPattern.fields.length; - const disabledQuery = numIndexFields > maxIndexFields; + const advancedEditorsSidebarWidth = '220px'; return ( - - -
- - {searchItems.savedSearch === undefined && ( - - - {indexPattern.title} - - {!disabledQuery && ( - - {!isAdvancedSourceEditorEnabled && ( - - setErrorMessage(undefined)} - input={ - - } - isOpen={ - errorMessage?.query === searchInput.query && - errorMessage?.message !== '' - } - > - - {i18n.translate( - 'xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar', - { - defaultMessage: 'Invalid query', - } - )} - {': '} - {errorMessage?.message.split('\n')[0]} - - - - )} - - )} - - )} - - {isAdvancedSourceEditorEnabled && ( - - - - { - setSearchString(undefined); - setAdvancedEditorSourceConfig(d); - - // Disable the "Apply"-Button if the config hasn't changed. - if (advancedEditorSourceConfigLastApplied === d) { - setAdvancedSourceEditorApplyButtonEnabled(false); - return; - } - - // Try to parse the string passed on from the editor. - // If parsing fails, the "Apply"-Button will be disabled - try { - JSON.parse(d); - setAdvancedSourceEditorApplyButtonEnabled(true); - } catch (e) { - setAdvancedSourceEditorApplyButtonEnabled(false); - } - }} - setOptions={{ - fontSize: '12px', - }} - theme="textmate" - aria-label={i18n.translate( - 'xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel', - { - defaultMessage: 'Advanced query editor', - } - )} - /> - - - - )} - {searchItems.savedSearch === undefined && ( - - - - { - if (isAdvancedSourceEditorEnabled && sourceConfigUpdated) { - setAdvancedSourceEditorSwitchModalVisible(true); - return; - } - - toggleAdvancedSourceEditor(); - }} - data-test-subj="transformAdvancedQueryEditorSwitch" - /> - {isAdvancedSourceEditorSwitchModalVisible && ( - setAdvancedSourceEditorSwitchModalVisible(false)} - onConfirm={() => { - setAdvancedSourceEditorSwitchModalVisible(false); - toggleAdvancedSourceEditor(true); - }} - type={'source'} +
+ + {searchItems.savedSearch === undefined && ( + + {indexPattern.title} + + )} + + <> + + + {/* Flex Column #1: Search Bar / Advanced Search Editor */} + {searchItems.savedSearch === undefined && ( + <> + {!disabledQuery && !isAdvancedSourceEditorEnabled && ( + )} + {isAdvancedSourceEditorEnabled && } + + )} + {searchItems?.savedSearch?.id !== undefined && ( + {searchItems.savedSearch.title} + )} + + + {/* Search options: Advanced Editor Switch / Copy to Clipboard / Advanced Editor Apply Button */} + + + + + + {searchItems.savedSearch === undefined && ( + + )} + + + + {(copy: () => void) => ( + + )} + + + {isAdvancedSourceEditorEnabled && ( - - {i18n.translate( - 'xpack.transform.stepDefineForm.advancedSourceEditorApplyButtonText', - { - defaultMessage: 'Apply changes', - } - )} - + + + + {i18n.translate( + 'xpack.transform.stepDefineForm.advancedSourceEditorHelpText', + { + defaultMessage: + 'The advanced editor allows you to edit the source query clause of the transform configuration.', + } + )}{' '} + + {i18n.translate( + 'xpack.transform.stepDefineForm.advancedEditorHelpTextLink', + { + defaultMessage: 'Learn more about available options.', + } + )} + + + + + {i18n.translate( + 'xpack.transform.stepDefineForm.advancedSourceEditorApplyButtonText', + { + defaultMessage: 'Apply changes', + } + )} + + )} - - )} - {searchItems.savedSearch !== undefined && searchItems.savedSearch.id !== undefined && ( - - {searchItems.savedSearch.title} - - )} - + + + + + + + + + + + {/* Flex Column #1: Pivot Config Form / Advanced Pivot Config Editor */} + {!isAdvancedPivotEditorEnabled && ( - - - - - - - - - - - - - - - + )} - {isAdvancedPivotEditorEnabled && ( - - - - { - setAdvancedEditorConfig(d); - - // Disable the "Apply"-Button if the config hasn't changed. - if (advancedEditorConfigLastApplied === d) { - setAdvancedPivotEditorApplyButtonEnabled(false); - return; - } - - // Try to parse the string passed on from the editor. - // If parsing fails, the "Apply"-Button will be disabled - try { - JSON.parse(convertToJson(d)); - setAdvancedPivotEditorApplyButtonEnabled(true); - } catch (e) { - setAdvancedPivotEditorApplyButtonEnabled(false); - } - }} - setOptions={{ - fontSize: '12px', - }} - theme="textmate" - aria-label={i18n.translate( - 'xpack.transform.stepDefineForm.advancedEditorAriaLabel', - { - defaultMessage: 'Advanced pivot editor', - } - )} - /> - - - + )} - - - - { - if ( - isAdvancedPivotEditorEnabled && - (isAdvancedPivotEditorApplyButtonEnabled || - advancedEditorConfig !== advancedEditorConfigLastApplied) - ) { - setAdvancedEditorSwitchModalVisible(true); - return; - } - - toggleAdvancedEditor(); - }} - data-test-subj="transformAdvancedPivotEditorSwitch" - /> - {isAdvancedEditorSwitchModalVisible && ( - setAdvancedEditorSwitchModalVisible(false)} - onConfirm={() => { - setAdvancedEditorSwitchModalVisible(false); - toggleAdvancedEditor(); - }} - type={'pivot'} - /> - )} - - {isAdvancedPivotEditorEnabled && ( + + + + + + + + + + + + {(copy: () => void) => ( + + )} + + + + + + {isAdvancedPivotEditorEnabled && ( + + + + <> + {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpText', { + defaultMessage: + 'The advanced editor allows you to edit the pivot configuration of the transform.', + })}{' '} + + {i18n.translate( + 'xpack.transform.stepDefineForm.advancedEditorHelpTextLink', + { + defaultMessage: 'Learn more about available options.', + } + )} + + + + {i18n.translate( @@ -962,58 +373,15 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, } )} - )} - - - {!valid && ( - - - - {i18n.translate('xpack.transform.stepDefineForm.formHelp', { - defaultMessage: - 'Transforms are scalable and automated processes for pivoting. Choose at least one group-by and aggregation to get started.', - })} - - - )} - -
-
- - - - - - -
+ + )} + + + +
+ + + +
); }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index f2e5d30b0601f..60bea6f20ae50 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -16,7 +16,7 @@ import { } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; -import { StepDefineExposedState } from './step_define_form'; +import { StepDefineExposedState } from './common'; import { StepDefineSummary } from './step_define_summary'; jest.mock('../../../../../shared_imports'); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index b9021f4ee5b11..414f6e37504da 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -8,14 +8,7 @@ import React, { Fragment, FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiText, -} from '@elastic/eui'; +import { EuiCodeBlock, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui'; import { dictionaryToArray } from '../../../../../../common/types/common'; @@ -35,7 +28,7 @@ import { SearchItems } from '../../../../hooks/use_search_items'; import { AggListSummary } from '../aggregation_list'; import { GroupByListSummary } from '../group_by_list'; -import { StepDefineExposedState } from './step_define_form'; +import { StepDefineExposedState } from './common'; interface Props { formState: StepDefineExposedState; @@ -65,85 +58,80 @@ export const StepDefineSummary: FC = ({ groupByList ); + const isModifiedQuery = + typeof searchString === 'undefined' && + !isDefaultQuery(pivotQuery) && + !isMatchAllQuery(pivotQuery); + return ( - - -
- - {searchItems.savedSearch === undefined && ( - - - {searchItems.indexPattern.title} - - {typeof searchString === 'string' && ( - - {searchString} - - )} - {typeof searchString === 'undefined' && - !isDefaultQuery(pivotQuery) && - !isMatchAllQuery(pivotQuery) && ( - - - {JSON.stringify(pivotQuery, null, 2)} - - - )} - +
+ + {searchItems.savedSearch === undefined && ( + + + {searchItems.indexPattern.title} + + {typeof searchString === 'string' && ( + + {searchString} + )} - - {searchItems.savedSearch !== undefined && searchItems.savedSearch.id !== undefined && ( + {isModifiedQuery && ( - {searchItems.savedSearch.title} + + {JSON.stringify(pivotQuery, null, 2)} + )} + + )} - - - - - - - - -
- - - + {searchItems.savedSearch !== undefined && searchItems.savedSearch.id !== undefined && ( + + {searchItems.savedSearch.title} + + )} + + + + + + + + + + = ({ toastNotifications={toastNotifications} /> - - +
+
); }; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/switch_modal/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/switch_modal/index.ts new file mode 100644 index 0000000000000..1aa00177cfbc8 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/switch_modal/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SwitchModal } from './switch_modal'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/switch_modal.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/switch_modal/switch_modal.tsx similarity index 100% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/switch_modal.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/switch_modal/switch_modal.tsx diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 0773ecbb1d8d3..3fcfd77ba54cd 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -155,8 +155,8 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) const stepsConfig = [ { - title: i18n.translate('xpack.transform.transformsWizard.stepDefineTitle', { - defaultMessage: 'Define pivot', + title: i18n.translate('xpack.transform.transformsWizard.stepConfigurationTitle', { + defaultMessage: 'Configuration', }), children: ( { await transform.wizard.enabledAdvancedPivotEditor(); await transform.wizard.assertAdvancedPivotEditorContent( - testData.expected.pivotAdvancedEditorValue + testData.expected.pivotAdvancedEditorValueArr ); }); diff --git a/x-pack/test/functional/services/transform_ui/wizard.ts b/x-pack/test/functional/services/transform_ui/wizard.ts index e63af493438d6..4b136746eb525 100644 --- a/x-pack/test/functional/services/transform_ui/wizard.ts +++ b/x-pack/test/functional/services/transform_ui/wizard.ts @@ -274,10 +274,15 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await this.assertAggregationEntryExists(index, expectedLabel); }, - async assertAdvancedPivotEditorContent(expectedValue: Record) { + async assertAdvancedPivotEditorContent(expectedValue: string[]) { const advancedEditorString = await aceEditor.getValue('transformAdvancedPivotEditor'); - const advancedEditorValue = JSON.parse(advancedEditorString); - expect(advancedEditorValue).to.eql(expectedValue); + // Not all lines may be visible in the editor and thus aceEditor may not return all lines. + // This means we might not get back valid JSON so we only test against the first few lines + // and see if the string matches. + + // const advancedEditorValue = JSON.parse(advancedEditorString); + // expect(advancedEditorValue).to.eql(expectedValue); + expect(advancedEditorString.split('\n').splice(0, 3)).to.eql(expectedValue); }, async assertAdvancedPivotEditorSwitchExists() { From eae92e34c3a48c9303a9a725a268f5d2dd93bc6d Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 5 May 2020 10:26:45 +0200 Subject: [PATCH 06/75] [SIEM] Adds Cypress 'Exports custom rule' test (#64099) * changes default browser download folder * adds 'exportRuleAction' data-test-subj attribute * implements 'Exports a custom rule' test * changes headless browser to chrome * updates path * updates 'EXPECTED_RULE_FILE_PATH' * gives time to the file to be downloaded * adds downloads folder * updates download directory * updates paths * captures API call to check the content of the file * removes browser launch hook since is not needed anymore Co-authored-by: Elastic Machine --- .../signal_detection_rules_export.spec.ts | 46 +++++++++++++++++++ x-pack/plugins/siem/cypress/plugins/index.js | 1 + .../cypress/screens/signal_detection_rules.ts | 2 + x-pack/plugins/siem/cypress/support/index.js | 5 ++ .../cypress/tasks/signal_detection_rules.ts | 9 ++++ .../test_files/expected_rules_export.ndjson | 2 + x-pack/plugins/siem/package.json | 2 +- .../detection_engine/rules/all/columns.tsx | 1 + 8 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/siem/cypress/integration/signal_detection_rules_export.spec.ts create mode 100644 x-pack/plugins/siem/cypress/test_files/expected_rules_export.ndjson diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_export.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_export.spec.ts new file mode 100644 index 0000000000000..f0e8b4556f704 --- /dev/null +++ b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_export.spec.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + goToManageSignalDetectionRules, + waitForSignalsIndexToBeCreated, + waitForSignalsPanelToBeLoaded, +} from '../tasks/detections'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { exportFirstRule } from '../tasks/signal_detection_rules'; + +import { DETECTIONS } from '../urls/navigation'; + +const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson'; + +describe('Export rules', () => { + before(() => { + esArchiverLoad('custom_rules'); + cy.server(); + cy.route( + 'POST', + '**api/detection_engine/rules/_export?exclude_export_details=false&file_name=rules_export.ndjson*' + ).as('export'); + }); + + after(() => { + esArchiverUnload('custom_rules'); + }); + + it('Exports a custom rule', () => { + loginAndWaitForPageWithoutDateRange(DETECTIONS); + waitForSignalsPanelToBeLoaded(); + waitForSignalsIndexToBeCreated(); + goToManageSignalDetectionRules(); + exportFirstRule(); + cy.wait('@export').then(xhr => { + cy.readFile(EXPECTED_EXPORTED_RULE_FILE_PATH).then($expectedExportedJson => { + cy.wrap(xhr.responseBody).should('eql', $expectedExportedJson); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/cypress/plugins/index.js b/x-pack/plugins/siem/cypress/plugins/index.js index 1132e66cc16dd..01d31b85de463 100644 --- a/x-pack/plugins/siem/cypress/plugins/index.js +++ b/x-pack/plugins/siem/cypress/plugins/index.js @@ -19,6 +19,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies const wp = require('@cypress/webpack-preprocessor'); + module.exports = on => { const options = { webpackOptions: { diff --git a/x-pack/plugins/siem/cypress/screens/signal_detection_rules.ts b/x-pack/plugins/siem/cypress/screens/signal_detection_rules.ts index f74f5c26ddc2e..a41b8296f83e4 100644 --- a/x-pack/plugins/siem/cypress/screens/signal_detection_rules.ts +++ b/x-pack/plugins/siem/cypress/screens/signal_detection_rules.ts @@ -18,6 +18,8 @@ export const DELETE_RULE_BULK_BTN = '[data-test-subj="deleteRuleBulk"]'; export const ELASTIC_RULES_BTN = '[data-test-subj="show-elastic-rules-filter-button"]'; +export const EXPORT_ACTION_BTN = '[data-test-subj="exportRuleAction"]'; + export const FIFTH_RULE = 4; export const FIRST_RULE = 0; diff --git a/x-pack/plugins/siem/cypress/support/index.js b/x-pack/plugins/siem/cypress/support/index.js index 37fa920a8bc31..672acfd41a264 100644 --- a/x-pack/plugins/siem/cypress/support/index.js +++ b/x-pack/plugins/siem/cypress/support/index.js @@ -32,5 +32,10 @@ Cypress.on('uncaught:exception', err => { } }); +Cypress.on('window:before:load', win => { + win.fetch = null; + win.Blob = null; +}); + // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/x-pack/plugins/siem/cypress/tasks/signal_detection_rules.ts b/x-pack/plugins/siem/cypress/tasks/signal_detection_rules.ts index a404f1142cba7..5a4d71de9e851 100644 --- a/x-pack/plugins/siem/cypress/tasks/signal_detection_rules.ts +++ b/x-pack/plugins/siem/cypress/tasks/signal_detection_rules.ts @@ -23,6 +23,7 @@ import { RULES_TABLE, SORT_RULES_BTN, THREE_HUNDRED_ROWS, + EXPORT_ACTION_BTN, } from '../screens/signal_detection_rules'; export const activateRule = (rulePosition: number) => { @@ -48,6 +49,14 @@ export const deleteSelectedRules = () => { cy.get(DELETE_RULE_BULK_BTN).click(); }; +export const exportFirstRule = () => { + cy.get(COLLAPSED_ACTION_BTN) + .first() + .click({ force: true }); + cy.get(EXPORT_ACTION_BTN).click(); + cy.get(EXPORT_ACTION_BTN).should('not.exist'); +}; + export const filterByCustomRules = () => { cy.get(CUSTOM_RULES_BTN).click({ force: true }); cy.get(LOADING_SPINNER).should('exist'); diff --git a/x-pack/plugins/siem/cypress/test_files/expected_rules_export.ndjson b/x-pack/plugins/siem/cypress/test_files/expected_rules_export.ndjson new file mode 100644 index 0000000000000..c2e779feeca77 --- /dev/null +++ b/x-pack/plugins/siem/cypress/test_files/expected_rules_export.ndjson @@ -0,0 +1,2 @@ +{"actions":[],"created_at":"2020-03-26T10:09:07.569Z","updated_at":"2020-03-26T10:09:08.021Z","created_by":"elastic","description":"Rule 1","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","id":"49db5bd1-bdd5-4821-be26-bb70a815dedb","immutable":false,"index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"0cea4194-03f2-4072-b281-d31b72221d9d","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":50,"name":"Rule 1","query":"host.name:*","references":[],"meta":{"from":"1m","throttle":"no_actions"},"severity":"low","updated_by":"elastic","tags":["rule1"],"to":"now","type":"query","threat":[],"throttle":"no_actions","version":1} +{"exported_count":1,"missing_rules":[],"missing_rules_count":0} diff --git a/x-pack/plugins/siem/package.json b/x-pack/plugins/siem/package.json index 829332918d3c4..a055d011a5cbb 100644 --- a/x-pack/plugins/siem/package.json +++ b/x-pack/plugins/siem/package.json @@ -8,7 +8,7 @@ "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "cypress open --config-file ./cypress/cypress.json", - "cypress:run": "cypress run --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-siem/cypress/results > ../../../target/kibana-siem/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../target/kibana-siem/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-siem/cypress/results/*.xml ../../../target/junit/ && exit $status;", + "cypress:run": "cypress run --browser chrome --headless --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-siem/cypress/results > ../../../target/kibana-siem/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../target/kibana-siem/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-siem/cypress/results/*.xml ../../../target/junit/ && exit $status;", "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/siem_cypress/config.ts" }, "devDependencies": { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index d383b5cd464ce..8e79f037d82b0 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -62,6 +62,7 @@ export const getActions = ( }, }, { + 'data-test-subj': 'exportRuleAction', description: i18n.EXPORT_RULE, icon: 'exportAction', name: i18n.EXPORT_RULE, From 537065a977c45b17fe5513cd5e964f40b09dc47e Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 5 May 2020 09:42:17 +0100 Subject: [PATCH 07/75] [SIEM] Fix auto save for template timeline (#65001) * update save timeline * fix types * allow template timeline to be updated via import * fix unit tests * fix for review * handle update timeline Co-authored-by: Elastic Machine --- .../siem/common/types/timeline/index.ts | 5 ++ .../components/open_timeline/helpers.test.ts | 13 ++++ .../components/open_timeline/helpers.ts | 4 +- .../timeline/one/index.gql_query.ts | 3 + x-pack/plugins/siem/public/graphql/types.ts | 6 ++ .../plugins/siem/public/mock/global_state.ts | 4 ++ .../siem/public/mock/timeline_results.ts | 39 +++++++----- .../components/signals/actions.test.tsx | 4 ++ .../siem/public/store/timeline/defaults.ts | 5 ++ .../siem/public/store/timeline/epic.test.ts | 15 ++++- .../siem/public/store/timeline/epic.ts | 7 +++ .../siem/public/store/timeline/helpers.ts | 1 + .../siem/public/store/timeline/model.ts | 12 ++++ .../public/store/timeline/reducer.test.ts | 26 ++++++++ .../convert_saved_object_to_savedtimeline.ts | 7 ++- .../lib/timeline/pick_saved_timeline.ts | 11 ++-- .../routes/__mocks__/request_responses.ts | 2 +- .../routes/import_timelines_route.test.ts | 6 +- .../timeline/routes/import_timelines_route.ts | 60 +++++++++++++++++-- .../routes/update_timelines_route.test.ts | 6 ++ .../timeline/routes/utils/import_timelines.ts | 2 + .../timeline/routes/utils/update_timelines.ts | 7 +-- 22 files changed, 211 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/siem/common/types/timeline/index.ts b/x-pack/plugins/siem/common/types/timeline/index.ts index 55b4f9c6aca4d..43f66da6109df 100644 --- a/x-pack/plugins/siem/common/types/timeline/index.ts +++ b/x-pack/plugins/siem/common/types/timeline/index.ts @@ -144,6 +144,11 @@ export const TimelineTypeLiteralRt = runtimeTypes.union([ runtimeTypes.literal(TimelineType.default), ]); +const TimelineTypeLiteralWithNullRt = unionWithNullType(TimelineTypeLiteralRt); + +export type TimelineTypeLiteral = runtimeTypes.TypeOf; +export type TimelineTypeLiteralWithNull = runtimeTypes.TypeOf; + export const SavedTimelineRuntimeType = runtimeTypes.partial({ columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), diff --git a/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts index a779d579bf4d1..a7c0b08fc8a21 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts @@ -36,6 +36,7 @@ import { KueryFilterQueryKind } from '../../store/model'; import { Note } from '../../lib/note'; import moment from 'moment'; import sinon from 'sinon'; +import { TimelineType } from '../../../common/types/timeline'; jest.mock('../../store/inputs/actions'); jest.mock('../../store/timeline/actions'); @@ -299,6 +300,9 @@ describe('helpers', () => { sortDirection: 'desc', }, title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, version: '1', width: 1100, }); @@ -393,6 +397,9 @@ describe('helpers', () => { sortDirection: 'desc', }, title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, version: '1', width: 1100, }); @@ -467,6 +474,9 @@ describe('helpers', () => { }, loadingEventIds: [], title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, @@ -632,6 +642,9 @@ describe('helpers', () => { }, loadingEventIds: [], title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, diff --git a/x-pack/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/plugins/siem/public/components/open_timeline/helpers.ts index 16ba2de872bd1..681d39feb09f8 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/helpers.ts +++ b/x-pack/plugins/siem/public/components/open_timeline/helpers.ts @@ -8,8 +8,8 @@ import ApolloClient from 'apollo-client'; import { getOr, set, isEmpty } from 'lodash/fp'; import { Action } from 'typescript-fsa'; import uuid from 'uuid'; - import { Dispatch } from 'redux'; + import { oneTimelineQuery } from '../../containers/timeline/one/index.gql_query'; import { TimelineResult, GetOneTimeline, NoteResult } from '../../graphql/types'; import { @@ -169,6 +169,8 @@ export const defaultTimelineToTimelineModel = ( savedObjectId: duplicate ? null : timeline.savedObjectId, version: duplicate ? null : timeline.version, title: duplicate ? '' : timeline.title || '', + templateTimelineId: duplicate ? null : timeline.templateTimelineId, + templateTimelineVersion: duplicate ? null : timeline.templateTimelineVersion, }).reduce((acc: TimelineModel, [key, value]) => (value != null ? set(key, value, acc) : acc), { ...timelineDefaults, id: '', diff --git a/x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts index e68db445a5cbb..d70a419b99a3b 100644 --- a/x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts +++ b/x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts @@ -129,6 +129,9 @@ export const oneTimelineQuery = gql` version } title + timelineType + templateTimelineId + templateTimelineVersion savedQueryId sort { columnId diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts index 8c39d5e58b99e..86890988c06b6 100644 --- a/x-pack/plugins/siem/public/graphql/types.ts +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -5112,6 +5112,12 @@ export namespace GetOneTimeline { title: Maybe; + timelineType: Maybe; + + templateTimelineId: Maybe; + + templateTimelineVersion: Maybe; + savedQueryId: Maybe; sort: Maybe; diff --git a/x-pack/plugins/siem/public/mock/global_state.ts b/x-pack/plugins/siem/public/mock/global_state.ts index 6678c3043a3da..d0223b7834db0 100644 --- a/x-pack/plugins/siem/public/mock/global_state.ts +++ b/x-pack/plugins/siem/public/mock/global_state.ts @@ -23,6 +23,7 @@ import { DEFAULT_INTERVAL_TYPE, DEFAULT_INTERVAL_VALUE, } from '../../common/constants'; +import { TimelineType } from '../../common/types/timeline'; export const mockGlobalState: State = { app: { @@ -201,6 +202,9 @@ export const mockGlobalState: State = { kqlQuery: { filterQuery: null, filterQueryDraft: null }, loadingEventIds: [], title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, noteIds: [], dateRange: { start: 0, diff --git a/x-pack/plugins/siem/public/mock/timeline_results.ts b/x-pack/plugins/siem/public/mock/timeline_results.ts index edd1c73771829..1af0f533a7ca9 100644 --- a/x-pack/plugins/siem/public/mock/timeline_results.ts +++ b/x-pack/plugins/siem/public/mock/timeline_results.ts @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { FilterStateStore } from '../../../../../src/plugins/data/common/es_query/filters/meta_filter'; + +import { TimelineType } from '../../common/types/timeline'; import { OpenTimelineResult } from '../components/open_timeline/types'; import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../graphql/types'; @@ -10,7 +13,6 @@ import { allTimelinesQuery } from '../containers/timeline/all/index.gql_query'; import { CreateTimelineProps } from '../pages/detection_engine/components/signals/types'; import { TimelineModel } from '../store/timeline/model'; import { timelineDefaults } from '../store/timeline/defaults'; -import { FilterStateStore } from '../../../../../src/plugins/data/common/es_query/filters/meta_filter'; export interface MockedProvidedQuery { request: { query: GetAllTimeline.Query; @@ -168,7 +170,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 1', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -297,7 +299,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 2', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -426,7 +428,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 2', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -555,7 +557,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 3', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -684,7 +686,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 4', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -813,7 +815,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 5', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -942,7 +944,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 6', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -1071,7 +1073,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -1200,7 +1202,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -1329,7 +1331,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -1458,7 +1460,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -1587,7 +1589,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -1716,7 +1718,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', - timelineType: null, + timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: null, created: 1558386787614, @@ -2141,6 +2143,9 @@ export const mockTimelineModel: TimelineModel = { sortDirection: Direction.desc, }, title: 'Test rule', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, version: '1', width: 1100, }; @@ -2164,6 +2169,9 @@ export const mockTimelineResult: TimelineResult = { ], kqlMode: 'filter', title: 'Test rule', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, version: '1', @@ -2235,6 +2243,9 @@ export const defaultTimelineProps: CreateTimelineProps = { showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: Direction.desc }, title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, version: null, width: 1100, }, diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx index 8aaed08a0a0a1..ab75fcb6d6d1f 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx @@ -15,6 +15,7 @@ import { } from '../../../../mock/'; import { CreateTimeline, UpdateTimelineLoading } from './types'; import { Ecs } from '../../../../graphql/types'; +import { TimelineType } from '../../../../../common/types/timeline'; jest.mock('apollo-client'); @@ -215,6 +216,9 @@ describe('signals actions', () => { sortDirection: 'desc', }, title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, version: null, width: 1100, }, diff --git a/x-pack/plugins/siem/public/store/timeline/defaults.ts b/x-pack/plugins/siem/public/store/timeline/defaults.ts index 7f04bb4c4dad0..9203720e2e28c 100644 --- a/x-pack/plugins/siem/public/store/timeline/defaults.ts +++ b/x-pack/plugins/siem/public/store/timeline/defaults.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TimelineType } from '../../../common/types/timeline'; + import { Direction } from '../../graphql/types'; import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/constants'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; @@ -33,6 +35,9 @@ export const timelineDefaults: SubsetTimelineModel & Pick { describe('#convertTimelineAsInput ', () => { @@ -135,6 +138,9 @@ describe('Epic Timeline', () => { }, loadingEventIds: [], title: 'saved', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, @@ -283,6 +289,9 @@ describe('Epic Timeline', () => { columnId: '@timestamp', sortDirection: 'desc', }, + templateTimelineId: null, + templateTimelineVersion: null, + timelineType: TimelineType.default, title: 'saved', }); }); diff --git a/x-pack/plugins/siem/public/store/timeline/epic.ts b/x-pack/plugins/siem/public/store/timeline/epic.ts index 6812d8d8aa672..a7b8c48b45068 100644 --- a/x-pack/plugins/siem/public/store/timeline/epic.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic.ts @@ -29,6 +29,7 @@ import { } from 'rxjs/operators'; import { esFilters, Filter, MatchAllFilter } from '../../../../../../src/plugins/data/public'; +import { TimelineType } from '../../../common/types/timeline'; import { TimelineInput, ResponseTimeline, TimelineResult } from '../../graphql/types'; import { AppApolloClient } from '../../lib/lib'; import { addError } from '../app/actions'; @@ -236,6 +237,9 @@ export const createTimelineEpic = (): Epic< ...savedTimeline, savedObjectId: response.timeline.savedObjectId, version: response.timeline.version, + timelineType: response.timeline.timelineType ?? TimelineType.default, + templateTimelineId: response.timeline.templateTimelineId ?? null, + templateTimelineVersion: response.timeline.templateTimelineVersion ?? null, isSaving: false, }, }), @@ -283,6 +287,9 @@ const timelineInput: TimelineInput = { kqlMode: null, kqlQuery: null, title: null, + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, dateRange: null, savedQueryId: null, sort: null, diff --git a/x-pack/plugins/siem/public/store/timeline/helpers.ts b/x-pack/plugins/siem/public/store/timeline/helpers.ts index 19de49918d100..adab029c11150 100644 --- a/x-pack/plugins/siem/public/store/timeline/helpers.ts +++ b/x-pack/plugins/siem/public/store/timeline/helpers.ts @@ -7,6 +7,7 @@ import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp'; import { Filter } from '../../../../../../src/plugins/data/public'; + import { getColumnWidthFromType } from '../../components/timeline/body/column_headers/helpers'; import { Sort } from '../../components/timeline/body/sort'; import { diff --git a/x-pack/plugins/siem/public/store/timeline/model.ts b/x-pack/plugins/siem/public/store/timeline/model.ts index 15bd2980e4aeb..7885064380eff 100644 --- a/x-pack/plugins/siem/public/store/timeline/model.ts +++ b/x-pack/plugins/siem/public/store/timeline/model.ts @@ -5,6 +5,9 @@ */ import { Filter } from '../../../../../../src/plugins/data/public'; + +import { TimelineTypeLiteralWithNull } from '../../../common/types/timeline'; + import { DataProvider } from '../../components/timeline/data_providers/data_provider'; import { Sort } from '../../components/timeline/body/sort'; import { PinnedEvent, TimelineNonEcsData } from '../../graphql/types'; @@ -77,6 +80,12 @@ export interface TimelineModel { }; /** Title */ title: string; + /** timelineTypes: default | template */ + timelineType: TimelineTypeLiteralWithNull; + /** an unique id for template timeline */ + templateTimelineId: string | null; + /** null for default timeline, number for template timeline */ + templateTimelineVersion: number | null; /** Notes added to the timeline itself. Notes added to events are stored (separately) in `eventIdToNote` */ noteIds: string[]; /** Events pinned to this timeline */ @@ -125,6 +134,9 @@ export type SubsetTimelineModel = Readonly< | 'kqlMode' | 'kqlQuery' | 'title' + | 'timelineType' + | 'templateTimelineId' + | 'templateTimelineVersion' | 'loadingEventIds' | 'noteIds' | 'pinnedEventIds' diff --git a/x-pack/plugins/siem/public/store/timeline/reducer.test.ts b/x-pack/plugins/siem/public/store/timeline/reducer.test.ts index 58fc1c7e1e3df..42c6d6ecb0e51 100644 --- a/x-pack/plugins/siem/public/store/timeline/reducer.test.ts +++ b/x-pack/plugins/siem/public/store/timeline/reducer.test.ts @@ -6,6 +6,8 @@ import { cloneDeep, set } from 'lodash/fp'; +import { TimelineType } from '../../../common/types/timeline'; + import { IS_OPERATOR, DataProvider, @@ -80,6 +82,9 @@ const timelineByIdMock: TimelineById = { kqlQuery: { filterQuery: null, filterQueryDraft: null }, loadingEventIds: [], title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, @@ -1110,6 +1115,9 @@ describe('Timeline', () => { kqlQuery: { filterQuery: null, filterQueryDraft: null }, loadingEventIds: [], title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, noteIds: [], dateRange: { start: 0, @@ -1202,6 +1210,9 @@ describe('Timeline', () => { kqlQuery: { filterQuery: null, filterQueryDraft: null }, loadingEventIds: [], title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, noteIds: [], dateRange: { start: 0, @@ -1400,6 +1411,9 @@ describe('Timeline', () => { kqlQuery: { filterQuery: null, filterQueryDraft: null }, loadingEventIds: [], title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, noteIds: [], dateRange: { start: 0, @@ -1492,6 +1506,9 @@ describe('Timeline', () => { kqlQuery: { filterQuery: null, filterQueryDraft: null }, loadingEventIds: [], title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, noteIds: [], dateRange: { start: 0, @@ -1679,6 +1696,9 @@ describe('Timeline', () => { kqlQuery: { filterQuery: null, filterQueryDraft: null }, loadingEventIds: [], title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, noteIds: [], dateRange: { start: 0, @@ -1755,6 +1775,9 @@ describe('Timeline', () => { kqlQuery: { filterQuery: null, filterQueryDraft: null }, loadingEventIds: [], title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, noteIds: [], dateRange: { start: 0, @@ -1855,6 +1878,9 @@ describe('Timeline', () => { kqlQuery: { filterQuery: null, filterQueryDraft: null }, loadingEventIds: [], title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, noteIds: [], dateRange: { start: 0, diff --git a/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts index bde24a338ec84..00fb77bfb1647 100644 --- a/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts @@ -11,16 +11,21 @@ import { identity } from 'fp-ts/lib/function'; import { TimelineSavedObjectRuntimeType, TimelineSavedObject, + TimelineType, } from '../../../common/types/timeline'; export const convertSavedObjectToSavedTimeline = (savedObject: unknown): TimelineSavedObject => { const timeline = pipe( TimelineSavedObjectRuntimeType.decode(savedObject), map(savedTimeline => { + const attributes = { + ...savedTimeline.attributes, + timelineType: savedTimeline.attributes.timelineType ?? TimelineType.default, + }; return { savedObjectId: savedTimeline.id, version: savedTimeline.version, - ...savedTimeline.attributes, + ...attributes, }; }), fold(errors => { diff --git a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts index eeded1cc2532d..6de10bffb1325 100644 --- a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts @@ -16,6 +16,7 @@ export const pickSavedTimeline = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any => { const dateNow = new Date().valueOf(); + if (timelineId == null) { savedTimeline.created = dateNow; savedTimeline.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; @@ -27,13 +28,15 @@ export const pickSavedTimeline = ( } if (savedTimeline.timelineType === TimelineType.template) { - savedTimeline.timelineType = TimelineType.template; if (savedTimeline.templateTimelineId == null) { + // create template timeline savedTimeline.templateTimelineId = uuid.v4(); - } - - if (savedTimeline.templateTimelineVersion == null) { savedTimeline.templateTimelineVersion = 1; + } else { + // update template timeline + if (savedTimeline.templateTimelineVersion != null) { + savedTimeline.templateTimelineVersion = savedTimeline.templateTimelineVersion + 1; + } } } else { savedTimeline.timelineType = TimelineType.default; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts index 304ca309775ff..2827c7a1c0ac6 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -109,7 +109,7 @@ export const updateTemplateTimelineWithTimelineId = { timeline: { ...inputTemplateTimeline, templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', - templateTimelineVersion: 2, + templateTimelineVersion: 1, }, timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', version: 'WzEyMjUsMV0=', diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts index 56c152d02ae98..11f93a9c48bf6 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts @@ -29,6 +29,7 @@ describe('import timelines', () => { let securitySetup: SecurityPluginSetup; let { context } = requestContextMock.createTools(); let mockGetTimeline: jest.Mock; + let mockGetTemplateTimeline: jest.Mock; let mockPersistTimeline: jest.Mock; let mockPersistPinnedEventOnTimeline: jest.Mock; let mockPersistNote: jest.Mock; @@ -51,6 +52,7 @@ describe('import timelines', () => { } as unknown) as SecurityPluginSetup; mockGetTimeline = jest.fn(); + mockGetTemplateTimeline = jest.fn(); mockPersistTimeline = jest.fn(); mockPersistPinnedEventOnTimeline = jest.fn(); mockPersistNote = jest.fn(); @@ -83,6 +85,7 @@ describe('import timelines', () => { jest.doMock('../saved_object', () => { return { getTimeline: mockGetTimeline.mockReturnValue(null), + getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null), persistTimeline: mockPersistTimeline.mockReturnValue({ timeline: { savedObjectId: newTimelineSavedObjectId, version: newTimelineVersion }, }), @@ -212,11 +215,12 @@ describe('import timelines', () => { }); }); - describe('Import a timeline already exist but overwrite is not allowed', () => { + describe('Import a timeline already exist', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null), persistTimeline: mockPersistTimeline, }; }); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index bff89bdf9b5b2..99621f1391acb 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -38,7 +38,9 @@ import { PromiseFromStreams, timelineSavedObjectOmittedFields, } from './utils/import_timelines'; -import { createTimelines, getTimeline } from './utils/create_timelines'; +import { createTimelines, getTimeline, getTemplateTimeline } from './utils/create_timelines'; +import { TimelineType } from '../../../../common/types/timeline'; +import { checkIsFailureCases } from './utils/update_timelines'; const CHUNK_PARSED_OBJECT_SIZE = 10; @@ -121,6 +123,9 @@ export const importTimelinesRoute = ( pinnedEventIds, globalNotes, eventNotes, + templateTimelineId, + templateTimelineVersion, + timelineType, } = parsedTimeline; const parsedTimelineObject = omit( timelineSavedObjectOmittedFields, @@ -128,9 +133,23 @@ export const importTimelinesRoute = ( ); let newTimeline = null; try { - const timeline = await getTimeline(frameworkRequest, savedObjectId); - - if (timeline == null) { + const templateTimeline = + templateTimelineId != null + ? await getTemplateTimeline(frameworkRequest, templateTimelineId) + : null; + const timeline = + templateTimeline?.savedObjectId != null || savedObjectId != null + ? await getTimeline( + frameworkRequest, + templateTimeline?.savedObjectId ?? savedObjectId + ) + : null; + const isHandlingTemplateTimeline = timelineType === TimelineType.template; + if ( + (timeline == null && !isHandlingTemplateTimeline) || + (templateTimeline == null && isHandlingTemplateTimeline) + ) { + // create timeline / template timeline newTimeline = await createTimelines( frameworkRequest, parsedTimelineObject, @@ -141,6 +160,37 @@ export const importTimelinesRoute = ( [] // existing note ids ); + resolve({ + timeline_id: newTimeline.timeline.savedObjectId, + status_code: 200, + }); + } else if ( + timeline != null && + templateTimeline != null && + isHandlingTemplateTimeline + ) { + // update template timeline + const errorObj = checkIsFailureCases( + isHandlingTemplateTimeline, + timeline.version, + templateTimeline.templateTimelineVersion ?? null, + timeline, + templateTimeline + ); + if (errorObj != null) { + return siemResponse.error(errorObj); + } + + newTimeline = await createTimelines( + frameworkRequest, + { ...parsedTimelineObject, templateTimelineId, templateTimelineVersion }, + timeline.savedObjectId, // timelineSavedObjectId + timeline.version, // timelineVersion + pinnedEventIds, + globalNotes, + [] // existing note ids + ); + resolve({ timeline_id: newTimeline.timeline.savedObjectId, status_code: 200, @@ -150,7 +200,7 @@ export const importTimelinesRoute = ( createBulkErrorObject({ id: savedObjectId, statusCode: 409, - message: `timeline_id: "${savedObjectId}" already exists`, + message: `timeline_id: "${timeline?.savedObjectId}" already exists`, }) ); } diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts index 9c47488d47159..2a3feb7afd59c 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts @@ -215,6 +215,12 @@ describe('update timelines', () => { ); }); + test('should Update existing template timeline with timelineId', async () => { + expect(mockPersistTimeline.mock.calls[0][1]).toEqual( + updateTemplateTimelineWithTimelineId.timelineId + ); + }); + test('should Update existing template timeline with timeline version', async () => { expect(mockPersistTimeline.mock.calls[0][2]).toEqual( updateTemplateTimelineWithTimelineId.version diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts index 9e120cdc023dc..a49627d40c8f5 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts @@ -96,4 +96,6 @@ export const timelineSavedObjectOmittedFields = [ 'createdBy', 'updated', 'updatedBy', + 'templateTimelineId', + 'templateTimelineVersion', ]; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts index 6a25d8def9116..a4efa676daddc 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts @@ -14,8 +14,7 @@ export const NO_MATCH_VERSION_ERROR_MESSAGE = 'TimelineVersion conflict: The given version doesn not match with existing timeline'; export const NO_MATCH_ID_ERROR_MESSAGE = "Timeline id doesn't match with existing template timeline"; -export const OLDER_VERSION_ERROR_MESSAGE = - 'Template timelineVersion conflict: The given version is older then existing version'; +export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE = 'Template timelineVersion conflict'; export const checkIsFailureCases = ( isHandlingTemplateTimeline: boolean, @@ -68,11 +67,11 @@ export const checkIsFailureCases = ( templateTimelineVersion != null && existTemplateTimeline != null && existTemplateTimeline.templateTimelineVersion != null && - existTemplateTimeline.templateTimelineVersion >= templateTimelineVersion + existTemplateTimeline.templateTimelineVersion !== templateTimelineVersion ) { // Throw error you can not update a template timeline version with an old version return { - body: OLDER_VERSION_ERROR_MESSAGE, + body: TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE, statusCode: 409, }; } else { From 43205ec8c7a2ba0cfe2f730217e87c33a873ed92 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Tue, 5 May 2020 11:47:55 +0300 Subject: [PATCH 08/75] [functional/page_objects] wait for infra ops page is loaded (#65050) * [functional/page_objects] wait for infra ops page is loaded * put waiting in PO method --- x-pack/plugins/infra/public/components/loading_page.tsx | 2 +- x-pack/test/functional/apps/infra/home_page.ts | 1 + x-pack/test/functional/page_objects/infra_home_page.ts | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/components/loading_page.tsx b/x-pack/plugins/infra/public/components/loading_page.tsx index ae179c6542c13..9d37fed45b583 100644 --- a/x-pack/plugins/infra/public/components/loading_page.tsx +++ b/x-pack/plugins/infra/public/components/loading_page.tsx @@ -27,7 +27,7 @@ export const LoadingPage = ({ message }: LoadingPageProps) => ( - {message} + {message}
diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index ed8bec570ab60..28d7956e353e1 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -33,6 +33,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { before(async () => { await esArchiver.load('infra/metrics_and_logs'); await pageObjects.common.navigateToApp('infraOps'); + await pageObjects.infraHome.waitForLoading(); }); after(async () => await esArchiver.unload('infra/metrics_and_logs')); diff --git a/x-pack/test/functional/page_objects/infra_home_page.ts b/x-pack/test/functional/page_objects/infra_home_page.ts index 998a60500aca2..51dad594f21f5 100644 --- a/x-pack/test/functional/page_objects/infra_home_page.ts +++ b/x-pack/test/functional/page_objects/infra_home_page.ts @@ -74,5 +74,9 @@ export function InfraHomePageProvider({ getService }: FtrProviderContext) { await testSubjects.click('configureSourceButton'); await testSubjects.exists('sourceConfigurationFlyout'); }, + + async waitForLoading() { + await testSubjects.missingOrFail('loadingMessage', { timeout: 20000 }); + }, }; } From 6d9c59da609adcf3b07f5b8d01ef3bc5d9b95743 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 5 May 2020 11:35:07 +0200 Subject: [PATCH 09/75] [Uptime] Filters in create alert flyout (#64753) --- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../triggers_actions_ui/public/types.ts | 2 +- .../__tests__/alert_monitor_status.test.tsx | 36 +- .../overview/alerts/add_filter_btn.tsx | 81 ++++ .../alerts/alert_expression_popover.tsx | 49 ++ .../overview/alerts/alert_field_number.tsx | 54 +++ .../overview/alerts/alert_monitor_status.tsx | 442 ++---------------- .../down_number_select.test.tsx.snap | 49 ++ .../time_expression_select.test.tsx.snap | 160 +++++++ .../__tests__/down_number_select.test.tsx | 29 ++ .../__tests__/time_expression_select.test.tsx | 21 + .../down_number_select.tsx | 42 ++ .../filters_expression_select.tsx | 163 +++++++ .../alerts/monitor_expressions/index.ts | 9 + .../time_expression_select.tsx | 118 +++++ .../monitor_expressions/translations.ts | 72 +++ .../overview/alerts/translations.ts | 113 +++++ .../filter_popover.test.tsx.snap | 4 +- .../overview/filter_group/filter_group.tsx | 55 +-- .../filter_group/filter_group_container.tsx | 97 +--- .../overview/filter_group/filter_popover.tsx | 37 +- .../overview/filter_group/translations.tsx | 23 + .../uptime/public/hooks/use_filter_update.ts | 56 +++ .../__tests__/monitor_status.test.ts | 2 +- .../public/lib/alert_types/monitor_status.tsx | 5 +- .../lib/alert_types/monitor_status_title.tsx | 34 ++ .../uptime/public/state/selectors/index.ts | 13 + .../functional/page_objects/uptime_page.ts | 39 +- .../test/functional/services/uptime/alerts.ts | 40 +- .../apps/uptime/alert_flyout.ts | 72 ++- 31 files changed, 1290 insertions(+), 635 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/alert_field_number.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/down_number_select.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/time_expression_select.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/down_number_select.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/time_expression_select.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/down_number_select.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/index.ts create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_expression_select.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/translations.ts create mode 100644 x-pack/plugins/uptime/public/components/overview/filter_group/translations.tsx create mode 100644 x-pack/plugins/uptime/public/hooks/use_filter_update.ts create mode 100644 x-pack/plugins/uptime/public/lib/alert_types/monitor_status_title.tsx diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f32d899809994..aeb86144635d9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15982,7 +15982,6 @@ "xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradeCompleteTitle": "クラスターがアップグレードされました", "xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingDescription": "1 つまたは複数の Elasticsearch ノードに、 Kibana よりも新しいバージョンの Elasticsearch があります。すべてのノードがアップグレードされた後で Kibana をアップグレードしてください。", "xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingTitle": "クラスターをアップグレード中です", - "xpack.uptime.alerts.locationSelectionItem.ariaLabel": "「{location}」の場所選択項目", "xpack.uptime.alerts.message.emptyTitle": "停止状況監視 ID を受信していません。", "xpack.uptime.alerts.message.fullListOverflow": "... とその他 {overflowCount} {pluralizedMonitor}", "xpack.uptime.alerts.message.multipleTitle": "停止状況監視: ", @@ -15990,9 +15989,6 @@ "xpack.uptime.alerts.message.singularTitle": "停止状況監視: ", "xpack.uptime.alerts.monitorStatus": "稼働状況監視ステータス", "xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel": "監視状態アラートのフィルター基準を許可するインプット", - "xpack.uptime.alerts.monitorStatus.locationSelection": "場所 {location} を選択します", - "xpack.uptime.alerts.monitorStatus.locationSelectionSwitch.ariaLabel": "アラートをトリガーする場所を選択します", - "xpack.uptime.alerts.monitorStatus.locationsSelectionExpression.ariaLabel": "ポップオーバーを開いてアラートをトリガーする場所を選択する", "xpack.uptime.alerts.monitorStatus.numTimesExpression.ariaLabel": "ダウンカウントインプットのポップオーバーを開く", "xpack.uptime.alerts.monitorStatus.numTimesField.ariaLabel": "アラートのトリガーに必要な停止回数を入力します", "xpack.uptime.alerts.monitorStatus.timerangeOption.days": "日", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0ab89bc250bb5..104d53a93088a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15990,7 +15990,6 @@ "xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradeCompleteTitle": "您的集群已升级", "xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingDescription": "一个或多个 Elasticsearch 节点的 Elasticsearch 版本比 Kibana 版本新。所有节点升级后,请升级 Kibana。", "xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingTitle": "您的集群正在升级", - "xpack.uptime.alerts.locationSelectionItem.ariaLabel": "“{location}”的位置选择项", "xpack.uptime.alerts.message.emptyTitle": "未接收到已关闭监测 ID", "xpack.uptime.alerts.message.fullListOverflow": "...以及 {overflowCount} 个其他{pluralizedMonitor}", "xpack.uptime.alerts.message.multipleTitle": "已关闭监测: ", @@ -15998,9 +15997,6 @@ "xpack.uptime.alerts.message.singularTitle": "已关闭监测: ", "xpack.uptime.alerts.monitorStatus": "运行时间监测状态", "xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel": "允许对监测状态告警使用筛选条件的输入", - "xpack.uptime.alerts.monitorStatus.locationSelection": "选择位置 {location}", - "xpack.uptime.alerts.monitorStatus.locationSelectionSwitch.ariaLabel": "选择告警应触发的位置", - "xpack.uptime.alerts.monitorStatus.locationsSelectionExpression.ariaLabel": "打开弹出框以选择告警应触发的位置", "xpack.uptime.alerts.monitorStatus.numTimesExpression.ariaLabel": "打开弹出框以输入已关闭计数", "xpack.uptime.alerts.monitorStatus.numTimesField.ariaLabel": "输入触发告警的已关闭计数", "xpack.uptime.alerts.monitorStatus.timerangeOption.days": "天", diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 7f78d327d0122..47cb7067296ce 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -104,7 +104,7 @@ export interface AlertTableItem extends Alert { export interface AlertTypeModel { id: string; - name: string; + name: string | JSX.Element; iconClass: string; validate: (alertParams: any) => ValidationResult; alertParamsExpression: React.FunctionComponent; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx index 8f33b6f652b9d..aa17086f49034 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx @@ -5,12 +5,8 @@ */ import React from 'react'; -import { - selectedLocationsToString, - AlertFieldNumber, - handleAlertFieldNumberChange, -} from '../alert_monitor_status'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { AlertFieldNumber, handleAlertFieldNumberChange } from '../alert_field_number'; describe('alert monitor status component', () => { describe('handleAlertFieldNumberChange', () => { @@ -146,34 +142,4 @@ describe('alert monitor status component', () => { expect(mockValueHandler.mock.calls).toEqual([]); }); }); - - describe('selectedLocationsToString', () => { - it('generates a formatted string for a valid list of options', () => { - const locations = [ - { - checked: 'on', - label: 'fairbanks', - }, - { - checked: 'on', - label: 'harrisburg', - }, - { - checked: undefined, - label: 'orlando', - }, - ]; - expect(selectedLocationsToString(locations)).toEqual('fairbanks, harrisburg'); - }); - - it('generates a formatted string for a single item', () => { - expect(selectedLocationsToString([{ checked: 'on', label: 'fairbanks' }])).toEqual( - 'fairbanks' - ); - }); - - it('returns an empty string when no valid options are available', () => { - expect(selectedLocationsToString([{ checked: 'off', label: 'harrisburg' }])).toEqual(''); - }); - }); }); diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.tsx new file mode 100644 index 0000000000000..c52489984dab4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { useFilterUpdate } from '../../../hooks/use_filter_update'; +import * as labels from './translations'; + +interface Props { + newFilters: string[]; + onNewFilter: (val: string) => void; +} + +export const AddFilterButton: React.FC = ({ newFilters, onNewFilter }) => { + const [isPopoverOpen, setPopover] = useState(false); + + const currentFilters = useFilterUpdate(); + + const getSelectedItems = (fieldName: string) => currentFilters.get(fieldName) || []; + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const items: JSX.Element[] = []; + + const allFilters = [ + { id: 'observer.geo.name', label: labels.LOCATION }, + { id: 'tags', label: labels.TAG }, + { id: 'url.port', label: labels.PORT }, + { id: 'monitor.type', label: labels.TYPE }, + ]; + + allFilters.forEach(filter => { + if (getSelectedItems(filter.id)?.length === 0 && !newFilters.includes(filter.id)) { + items.push( + { + closePopover(); + onNewFilter(filter.id); + }} + > + {filter.label} + + ); + } + }); + + const button = ( + + {labels.ADD_FILTER} + + ); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx new file mode 100644 index 0000000000000..00e8e45148985 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiExpression, EuiPopover } from '@elastic/eui'; + +interface AlertExpressionPopoverProps { + 'aria-label': string; + content: React.ReactElement; + description: string; + 'data-test-subj': string; + id: string; + value: string; +} + +export const AlertExpressionPopover: React.FC = ({ + 'aria-label': ariaLabel, + content, + 'data-test-subj': dataTestSubj, + description, + id, + value, +}) => { + const [isOpen, setIsOpen] = useState(false); + return ( + setIsOpen(!isOpen)} + value={value} + /> + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + > + {content} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_field_number.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_field_number.tsx new file mode 100644 index 0000000000000..61ead73878e03 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alert_field_number.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiFieldNumber } from '@elastic/eui'; + +interface AlertFieldNumberProps { + 'aria-label': string; + 'data-test-subj': string; + disabled: boolean; + fieldValue: number; + setFieldValue: React.Dispatch>; +} + +export const handleAlertFieldNumberChange = ( + e: React.ChangeEvent, + isInvalid: boolean, + setIsInvalid: React.Dispatch>, + setFieldValue: React.Dispatch> +) => { + const num = parseInt(e.target.value, 10); + if (isNaN(num) || num < 1) { + setIsInvalid(true); + } else { + if (isInvalid) setIsInvalid(false); + setFieldValue(num); + } +}; + +export const AlertFieldNumber = ({ + 'aria-label': ariaLabel, + 'data-test-subj': dataTestSubj, + disabled, + fieldValue, + setFieldValue, +}: AlertFieldNumberProps) => { + const [isInvalid, setIsInvalid] = useState(false); + + return ( + handleAlertFieldNumberChange(e, isInvalid, setIsInvalid, setFieldValue)} + disabled={disabled} + value={fieldValue} + isInvalid={isInvalid} + /> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx index 83892bf23dced..d532c94e9b081 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx @@ -4,122 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; -import { - EuiExpression, - EuiFieldNumber, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiSelectable, - EuiSpacer, - EuiSwitch, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useEffect, useState } from 'react'; +import { EuiSpacer } from '@elastic/eui'; import { DataPublicPluginSetup } from 'src/plugins/data/public'; -import { KueryBar } from '..'; - -interface AlertFieldNumberProps { - 'aria-label': string; - 'data-test-subj': string; - disabled: boolean; - fieldValue: number; - setFieldValue: React.Dispatch>; -} - -export const handleAlertFieldNumberChange = ( - e: React.ChangeEvent, - isInvalid: boolean, - setIsInvalid: React.Dispatch>, - setFieldValue: React.Dispatch> -) => { - const num = parseInt(e.target.value, 10); - if (isNaN(num) || num < 1) { - setIsInvalid(true); - } else { - if (isInvalid) setIsInvalid(false); - setFieldValue(num); - } -}; - -export const AlertFieldNumber = ({ - 'aria-label': ariaLabel, - 'data-test-subj': dataTestSubj, - disabled, - fieldValue, - setFieldValue, -}: AlertFieldNumberProps) => { - const [isInvalid, setIsInvalid] = useState(false); - - return ( - handleAlertFieldNumberChange(e, isInvalid, setIsInvalid, setFieldValue)} - disabled={disabled} - value={fieldValue} - isInvalid={isInvalid} - /> - ); -}; - -interface AlertExpressionPopoverProps { - 'aria-label': string; - content: React.ReactElement; - description: string; - 'data-test-subj': string; - id: string; - value: string; -} - -const AlertExpressionPopover: React.FC = ({ - 'aria-label': ariaLabel, - content, - 'data-test-subj': dataTestSubj, - description, - id, - value, -}) => { - const [isOpen, setIsOpen] = useState(false); - return ( - setIsOpen(!isOpen)} - value={value} - /> - } - isOpen={isOpen} - closePopover={() => setIsOpen(false)} - > - {content} - - ); -}; +import * as labels from './translations'; +import { + DownNoExpressionSelect, + TimeExpressionSelect, + FiltersExpressionsSelect, +} from './monitor_expressions'; -export const selectedLocationsToString = (selectedLocations: any[]) => - // create a nicely-formatted description string for all `on` locations - selectedLocations - .filter(({ checked }) => checked === 'on') - .map(({ label }) => label) - .sort() - .reduce((acc, cur) => { - if (acc === '') { - return cur; - } - return acc + `, ${cur}`; - }, ''); +import { AddFilterButton } from './add_filter_btn'; +import { KueryBar } from '..'; interface AlertMonitorStatusProps { autocomplete: DataPublicPluginSetup['autocomplete']; @@ -135,101 +31,9 @@ interface AlertMonitorStatusProps { } export const AlertMonitorStatusComponent: React.FC = props => { - const { filters, locations } = props; - const [numTimes, setNumTimes] = useState(5); - const [numMins, setNumMins] = useState(15); - const [allLabels, setAllLabels] = useState(true); + const { filters, setAlertParams } = props; - // locations is an array of `Option[]`, but that type doesn't seem to be exported by EUI - const [selectedLocations, setSelectedLocations] = useState( - locations.map(location => ({ - 'aria-label': i18n.translate('xpack.uptime.alerts.locationSelectionItem.ariaLabel', { - defaultMessage: 'Location selection item for "{location}"', - values: { - location, - }, - }), - disabled: allLabels, - label: location, - })) - ); - const [timerangeUnitOptions, setTimerangeUnitOptions] = useState([ - { - 'aria-label': i18n.translate( - 'xpack.uptime.alerts.timerangeUnitSelectable.secondsOption.ariaLabel', - { - defaultMessage: '"Seconds" time range select item', - } - ), - 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.secondsOption', - key: 's', - label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.seconds', { - defaultMessage: 'seconds', - }), - }, - { - 'aria-label': i18n.translate( - 'xpack.uptime.alerts.timerangeUnitSelectable.minutesOption.ariaLabel', - { - defaultMessage: '"Minutes" time range select item', - } - ), - 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.minutesOption', - checked: 'on', - key: 'm', - label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.minutes', { - defaultMessage: 'minutes', - }), - }, - { - 'aria-label': i18n.translate( - 'xpack.uptime.alerts.timerangeUnitSelectable.hoursOption.ariaLabel', - { - defaultMessage: '"Hours" time range select item', - } - ), - 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.hoursOption', - key: 'h', - label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.hours', { - defaultMessage: 'hours', - }), - }, - { - 'aria-label': i18n.translate( - 'xpack.uptime.alerts.timerangeUnitSelectable.daysOption.ariaLabel', - { - defaultMessage: '"Days" time range select item', - } - ), - 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.daysOption', - key: 'd', - label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.days', { - defaultMessage: 'days', - }), - }, - ]); - - const { setAlertParams } = props; - - useEffect(() => { - setAlertParams('numTimes', numTimes); - }, [numTimes, setAlertParams]); - - useEffect(() => { - const timerangeUnit = timerangeUnitOptions.find(({ checked }) => checked === 'on')?.key ?? 'm'; - setAlertParams('timerange', { from: `now-${numMins}${timerangeUnit}`, to: 'now' }); - }, [numMins, timerangeUnitOptions, setAlertParams]); - - useEffect(() => { - if (allLabels) { - setAlertParams('locations', []); - } else { - setAlertParams( - 'locations', - selectedLocations.filter(l => l.checked === 'on').map(l => l.label) - ); - } - }, [selectedLocations, setAlertParams, allLabels]); + const [newFilters, setNewFilters] = useState([]); useEffect(() => { setAlertParams('filters', filters); @@ -239,207 +43,41 @@ export const AlertMonitorStatusComponent: React.FC = pr <> + - - } - data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression" - description={ - filters - ? i18n.translate( - 'xpack.uptime.alerts.monitorStatus.numTimesExpression.matchingMonitors.description', - { - defaultMessage: 'matching monitors are down >', - } - ) - : i18n.translate( - 'xpack.uptime.alerts.monitorStatus.numTimesExpression.anyMonitors.description', - { - defaultMessage: 'any monitor is down >', - } - ) - } - id="ping-count" - value={`${numTimes} times`} - /> + + + - - - - } - data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueExpression" - description="within" - id="timerange" - value={`last ${numMins}`} - /> - - - - -
- -
-
- { - if (newOptions.reduce((acc, { checked }) => acc || checked === 'on', false)) { - setTimerangeUnitOptions(newOptions); - } - }} - singleSelection={true} - listProps={{ - showIcons: true, - }} - > - {list => list} - - - } - data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitExpression" - description="" - id="timerange-unit" - value={ - timerangeUnitOptions.find(({ checked }) => checked === 'on')?.label.toLowerCase() ?? - '' - } - /> -
-
+ + + - {selectedLocations.length === 0 && ( - - )} - {selectedLocations.length > 0 && ( - - - { - setAllLabels(!allLabels); - setSelectedLocations( - selectedLocations.map((l: any) => ({ - 'aria-label': i18n.translate( - 'xpack.uptime.alerts.monitorStatus.locationSelection', - { - defaultMessage: 'Select the location {location}', - values: { - location: l, - }, - } - ), - ...l, - 'data-test-subj': `xpack.uptime.alerts.monitorStatus.locationSelection.${l.label}LocationOption`, - disabled: !allLabels, - })) - ); - }} - /> - - - setSelectedLocations(e)} - > - {location => location} - - -
- } - data-test-subj="xpack.uptime.alerts.monitorStatus.locationsSelectionExpression" - description="from" - id="locations" - value={ - selectedLocations.length === 0 || allLabels - ? 'any location' - : selectedLocationsToString(selectedLocations) + + { + if (newFilters.includes(removeFiler)) { + setNewFilters(newFilters.filter(item => item !== removeFiler)); } - /> - )} + }} + /> + + + + { + setNewFilters([...newFilters, newFilter]); + }} + /> + + ); }; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/down_number_select.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/down_number_select.test.tsx.snap new file mode 100644 index 0000000000000..b761bc3e2368a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/down_number_select.test.tsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DownNoExpressionSelect component should renders against props 1`] = ` +
+
+ +
+
+`; + +exports[`DownNoExpressionSelect component should shallow renders against props 1`] = ` + + } + data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression" + description="matching monitors are down >" + id="ping-count" + value="5 times" +/> +`; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/time_expression_select.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/time_expression_select.test.tsx.snap new file mode 100644 index 0000000000000..cbbaccbab34e4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/time_expression_select.test.tsx.snap @@ -0,0 +1,160 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TimeExpressionSelect component should renders against props 1`] = ` +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`TimeExpressionSelect component should shallow renders against props 1`] = ` + + + + } + data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueExpression" + description="within" + id="timerange" + value="last 15" + /> + + + + +
+ +
+
+ + [Function] + + + } + data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitExpression" + description="" + id="timerange-unit" + value="minutes" + /> +
+
+`; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/down_number_select.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/down_number_select.test.tsx new file mode 100644 index 0000000000000..13db9f2f80909 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/down_number_select.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { DownNoExpressionSelect } from '../down_number_select'; + +describe('DownNoExpressionSelect component', () => { + const filters = + '"{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"US-West"}}],"minimum_should_match":1}},' + + '{"bool":{"should":[{"match":{"url.port":443}}],"minimum_should_match":1}}]}}"'; + + it('should shallow renders against props', function() { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); + + it('should renders against props', function() { + const component = renderWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/time_expression_select.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/time_expression_select.test.tsx new file mode 100644 index 0000000000000..37df7dfdaaec1 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/time_expression_select.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { TimeExpressionSelect } from '../time_expression_select'; + +describe('TimeExpressionSelect component', () => { + it('should shallow renders against props', function() { + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('should renders against props', function() { + const component = renderWithIntl(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/down_number_select.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/down_number_select.tsx new file mode 100644 index 0000000000000..7f68aef8e179c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/down_number_select.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; +import { AlertExpressionPopover } from '../alert_expression_popover'; +import * as labels from '../translations'; +import { AlertFieldNumber } from '../alert_field_number'; + +interface Props { + setAlertParams: (key: string, value: any) => void; + filters: string; +} + +export const DownNoExpressionSelect: React.FC = ({ filters, setAlertParams }) => { + const [numTimes, setNumTimes] = useState(5); + + useEffect(() => { + setAlertParams('numTimes', numTimes); + }, [numTimes, setAlertParams]); + + return ( + + } + data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression" + description={filters ? labels.MATCHING_MONITORS_DOWN : labels.ANY_MONITOR_DOWN} + id="ping-count" + value={`${numTimes} times`} + /> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx new file mode 100644 index 0000000000000..8298f202b9458 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { EuiButtonIcon, EuiExpression, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { FilterPopover } from '../../filter_group/filter_popover'; +import { overviewFiltersSelector } from '../../../../state/selectors'; +import { useFilterUpdate } from '../../../../hooks/use_filter_update'; +import { filterLabels } from '../../filter_group/translations'; +import { alertFilterLabels } from './translations'; + +interface Props { + newFilters: string[]; + onRemoveFilter: (val: string) => void; + setAlertParams: (key: string, value: any) => void; +} + +export const FiltersExpressionsSelect: React.FC = ({ + setAlertParams, + newFilters, + onRemoveFilter, +}) => { + const { tags, ports, schemes, locations } = useSelector(overviewFiltersSelector); + + const [updatedFieldValues, setUpdatedFieldValues] = useState<{ + fieldName: string; + values: string[]; + }>({ fieldName: '', values: [] }); + + const currentFilters = useFilterUpdate(updatedFieldValues.fieldName, updatedFieldValues.values); + + useEffect(() => { + if (updatedFieldValues.fieldName === 'observer.geo.name') { + setAlertParams('locations', updatedFieldValues.values); + } + }, [setAlertParams, updatedFieldValues]); + + useEffect(() => { + setAlertParams('locations', []); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const selectedTags = currentFilters.get('tags'); + const selectedPorts = currentFilters.get('url.port'); + const selectedScheme = currentFilters.get('monitor.type'); + const selectedLocation = currentFilters.get('observer.geo.name'); + + const getSelectedItems = (fieldName: string) => currentFilters.get(fieldName) || []; + + const onFilterFieldChange = (fieldName: string, values: string[]) => { + setUpdatedFieldValues({ fieldName, values }); + }; + + const monitorFilters = [ + { + onFilterFieldChange, + loading: false, + fieldName: 'url.port', + id: 'filter_port', + disabled: ports?.length === 0, + items: ports?.map((p: number) => p.toString()) ?? [], + selectedItems: getSelectedItems('url.port'), + title: filterLabels.PORT, + description: selectedPorts ? alertFilterLabels.USING_PORT : alertFilterLabels.USING, + value: selectedPorts?.join(',') ?? alertFilterLabels.ANY_PORT, + }, + { + onFilterFieldChange, + loading: false, + fieldName: 'tags', + id: 'filter_tags', + disabled: tags?.length === 0, + items: tags ?? [], + selectedItems: getSelectedItems('tags'), + title: filterLabels.TAGS, + description: selectedTags ? alertFilterLabels.WITH_TAG : alertFilterLabels.WITH, + value: selectedTags?.join(',') ?? alertFilterLabels.ANY_TAG, + }, + { + onFilterFieldChange, + loading: false, + fieldName: 'monitor.type', + id: 'filter_scheme', + disabled: schemes?.length === 0, + items: schemes ?? [], + selectedItems: getSelectedItems('monitor.type'), + title: filterLabels.SCHEME, + description: selectedScheme ? alertFilterLabels.OF_TYPE : alertFilterLabels.OF, + value: selectedScheme?.join(',') ?? alertFilterLabels.ANY_TYPE, + }, + { + onFilterFieldChange, + loading: false, + fieldName: 'observer.geo.name', + id: 'filter_location', + disabled: locations?.length === 0, + items: locations ?? [], + selectedItems: getSelectedItems('observer.geo.name'), + title: filterLabels.SCHEME, + description: selectedLocation ? alertFilterLabels.FROM_LOCATION : alertFilterLabels.FROM, + value: selectedLocation?.join(',') ?? alertFilterLabels.ANY_LOCATION, + }, + ]; + + const [isOpen, setIsOpen] = useState({ + filter_port: false, + filter_tags: false, + filter_scheme: false, + filter_location: false, + }); + + const filtersToDisplay = monitorFilters.filter( + curr => curr.selectedItems.length > 0 || newFilters?.includes(curr.fieldName) + ); + + return ( + <> + {filtersToDisplay.map(({ description, value, ...item }) => ( + + + setIsOpen({ ...isOpen, [item.id]: !isOpen[item.id] })} + /> + } + forceOpen={isOpen[item.id]} + setForceOpen={() => { + setIsOpen({ ...isOpen, [item.id]: !isOpen[item.id] }); + }} + /> + + + { + onRemoveFilter(item.fieldName); + onFilterFieldChange(item.fieldName, []); + }} + /> + + + + + ))} + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/index.ts b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/index.ts new file mode 100644 index 0000000000000..acc19dfbc8f8b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DownNoExpressionSelect } from './down_number_select'; +export { FiltersExpressionsSelect } from './filters_expression_select'; +export { TimeExpressionSelect } from './time_expression_select'; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_expression_select.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_expression_select.tsx new file mode 100644 index 0000000000000..8cab4315967d3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_expression_select.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiTitle } from '@elastic/eui'; +import { AlertExpressionPopover } from '../alert_expression_popover'; +import * as labels from '../translations'; +import { AlertFieldNumber } from '../alert_field_number'; +import { timeExpLabels } from './translations'; + +interface Props { + setAlertParams: (key: string, value: any) => void; +} + +const TimeRangeOptions = [ + { + 'aria-label': labels.SECONDS_TIME_RANGE, + 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.secondsOption', + key: 's', + label: labels.SECONDS, + }, + { + 'aria-label': labels.MINUTES_TIME_RANGE, + 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.minutesOption', + checked: 'on', + key: 'm', + label: labels.MINUTES, + }, + { + 'aria-label': labels.HOURS_TIME_RANGE, + 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.hoursOption', + key: 'h', + label: labels.HOURS, + }, + { + 'aria-label': labels.DAYS_TIME_RANGE, + 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.daysOption', + key: 'd', + label: labels.DAYS, + }, +]; + +export const TimeExpressionSelect: React.FC = ({ setAlertParams }) => { + const [numUnits, setNumUnits] = useState(15); + + const [timerangeUnitOptions, setTimerangeUnitOptions] = useState(TimeRangeOptions); + + useEffect(() => { + const timerangeUnit = timerangeUnitOptions.find(({ checked }) => checked === 'on')?.key ?? 'm'; + setAlertParams('timerange', { from: `now-${numUnits}${timerangeUnit}`, to: 'now' }); + }, [numUnits, timerangeUnitOptions, setAlertParams]); + + return ( + + + + } + data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueExpression" + description="within" + id="timerange" + value={`last ${numUnits}`} + /> + + + + +
+ +
+
+ { + if (newOptions.reduce((acc, { checked }) => acc || checked === 'on', false)) { + setTimerangeUnitOptions(newOptions); + } + }} + singleSelection={true} + listProps={{ + showIcons: true, + }} + > + {list => list} + + + } + data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitExpression" + description="" + id="timerange-unit" + value={ + timerangeUnitOptions.find(({ checked }) => checked === 'on')?.label.toLowerCase() ?? '' + } + /> +
+
+ ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/translations.ts b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/translations.ts new file mode 100644 index 0000000000000..5fefc9f3ae35b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/translations.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const alertFilterLabels = { + USING: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.using', { + defaultMessage: 'Using', + }), + + USING_PORT: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.usingPort', { + defaultMessage: 'Using port', + }), + + ANY_PORT: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.anyPort', { + defaultMessage: 'any port', + }), + + WITH: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.with', { + defaultMessage: 'Using', + }), + + WITH_TAG: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.withTag', { + defaultMessage: 'With tag', + }), + + ANY_TAG: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.anyTag', { + defaultMessage: 'any tag', + }), + + OF: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.of', { + defaultMessage: 'Of', + }), + + OF_TYPE: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.ofType', { + defaultMessage: 'Of type', + }), + + ANY_TYPE: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.anyType', { + defaultMessage: 'any type', + }), + + FROM: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.from', { + defaultMessage: 'From', + }), + + FROM_LOCATION: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.fromLocation', { + defaultMessage: 'From location', + }), + + ANY_LOCATION: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.anyLocation', { + defaultMessage: 'any location', + }), +}; + +export const timeExpLabels = { + OPEN_TIME_POPOVER: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.timerangeUnitExpression.ariaLabel', + { + defaultMessage: 'Open the popover for time range unit select field', + } + ), + SELECT_TIME_RANGE_ARIA: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable', + { + defaultMessage: 'Selectable field for the time range units alerts should use', + } + ), +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts b/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts index 406654c808186..637fe0a108958 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts +++ b/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts @@ -6,6 +6,119 @@ import { i18n } from '@kbn/i18n'; +export const SECONDS_TIME_RANGE = i18n.translate( + 'xpack.uptime.alerts.timerangeUnitSelectable.secondsOption.ariaLabel', + { + defaultMessage: '"Seconds" time range select item', + } +); + +export const SECONDS = i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.seconds', { + defaultMessage: 'seconds', +}); + +export const MINUTES_TIME_RANGE = i18n.translate( + 'xpack.uptime.alerts.timerangeUnitSelectable.minutesOption.ariaLabel', + { + defaultMessage: '"Minutes" time range select item', + } +); + +export const MINUTES = i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.minutes', { + defaultMessage: 'minutes', +}); + +export const HOURS_TIME_RANGE = i18n.translate( + 'xpack.uptime.alerts.timerangeUnitSelectable.hoursOption.ariaLabel', + { + defaultMessage: '"Hours" time range select item', + } +); + +export const HOURS = i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.hours', { + defaultMessage: 'hours', +}); + +export const DAYS_TIME_RANGE = i18n.translate( + 'xpack.uptime.alerts.timerangeUnitSelectable.daysOption.ariaLabel', + { + defaultMessage: '"Days" time range select item', + } +); + +export const DAYS = i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.days', { + defaultMessage: 'days', +}); + +export const ALERT_KUERY_BAR_ARIA = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel', + { + defaultMessage: 'Input that allows filtering criteria for the monitor status alert', + } +); + +export const OPEN_THE_POPOVER_DOWN_COUNT = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.numTimesExpression.ariaLabel', + { + defaultMessage: 'Open the popover for down count input', + } +); + +export const ENTER_NUMBER_OF_DOWN_COUNTS = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.numTimesField.ariaLabel', + { + defaultMessage: 'Enter number of down counts required to trigger the alert', + } +); + +export const MATCHING_MONITORS_DOWN = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.numTimesExpression.matchingMonitors.description', + { + defaultMessage: 'matching monitors are down >', + } +); + +export const ANY_MONITOR_DOWN = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.numTimesExpression.anyMonitors.description', + { + defaultMessage: 'any monitor is down >', + } +); + +export const OPEN_THE_POPOVER_TIME_RANGE_VALUE = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.timerangeValueExpression.ariaLabel', + { + defaultMessage: 'Open the popover for time range value field', + } +); + +export const ENTER_NUMBER_OF_TIME_UNITS = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.timerangeValueField.ariaLabel', + { + defaultMessage: `Enter the number of time units for the alert's range`, + } +); + +export const ADD_FILTER = i18n.translate('xpack.uptime.alerts.monitorStatus.addFilter', { + defaultMessage: `Add filter`, +}); + +export const LOCATION = i18n.translate('xpack.uptime.alerts.monitorStatus.addFilter.location', { + defaultMessage: `Location`, +}); + +export const TAG = i18n.translate('xpack.uptime.alerts.monitorStatus.addFilter.tag', { + defaultMessage: `Tag`, +}); + +export const PORT = i18n.translate('xpack.uptime.alerts.monitorStatus.addFilter.port', { + defaultMessage: `Port`, +}); + +export const TYPE = i18n.translate('xpack.uptime.alerts.monitorStatus.addFilter.type', { + defaultMessage: `Type`, +}); + export const TlsTranslations = { criteriaAriaLabel: i18n.translate('xpack.uptime.alerts.tls.criteriaExpression.ariaLabel', { defaultMessage: diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap index 31a7e12dbdc29..c7ffc36532b71 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap @@ -21,7 +21,7 @@ exports[`FilterPopover component does not show item list when loading 1`] = ` ownFocus={true} panelPaddingSize="m" withTitle={true} - zIndex={1000} + zIndex={10000} > void; } export const FilterGroupComponent: React.FC = ({ - currentFilter, overviewFilters, loading, - onFilterUpdate, }) => { const { locations, ports, schemes, tags } = overviewFilters; - let filterKueries: Map; - try { - filterKueries = new Map(JSON.parse(currentFilter)); - } catch { - filterKueries = new Map(); - } + const [updatedFieldValues, setUpdatedFieldValues] = useState<{ + fieldName: string; + values: string[]; + }>({ fieldName: '', values: [] }); - /** - * Handle an added or removed value to filter against for an uptime field. - * @param fieldName the name of the field to filter against - * @param values the list of values to use when filter a field - */ - const onFilterFieldChange = (fieldName: string, values: string[]) => { - // add new term to filter map, toggle it off if already present - const updatedFilterMap = new Map(filterKueries); - updatedFilterMap.set(fieldName, values); - Array.from(updatedFilterMap.keys()).forEach(key => { - const value = updatedFilterMap.get(key); - if (value && value.length === 0) { - updatedFilterMap.delete(key); - } - }); + const currentFilters = useFilterUpdate(updatedFieldValues.fieldName, updatedFieldValues.values); - // store the new set of filters - const persistedFilters = Array.from(updatedFilterMap); - onFilterUpdate(persistedFilters.length === 0 ? '' : JSON.stringify(persistedFilters)); + const onFilterFieldChange = (fieldName: string, values: string[]) => { + setUpdatedFieldValues({ fieldName, values }); }; - const getSelectedItems = (fieldName: string) => filterKueries.get(fieldName) || []; + const getSelectedItems = (fieldName: string) => currentFilters.get(fieldName) || []; const filterPopoverProps: FilterPopoverProps[] = [ { @@ -64,9 +45,7 @@ export const FilterGroupComponent: React.FC = ({ id: 'location', items: locations, selectedItems: getSelectedItems('observer.geo.name'), - title: i18n.translate('xpack.uptime.filterBar.options.location.name', { - defaultMessage: 'Location', - }), + title: filterLabels.LOCATION, }, { loading, @@ -76,7 +55,7 @@ export const FilterGroupComponent: React.FC = ({ disabled: ports.length === 0, items: ports.map((p: number) => p.toString()), selectedItems: getSelectedItems('url.port'), - title: i18n.translate('xpack.uptime.filterBar.options.portLabel', { defaultMessage: 'Port' }), + title: filterLabels.PORT, }, { loading, @@ -86,9 +65,7 @@ export const FilterGroupComponent: React.FC = ({ disabled: schemes.length === 0, items: schemes, selectedItems: getSelectedItems('monitor.type'), - title: i18n.translate('xpack.uptime.filterBar.options.schemeLabel', { - defaultMessage: 'Scheme', - }), + title: filterLabels.SCHEME, }, { loading, @@ -98,9 +75,7 @@ export const FilterGroupComponent: React.FC = ({ disabled: tags.length === 0, items: tags, selectedItems: getSelectedItems('tags'), - title: i18n.translate('xpack.uptime.filterBar.options.tagsLabel', { - defaultMessage: 'Tags', - }), + title: filterLabels.TAGS, }, ]; diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx index 3612604fdf116..67cb89745269e 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx @@ -5,56 +5,41 @@ */ import React, { useContext, useEffect } from 'react'; -import { connect } from 'react-redux'; -import { useUrlParams } from '../../../hooks'; +import { useDispatch, useSelector } from 'react-redux'; +import { useGetUrlParams } from '../../../hooks'; import { parseFiltersMap } from './parse_filter_map'; -import { AppState } from '../../../state'; -import { fetchOverviewFilters, GetOverviewFiltersPayload } from '../../../state/actions'; +import { fetchOverviewFilters } from '../../../state/actions'; import { FilterGroupComponent } from './index'; -import { OverviewFilters } from '../../../../common/runtime_types/overview_filters'; import { UptimeRefreshContext } from '../../../contexts'; +import { filterGroupDataSelector } from '../../../state/selectors'; -interface OwnProps { +interface Props { esFilters?: string; } -interface StoreProps { - esKuery: string; - lastRefresh: number; - loading: boolean; - overviewFilters: OverviewFilters; -} - -interface DispatchProps { - loadFilterGroup: typeof fetchOverviewFilters; -} +export const FilterGroup: React.FC = ({ esFilters }: Props) => { + const { lastRefresh } = useContext(UptimeRefreshContext); -type Props = OwnProps & StoreProps & DispatchProps; + const { esKuery, filters: overviewFilters, loading } = useSelector(filterGroupDataSelector); -export const Container: React.FC = ({ - esKuery, - esFilters, - loading, - loadFilterGroup, - overviewFilters, -}: Props) => { - const { lastRefresh } = useContext(UptimeRefreshContext); + const { dateRangeStart, dateRangeEnd, statusFilter, filters: urlFilters } = useGetUrlParams(); - const [getUrlParams, updateUrl] = useUrlParams(); - const { dateRangeStart, dateRangeEnd, statusFilter, filters: urlFilters } = getUrlParams(); + const dispatch = useDispatch(); useEffect(() => { const filterSelections = parseFiltersMap(urlFilters); - loadFilterGroup({ - dateRangeStart, - dateRangeEnd, - locations: filterSelections.locations ?? [], - ports: filterSelections.ports ?? [], - schemes: filterSelections.schemes ?? [], - search: esKuery, - statusFilter, - tags: filterSelections.tags ?? [], - }); + dispatch( + fetchOverviewFilters({ + dateRangeStart, + dateRangeEnd, + locations: filterSelections.locations ?? [], + ports: filterSelections.ports ?? [], + schemes: filterSelections.schemes ?? [], + search: esKuery, + statusFilter, + tags: filterSelections.tags ?? [], + }) + ); }, [ lastRefresh, dateRangeStart, @@ -63,42 +48,8 @@ export const Container: React.FC = ({ esFilters, statusFilter, urlFilters, - loadFilterGroup, + dispatch, ]); - // update filters in the URL from filter group - const onFilterUpdate = (filtersKuery: string) => { - if (urlFilters !== filtersKuery) { - updateUrl({ filters: filtersKuery, pagination: '' }); - } - }; - - return ( - - ); + return ; }; - -const mapStateToProps = ({ - overviewFilters: { loading, filters }, - ui: { esKuery, lastRefresh }, -}: AppState): StoreProps => ({ - esKuery, - overviewFilters: filters, - lastRefresh, - loading, -}); - -const mapDispatchToProps = (dispatch: any): DispatchProps => ({ - loadFilterGroup: (payload: GetOverviewFiltersPayload) => dispatch(fetchOverviewFilters(payload)), -}); - -export const FilterGroup = connect( - // @ts-ignore connect is expecting null | undefined for some reason - mapStateToProps, - mapDispatchToProps -)(Container); diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx index ac65063ee897d..18d40b83be369 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx @@ -20,6 +20,9 @@ export interface FilterPopoverProps { onFilterFieldChange: (fieldName: string, values: string[]) => void; selectedItems: string[]; title: string; + btnContent?: JSX.Element; + forceOpen?: boolean; + setForceOpen?: (val: boolean) => void; } const isItemSelected = (selectedItems: string[], item: string): 'on' | undefined => @@ -34,6 +37,9 @@ export const FilterPopover = ({ onFilterFieldChange, selectedItems, title, + btnContent, + forceOpen, + setForceOpen, }: FilterPopoverProps) => { const [isOpen, setIsOpen] = useState(false); const [itemsToDisplay, setItemsToDisplay] = useState([]); @@ -52,28 +58,33 @@ export const FilterPopover = ({ return ( 0} - numFilters={items.length} - numActiveFilters={tempSelectedItems.length} - onClick={() => { - setIsOpen(!isOpen); - onFilterFieldChange(fieldName, tempSelectedItems); - }} - title={title} - /> + btnContent ?? ( + 0} + numFilters={items.length} + numActiveFilters={tempSelectedItems.length} + onClick={() => { + setIsOpen(!isOpen); + onFilterFieldChange(fieldName, tempSelectedItems); + }} + title={title} + /> + ) } closePopover={() => { setIsOpen(false); onFilterFieldChange(fieldName, tempSelectedItems); + if (setForceOpen) { + setForceOpen(false); + } }} data-test-subj={`filter-popover_${id}`} id={id} - isOpen={isOpen} + isOpen={isOpen || forceOpen} ownFocus={true} withTitle - zIndex={1000} + zIndex={10000} > { + const [getUrlParams, updateUrl] = useUrlParams(); + + const { filters: currentFilters } = getUrlParams(); + + // update filters in the URL from filter group + const onFilterUpdate = (filtersKuery: string) => { + if (currentFilters !== filtersKuery) { + updateUrl({ filters: filtersKuery, pagination: '' }); + } + }; + + let filterKueries: Map; + try { + filterKueries = new Map(JSON.parse(currentFilters)); + } catch { + filterKueries = new Map(); + } + + useEffect(() => { + if (fieldName) { + // add new term to filter map, toggle it off if already present + const updatedFilterMap = new Map(filterKueries); + updatedFilterMap.set(fieldName, values); + Array.from(updatedFilterMap.keys()).forEach(key => { + const value = updatedFilterMap.get(key); + if (value && value.length === 0) { + updatedFilterMap.delete(key); + } + }); + + // store the new set of filters + const persistedFilters = Array.from(updatedFilterMap); + onFilterUpdate(persistedFilters.length === 0 ? '' : JSON.stringify(persistedFilters)); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fieldName, values]); + + return filterKueries; +}; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts index 81402c00e484e..0d18facaa5bbb 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts @@ -174,7 +174,7 @@ describe('monitor status alert type', () => { {{context.downMonitorsWithGeo}}", "iconClass": "uptimeApp", "id": "xpack.uptime.alerts.monitorStatus", - "name": "Uptime monitor status", + "name": , "validate": [Function], } `); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx index f93e17270a192..66e61fbf73b64 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -12,6 +12,7 @@ import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { AlertTypeInitializer } from '.'; import { StatusCheckExecutorParamsType } from '../../../common/runtime_types'; import { AlertMonitorStatus } from '../../components/overview/alerts/alerts_containers'; +import { MonitorStatusTitle } from './monitor_status_title'; import { CLIENT_ALERT_TYPES } from '../../../common/constants'; import { MonitorStatusTranslations } from './translations'; @@ -54,13 +55,13 @@ export const validate = (alertParams: any) => { return { errors }; }; -const { name, defaultActionMessage } = MonitorStatusTranslations; +const { defaultActionMessage } = MonitorStatusTranslations; export const initMonitorStatusAlertType: AlertTypeInitializer = ({ autocomplete, }): AlertTypeModel => ({ id: CLIENT_ALERT_TYPES.MONITOR_STATUS, - name, + name: , iconClass: 'uptimeApp', alertParamsExpression: params => , validate, diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status_title.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status_title.tsx new file mode 100644 index 0000000000000..3fe497f9e88bc --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status_title.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useSelector } from 'react-redux'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText } from '@elastic/eui'; +import { snapshotDataSelector } from '../../state/selectors'; + +export const MonitorStatusTitle = () => { + const { count, loading } = useSelector(snapshotDataSelector); + return ( + + + {' '} + + + {!loading ? ( + + {count.total} monitors + + ) : ( + + )} + + + ); +}; diff --git a/x-pack/plugins/uptime/public/state/selectors/index.ts b/x-pack/plugins/uptime/public/state/selectors/index.ts index 9552538182dd2..2b7eabe727ad1 100644 --- a/x-pack/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/plugins/uptime/public/state/selectors/index.ts @@ -107,3 +107,16 @@ export const monitorListSelector = ({ monitorList, ui: { lastRefresh } }: AppSta monitorList, lastRefresh, }); + +export const overviewFiltersSelector = ({ overviewFilters }: AppState) => { + return overviewFilters.filters; +}; + +export const filterGroupDataSelector = ({ + overviewFilters: { loading, filters }, + ui: { esKuery }, +}: AppState) => ({ + esKuery, + filters, + loading, +}); diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index 53c89eadeced7..1155b6a5cb296 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function UptimePageProvider({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'timePicker']); - const { alerts, common: commonService, monitor, navigation } = getService('uptime'); + const { common: commonService, monitor, navigation } = getService('uptime'); const retry = getService('retry'); return new (class UptimePage { @@ -97,42 +97,9 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo return await commonService.getSnapshotCount(); } - public async openAlertFlyoutAndCreateMonitorStatusAlert({ - alertInterval, - alertName, - alertNumTimes, - alertTags, - alertThrottleInterval, - alertTimerangeSelection, - alertType, - filters, - }: { - alertName: string; - alertTags: string[]; - alertInterval: string; - alertThrottleInterval: string; - alertNumTimes: string; - alertTimerangeSelection: string; - alertType?: string; - filters?: string; - }) { + public async setAlertKueryBarText(filters: string) { const { setKueryBarText } = commonService; - await alerts.openFlyout(); - if (alertType) { - await alerts.openMonitorStatusAlertType(alertType); - } - await alerts.setAlertName(alertName); - await alerts.setAlertTags(alertTags); - await alerts.setAlertInterval(alertInterval); - await alerts.setAlertThrottleInterval(alertThrottleInterval); - if (filters) { - await setKueryBarText('xpack.uptime.alerts.monitorStatus.filterBar', filters); - } - await alerts.setAlertStatusNumTimes(alertNumTimes); - await alerts.setAlertTimerangeSelection(alertTimerangeSelection); - await alerts.setMonitorStatusSelectableToHours(); - await alerts.setLocationsSelectable(); - await alerts.clickSaveAlertButtion(); + await setKueryBarText('xpack.uptime.alerts.monitorStatus.filterBar', filters); } public async setMonitorListPageSize(size: number): Promise { diff --git a/x-pack/test/functional/services/uptime/alerts.ts b/x-pack/test/functional/services/uptime/alerts.ts index 03620621d68f1..dc10fcccaa6ce 100644 --- a/x-pack/test/functional/services/uptime/alerts.ts +++ b/x-pack/test/functional/services/uptime/alerts.ts @@ -77,19 +77,37 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) { ['xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.hoursOption'] ); }, - async setLocationsSelectable() { - await testSubjects.click( - 'xpack.uptime.alerts.monitorStatus.locationsSelectionExpression', - 5000 - ); - await testSubjects.click('xpack.uptime.alerts.monitorStatus.locationsSelectionSwitch', 5000); - await testSubjects.click( - 'xpack.uptime.alerts.monitorStatus.locationsSelectionSelectable', - 5000 - ); + async clickAddFilter() { + await testSubjects.click('uptimeCreateAlertAddFilter'); + }, + async clickAddFilterLocation() { + await this.clickAddFilter(); + await testSubjects.click('uptimeAlertAddFilter.observer.geo.name'); + }, + async clickAddFilterPort() { + await this.clickAddFilter(); + await testSubjects.click('uptimeAlertAddFilter.url.port'); + }, + async clickAddFilterType() { + await this.clickAddFilter(); + await testSubjects.click('uptimeAlertAddFilter.monitor.type'); + }, + async clickLocationExpression(filter: string) { + await testSubjects.click('uptimeCreateStatusAlert.filter_location'); + await testSubjects.click(`filter-popover-item_${filter}`); + return browser.pressKeys(browser.keys.ESCAPE); + }, + async clickPortExpression(filter: string) { + await testSubjects.click('uptimeCreateStatusAlert.filter_port'); + await testSubjects.click(`filter-popover-item_${filter}`); + return browser.pressKeys(browser.keys.ESCAPE); + }, + async clickTypeExpression(filter: string) { + await testSubjects.click('uptimeCreateStatusAlert.filter_scheme'); + await testSubjects.click(`filter-popover-item_${filter}`); return browser.pressKeys(browser.keys.ESCAPE); }, - async clickSaveAlertButtion() { + async clickSaveAlertButton() { return testSubjects.click('saveAlertButton'); }, }; diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index 3e5a8c57c4c7e..fb4f34d65f9b0 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -14,20 +14,67 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'uptime']); const supertest = getService('supertest'); const retry = getService('retry'); + let alerts: any; - it('posts an alert, verfies its presence, and deletes the alert', async () => { + before(async () => { + alerts = getService('uptime').alerts; + }); + + it('can open alert flyout', async () => { await pageObjects.uptime.goToUptimeOverviewAndLoadData(DEFAULT_DATE_START, DEFAULT_DATE_END); + await alerts.openFlyout(); + }); - await pageObjects.uptime.openAlertFlyoutAndCreateMonitorStatusAlert({ - alertInterval: '11', - alertName: 'uptime-test', - alertNumTimes: '3', - alertTags: ['uptime', 'another'], - alertThrottleInterval: '30', - alertTimerangeSelection: '1', - filters: 'monitor.id: "0001-up"', - }); + it('can set alert name', async () => { + await alerts.setAlertName('uptime-test'); + }); + + it('can set alert tags', async () => { + await alerts.setAlertTags(['uptime', 'another']); + }); + + it('can set alert interval', async () => { + await alerts.setAlertInterval('11'); + }); + + it('can set alert throttle interval', async () => { + await alerts.setAlertThrottleInterval('30'); + }); + + it('can set alert status number of time', async () => { + await alerts.setAlertStatusNumTimes('3'); + }); + it('can set alert time range', async () => { + await alerts.setAlertTimerangeSelection('1'); + }); + it('can set monitor hours', async () => { + await alerts.setMonitorStatusSelectableToHours(); + }); + + it('can set kuery bar filters', async () => { + await pageObjects.uptime.setAlertKueryBarText('monitor.id: "0001-up"'); + }); + + it('can select location filter', async () => { + await alerts.clickAddFilterLocation(); + await alerts.clickLocationExpression('mpls'); + }); + + it('can select port filter', async () => { + await alerts.clickAddFilterPort(); + await alerts.clickPortExpression('5678'); + }); + + it('can select type/scheme filter', async () => { + await alerts.clickAddFilterType(); + await alerts.clickTypeExpression('http'); + }); + + it('can save alert', async () => { + await alerts.clickSaveAlertButton(); + }); + it('posts an alert, verifies its presence, and deletes the alert', async () => { // The creation of the alert could take some time, so the first few times we query after // the previous line resolves, the API may not be done creating the alert yet, so we // put the fetch code in a retry block with a timeout. @@ -67,7 +114,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(timerange.to).to.be('now'); expect(locations).to.eql(['mpls']); expect(filters).to.eql( - '{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],"minimum_should_match":1}}' + '{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],' + + '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"mpls"}}],' + + '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"url.port":5678}}],' + + '"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}' ); } finally { await supertest From cd595ad2e25c40d37fbec3f41e0e2f5d9e4da844 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Tue, 5 May 2020 14:23:52 +0300 Subject: [PATCH 10/75] [TSVB] Fix std deviation band mode (#64413) * Fix std_deviation_bands mode * Fix jest test Co-authored-by: Elastic Machine --- .../visualizations/views/timeseries/index.js | 6 +-- .../server/lib/vis_data/helpers/index.js | 37 ++++---------- .../series/std_deviation_bands.js | 50 +++++++++---------- .../series/std_deviation_bands.test.js | 23 +++------ .../series/std_deviation_sibling.js | 42 +++++++--------- .../series/std_deviation_sibling.test.js | 23 +++------ 6 files changed, 70 insertions(+), 111 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index 874fc037c4896..dc0c4310de576 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -87,7 +87,7 @@ export const TimeSeries = ({ const tooltipFormatter = decorateFormatter(xAxisFormatter); const uiSettings = getUISettings(); const timeZone = getTimezone(uiSettings); - const hasBarChart = series.some(({ bars }) => bars.show); + const hasBarChart = series.some(({ bars }) => bars?.show); // compute the theme based on the bg color const theme = getTheme(darkMode, backgroundColor); @@ -180,7 +180,7 @@ export const TimeSeries = ({ // Only use color mapping if there is no color from the server const finalColor = color ?? colors.mappedColors.mapping[label]; - if (bars.show) { + if (bars?.show) { return ( results => { const metric = getLastMetric(series); - if (metric.type === 'std_deviation' && metric.mode === 'band') { - getSplits(resp, panel, series, meta).forEach(split => { - const upper = split.timeseries.buckets.map( - mapBucket(_.assign({}, metric, { mode: 'upper' })) - ); - const lower = split.timeseries.buckets.map( - mapBucket(_.assign({}, metric, { mode: 'lower' })) - ); - results.push({ - id: `${split.id}:upper`, - label: split.label, - color: split.color, - lines: { show: true, fill: 0.5, lineWidth: 0 }, - points: { show: false }, - fillBetween: `${split.id}:lower`, - data: upper, - }); + if (metric.type === METRIC_TYPES.STD_DEVIATION && metric.mode === 'band') { + getSplits(resp, panel, series, meta).forEach(({ id, color, label, timeseries }) => { + const data = timeseries.buckets.map(bucket => [ + bucket.key, + getAggValue(bucket, { ...metric, mode: 'upper' }), + getAggValue(bucket, { ...metric, mode: 'lower' }), + ]); + results.push({ - id: `${split.id}:lower`, - color: split.color, - lines: { show: true, fill: false, lineWidth: 0 }, + id, + label, + color, + data, + lines: { + show: series.chart_type === 'line', + fill: 0.5, + lineWidth: 0, + mode: 'band', + }, + bars: { + show: series.chart_type === 'bar', + fill: 0.5, + mode: 'band', + }, points: { show: false }, - data: lower, }); }); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js index 77949ff94dc4c..a229646ba8f3f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js @@ -86,29 +86,18 @@ describe('stdDeviationBands(resp, panel, series)', () => { test('creates a series', () => { const next = results => results; const results = stdDeviationBands(resp, panel, series)(next)([]); - expect(results).toHaveLength(2); + expect(results).toHaveLength(1); expect(results[0]).toEqual({ - id: 'test:upper', + id: 'test', label: 'Std. Deviation of cpu', color: 'rgb(255, 0, 0)', - lines: { show: true, fill: 0.5, lineWidth: 0 }, - points: { show: false }, - fillBetween: 'test:lower', - data: [ - [1, 3.2], - [2, 3.5], - ], - }); - - expect(results[1]).toEqual({ - id: 'test:lower', - color: 'rgb(255, 0, 0)', - lines: { show: true, fill: false, lineWidth: 0 }, + lines: { show: true, fill: 0.5, lineWidth: 0, mode: 'band' }, + bars: { show: false, fill: 0.5, mode: 'band' }, points: { show: false }, data: [ - [1, 0.2], - [2, 0.5], + [1, 3.2, 0.2], + [2, 3.5, 0.5], ], }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.js index 96ead42c55253..1c6ee94050a62 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.js @@ -17,40 +17,36 @@ * under the License. */ -import _ from 'lodash'; -import { getSplits } from '../../helpers/get_splits'; -import { getLastMetric } from '../../helpers/get_last_metric'; -import { getSiblingAggValue } from '../../helpers/get_sibling_agg_value'; +import { getSplits, getLastMetric, getSiblingAggValue } from '../../helpers'; export function stdDeviationSibling(resp, panel, series, meta) { return next => results => { const metric = getLastMetric(series); if (metric.mode === 'band' && metric.type === 'std_deviation_bucket') { getSplits(resp, panel, series, meta).forEach(split => { - const mapBucketByMode = mode => { - return bucket => { - return [bucket.key, getSiblingAggValue(split, _.assign({}, metric, { mode }))]; - }; - }; + const data = split.timeseries.buckets.map(bucket => [ + bucket.key, + getSiblingAggValue(split, { ...metric, mode: 'upper' }), + getSiblingAggValue(split, { ...metric, mode: 'lower' }), + ]); - const upperData = split.timeseries.buckets.map(mapBucketByMode('upper')); - const lowerData = split.timeseries.buckets.map(mapBucketByMode('lower')); - - results.push({ - id: `${split.id}:lower`, - lines: { show: true, fill: false, lineWidth: 0 }, - points: { show: false }, - color: split.color, - data: lowerData, - }); results.push({ - id: `${split.id}:upper`, + id: split.id, label: split.label, color: split.color, - lines: { show: true, fill: 0.5, lineWidth: 0 }, + lines: { + show: series.chart_type === 'line', + fill: 0.5, + lineWidth: 0, + mode: 'band', + }, + bars: { + show: series.chart_type === 'bar', + fill: 0.5, + mode: 'band', + }, points: { show: false }, - fillBetween: `${split.id}:lower`, - data: upperData, + data, }); }); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js index adc5a3a4a991b..b93d929d5157a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js @@ -86,29 +86,18 @@ describe('stdDeviationSibling(resp, panel, series)', () => { test('creates a series', () => { const next = results => results; const results = stdDeviationSibling(resp, panel, series)(next)([]); - expect(results).toHaveLength(2); + expect(results).toHaveLength(1); expect(results[0]).toEqual({ - id: 'test:lower', + id: 'test', color: 'rgb(255, 0, 0)', - lines: { show: true, fill: false, lineWidth: 0 }, - points: { show: false }, - data: [ - [1, 0.01], - [2, 0.01], - ], - }); - - expect(results[1]).toEqual({ - id: 'test:upper', label: 'Overall Std. Deviation of Average of cpu', - color: 'rgb(255, 0, 0)', - fillBetween: 'test:lower', - lines: { show: true, fill: 0.5, lineWidth: 0 }, + lines: { show: true, fill: 0.5, lineWidth: 0, mode: 'band' }, + bars: { show: false, fill: 0.5, mode: 'band' }, points: { show: false }, data: [ - [1, 0.7], - [2, 0.7], + [1, 0.7, 0.01], + [2, 0.7, 0.01], ], }); }); From 5c2fb4ce386ccba823d6b2b7e834ff9443c180e2 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 5 May 2020 13:45:30 +0200 Subject: [PATCH 11/75] [SIEM] Adds 'Configure connector' Cypress test (#64807) * adds 'Configures a new connector' test * refactor code * updates configure_cases screen selectors Co-authored-by: Elastic Machine --- .../integration/cases_connectors.spec.ts | 47 +++++++++++++++++ x-pack/plugins/siem/cypress/objects/case.ts | 14 +++++ .../plugins/siem/cypress/screens/all_cases.ts | 2 + .../siem/cypress/screens/configure_cases.ts | 30 +++++++++++ .../plugins/siem/cypress/tasks/all_cases.ts | 10 +++- .../siem/cypress/tasks/configure_cases.ts | 52 +++++++++++++++++++ 6 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts create mode 100644 x-pack/plugins/siem/cypress/screens/configure_cases.ts create mode 100644 x-pack/plugins/siem/cypress/tasks/configure_cases.ts diff --git a/x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts b/x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts new file mode 100644 index 0000000000000..2d650b1bbd9d1 --- /dev/null +++ b/x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { serviceNowConnector } from '../objects/case'; + +import { TOASTER } from '../screens/configure_cases'; + +import { goToEditExternalConnection } from '../tasks/all_cases'; +import { + addServiceNowConnector, + openAddNewConnectorOption, + saveChanges, + selectLastConnectorCreated, +} from '../tasks/configure_cases'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { CASES } from '../urls/navigation'; + +describe('Cases connectors', () => { + before(() => { + cy.server(); + cy.route('POST', '**/api/action').as('createConnector'); + cy.route('POST', '**/api/cases/configure').as('saveConnector'); + }); + + it('Configures a new connector', () => { + loginAndWaitForPageWithoutDateRange(CASES); + goToEditExternalConnection(); + openAddNewConnectorOption(); + addServiceNowConnector(serviceNowConnector); + + cy.wait('@createConnector') + .its('status') + .should('eql', 200); + cy.get(TOASTER).should('have.text', "Created 'New connector'"); + + selectLastConnectorCreated(); + saveChanges(); + + cy.wait('@saveConnector', { timeout: 10000 }) + .its('status') + .should('eql', 200); + cy.get(TOASTER).should('have.text', 'Saved external connection settings'); + }); +}); diff --git a/x-pack/plugins/siem/cypress/objects/case.ts b/x-pack/plugins/siem/cypress/objects/case.ts index 1c7bc34bca417..12d3f925169af 100644 --- a/x-pack/plugins/siem/cypress/objects/case.ts +++ b/x-pack/plugins/siem/cypress/objects/case.ts @@ -14,6 +14,13 @@ export interface TestCase { reporter: string; } +export interface Connector { + connectorName: string; + URL: string; + username: string; + password: string; +} + const caseTimeline: Timeline = { title: 'SIEM test', description: 'description', @@ -27,3 +34,10 @@ export const case1: TestCase = { timeline: caseTimeline, reporter: 'elastic', }; + +export const serviceNowConnector: Connector = { + connectorName: 'New connector', + URL: 'https://www.test.service-now.com', + username: 'Username Name', + password: 'password', +}; diff --git a/x-pack/plugins/siem/cypress/screens/all_cases.ts b/x-pack/plugins/siem/cypress/screens/all_cases.ts index b1e4c66515352..4fa6b69eea7c3 100644 --- a/x-pack/plugins/siem/cypress/screens/all_cases.ts +++ b/x-pack/plugins/siem/cypress/screens/all_cases.ts @@ -39,3 +39,5 @@ export const ALL_CASES_TAGS = (index: number) => { }; export const ALL_CASES_TAGS_COUNT = '[data-test-subj="options-filter-popover-button-Tags"]'; + +export const EDIT_EXTERNAL_CONNECTION = '[data-test-subj="configure-case-button"]'; diff --git a/x-pack/plugins/siem/cypress/screens/configure_cases.ts b/x-pack/plugins/siem/cypress/screens/configure_cases.ts new file mode 100644 index 0000000000000..5a1e897c43e27 --- /dev/null +++ b/x-pack/plugins/siem/cypress/screens/configure_cases.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ADD_NEW_CONNECTOR_OPTION_LINK = + '[data-test-subj="case-configure-add-connector-button"]'; + +export const CONNECTOR = (id: string) => { + return `[data-test-subj='dropdown-connector-${id}']`; +}; + +export const CONNECTOR_NAME = '[data-test-subj="nameInput"]'; + +export const CONNECTORS_DROPDOWN = '[data-test-subj="dropdown-connectors"]'; + +export const PASSWORD = '[data-test-subj="connector-servicenow-password-form-input"]'; + +export const SAVE_BTN = '[data-test-subj="saveNewActionButton"]'; + +export const SAVE_CHANGES_BTN = '[data-test-subj="case-configure-action-bottom-bar-save-button"]'; + +export const SERVICE_NOW_CONNECTOR_CARD = '[data-test-subj=".servicenow-card"]'; + +export const TOASTER = '[data-test-subj="euiToastHeader"]'; + +export const URL = '[data-test-subj="apiUrlFromInput"]'; + +export const USERNAME = '[data-test-subj="connector-servicenow-username-form-input"]'; diff --git a/x-pack/plugins/siem/cypress/tasks/all_cases.ts b/x-pack/plugins/siem/cypress/tasks/all_cases.ts index f374532201324..8ebe35e173e59 100644 --- a/x-pack/plugins/siem/cypress/tasks/all_cases.ts +++ b/x-pack/plugins/siem/cypress/tasks/all_cases.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ALL_CASES_NAME, ALL_CASES_CREATE_NEW_CASE_BTN } from '../screens/all_cases'; +import { + ALL_CASES_NAME, + ALL_CASES_CREATE_NEW_CASE_BTN, + EDIT_EXTERNAL_CONNECTION, +} from '../screens/all_cases'; export const goToCreateNewCase = () => { cy.get(ALL_CASES_CREATE_NEW_CASE_BTN).click({ force: true }); @@ -13,3 +17,7 @@ export const goToCreateNewCase = () => { export const goToCaseDetails = () => { cy.get(ALL_CASES_NAME).click({ force: true }); }; + +export const goToEditExternalConnection = () => { + cy.get(EDIT_EXTERNAL_CONNECTION).click({ force: true }); +}; diff --git a/x-pack/plugins/siem/cypress/tasks/configure_cases.ts b/x-pack/plugins/siem/cypress/tasks/configure_cases.ts new file mode 100644 index 0000000000000..9172e02708ae7 --- /dev/null +++ b/x-pack/plugins/siem/cypress/tasks/configure_cases.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ADD_NEW_CONNECTOR_OPTION_LINK, + CONNECTOR, + CONNECTOR_NAME, + CONNECTORS_DROPDOWN, + PASSWORD, + SAVE_BTN, + SAVE_CHANGES_BTN, + SERVICE_NOW_CONNECTOR_CARD, + URL, + USERNAME, +} from '../screens/configure_cases'; +import { MAIN_PAGE } from '../screens/siem_main'; + +import { Connector } from '../objects/case'; + +export const addServiceNowConnector = (connector: Connector) => { + cy.get(SERVICE_NOW_CONNECTOR_CARD).click(); + cy.get(CONNECTOR_NAME).type(connector.connectorName); + cy.get(URL).type(connector.URL); + cy.get(USERNAME).type(connector.username); + cy.get(PASSWORD).type(connector.password); + cy.get(SAVE_BTN).click({ force: true }); +}; + +export const openAddNewConnectorOption = () => { + cy.get(MAIN_PAGE).then($page => { + if ($page.find(SERVICE_NOW_CONNECTOR_CARD).length !== 1) { + cy.wait(1000); + cy.get(ADD_NEW_CONNECTOR_OPTION_LINK).click({ force: true }); + } + }); +}; + +export const saveChanges = () => { + cy.get(SAVE_CHANGES_BTN).click(); +}; + +export const selectLastConnectorCreated = () => { + cy.get(CONNECTORS_DROPDOWN).click({ force: true }); + cy.get('@createConnector') + .its('response') + .then(response => { + cy.get(CONNECTOR(response.body.id)).click(); + }); +}; From 03f4622414cd35276e7129f2b5db16b07970aea2 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 5 May 2020 07:46:38 -0400 Subject: [PATCH 12/75] Feature fleet enrollment instructions (#65176) --- .../enrollment_instructions/index.tsx | 1 - .../enrollment_instructions/manual/index.tsx | 58 +++-- .../enrollment_instructions/shell/index.tsx | 92 -------- .../details_page/components/yaml/index.tsx | 49 +---- .../agent_enrollment_flyout/index.tsx | 107 --------- .../agent_enrollment_flyout/instructions.tsx | 128 ----------- .../agent_enrollment_flyout/key_selection.tsx | 203 ------------------ .../fleet/agent_list_page/components/index.ts | 6 - .../sections/fleet/agent_list_page/index.tsx | 2 +- .../agent_config_datasource_badges.tsx | 59 +++++ .../config_selection.tsx | 129 +++++++++++ .../agent_enrollment_flyout/index.tsx | 131 +++++++++++ .../agent_reassign_config_flyout/index.tsx | 52 +---- .../sections/fleet/components/index.tsx | 1 + .../sections/fleet/components/list_layout.tsx | 2 +- .../sections/overview/index.tsx | 2 +- .../plugins/ingest_manager/server/plugin.ts | 15 +- .../server/routes/install_script/index.ts | 33 ++- .../server/routes/setup/handlers.ts | 2 +- .../server/services/app_context.ts | 23 +- .../server/services/install_script/index.ts | 8 +- .../install_script/install_templates/linux.ts | 21 ++ .../install_script/install_templates/macos.ts | 14 +- .../install_script/install_templates/types.ts | 5 +- .../ingest_manager/server/services/setup.ts | 14 +- .../server/types/rest_spec/install_script.ts | 2 +- .../translations/translations/ja-JP.json | 14 -- .../translations/translations/zh-CN.json | 14 -- 28 files changed, 473 insertions(+), 714 deletions(-) delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/shell/index.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/key_selection.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/index.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_config_datasource_badges.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx create mode 100644 x-pack/plugins/ingest_manager/server/services/install_script/install_templates/linux.ts diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/index.tsx index 34233a00e630a..bc0a250b9a809 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/index.tsx @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ShellEnrollmentInstructions } from './shell'; export { ManualInstructions } from './manual'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx index b1da4583b74cc..5d2938f3e9fa0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx @@ -4,33 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiText, EuiSpacer } from '@elastic/eui'; import React from 'react'; +import { EuiText, EuiSpacer, EuiCode, EuiCodeBlock, EuiCopy, EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EnrollmentAPIKey } from '../../../types'; -export const ManualInstructions: React.FunctionComponent = () => { +interface Props { + kibanaUrl: string; + apiKey: EnrollmentAPIKey; + kibanaCASha256?: string; +} + +export const ManualInstructions: React.FunctionComponent = ({ + kibanaUrl, + apiKey, + kibanaCASha256, +}) => { + const command = ` +./elastic-agent enroll ${kibanaUrl} ${apiKey.api_key}${ + kibanaCASha256 ? ` --ca_sha256=${kibanaCASha256}` : '' + } +./elastic-agent run`; return ( <> - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vestibulum ullamcorper - turpis vitae interdum. Maecenas orci magna, auctor volutpat pellentesque eu, consectetur id - est. Nunc orci lacus, condimentum vel congue ac, fringilla eget tortor. Aliquam blandit, - nisi et congue euismod, leo lectus blandit risus, eu blandit erat metus sit amet leo. Nam - dictum lobortis condimentum. + agent enroll, + }} + /> - - Vivamus sem sapien, dictum eu tellus vel, rutrum aliquam purus. Cras quis cursus nibh. - Aliquam fermentum ipsum nec turpis luctus lobortis. Nulla facilisi. Etiam nec fringilla - urna, sed vehicula ipsum. Quisque vel pellentesque lorem, at egestas enim. Nunc semper elit - lectus, in sollicitudin erat fermentum in. Pellentesque tempus massa eget purus pharetra - blandit. - + +
{command}
+
- - Mauris congue enim nulla, nec semper est posuere non. Donec et eros eu nisi gravida - malesuada eget in velit. Morbi placerat semper euismod. Suspendisse potenti. Morbi quis - porta erat, quis cursus nulla. Aenean mauris lorem, mollis in mattis et, lobortis a lectus. - + + {copy => ( + + + + )} + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/shell/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/shell/index.tsx deleted file mode 100644 index cb65e31fb74b5..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/shell/index.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { useState } from 'react'; -import { - EuiButtonEmpty, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiCopy, - EuiFieldText, - EuiPopover, -} from '@elastic/eui'; -import { EnrollmentAPIKey } from '../../../types'; - -// No need for i18n as these are platform names -const PLATFORMS = { - macos: 'macOS', - windows: 'Windows', - linux: 'Linux', -}; - -interface Props { - kibanaUrl: string; - kibanaCASha256?: string; - apiKey: EnrollmentAPIKey; -} - -export const ShellEnrollmentInstructions: React.FunctionComponent = ({ - kibanaUrl, - kibanaCASha256, - apiKey, -}) => { - // Platform state - const [currentPlatform, setCurrentPlatform] = useState('macos'); - const [isPlatformOptionsOpen, setIsPlatformOptionsOpen] = useState(false); - - // Build quick installation command - // const quickInstallInstructions = `${ - // kibanaCASha256 ? `CA_SHA256=${kibanaCASha256} ` : '' - // }API_KEY=${ - // apiKey.api_key - // } sh -c "$(curl ${kibanaUrl}/api/ingest_manager/fleet/install/${currentPlatform})"`; - - const quickInstallInstructions = `./elastic-agent enroll ${kibanaUrl} ${apiKey.api_key}`; - - return ( - <> - setIsPlatformOptionsOpen(true)} - > - {PLATFORMS[currentPlatform]} - - } - isOpen={isPlatformOptionsOpen} - closePopover={() => setIsPlatformOptionsOpen(false)} - > - ( - { - setCurrentPlatform(platform as typeof currentPlatform); - setIsPlatformOptionsOpen(false); - }} - > - {name} - - ))} - /> -
- } - append={ - - {copy => } - - } - /> - - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx index 9f2088521ed38..39fa8c6ee8701 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx @@ -6,23 +6,9 @@ import React, { memo } from 'react'; import { dump } from 'js-yaml'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiTitle, - EuiSpacer, - EuiText, - EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiCodeBlock, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { AgentConfig } from '../../../../../types'; -import { - useGetOneAgentConfigFull, - useGetEnrollmentAPIKeys, - useGetOneEnrollmentAPIKey, - useCore, -} from '../../../../../hooks'; -import { ShellEnrollmentInstructions } from '../../../../../components/enrollment_instructions'; +import { useGetOneAgentConfigFull } from '../../../../../hooks'; import { Loading } from '../../../../../components'; const CONFIG_KEYS_ORDER = [ @@ -38,14 +24,7 @@ const CONFIG_KEYS_ORDER = [ ]; export const ConfigYamlView = memo<{ config: AgentConfig }>(({ config }) => { - const core = useCore(); - const fullConfigRequest = useGetOneAgentConfigFull(config.id); - const apiKeysRequest = useGetEnrollmentAPIKeys({ - page: 1, - perPage: 1000, - }); - const apiKeyRequest = useGetOneEnrollmentAPIKey(apiKeysRequest.data?.list?.[0]?.id); if (fullConfigRequest.isLoading && !fullConfigRequest.data) { return ; @@ -72,30 +51,6 @@ export const ConfigYamlView = memo<{ config: AgentConfig }>(({ config }) => { })} - {apiKeyRequest.data && ( - - -

- -

-
- - - - - - -
- )} ); }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx deleted file mode 100644 index e9347ccd2d6c9..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { useState } from 'react'; -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiSpacer, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiButton, - EuiFlyoutFooter, - EuiLink, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { AgentConfig } from '../../../../../types'; -import { APIKeySelection } from './key_selection'; -import { EnrollmentInstructions } from './instructions'; -import { useFleetStatus } from '../../../../../hooks/use_fleet_status'; -import { useLink } from '../../../../../hooks'; -import { FLEET_PATH } from '../../../../../constants'; - -interface Props { - onClose: () => void; - agentConfigs: AgentConfig[]; -} - -export const AgentEnrollmentFlyout: React.FunctionComponent = ({ - onClose, - agentConfigs = [], -}) => { - const fleetStatus = useFleetStatus(); - const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); - - const fleetLink = useLink(FLEET_PATH); - - return ( - - - -

- -

-
-
- - {fleetStatus.isReady ? ( - <> - setSelectedAPIKeyId(keyId)} - /> - - - - ) : ( - <> - - - - ), - }} - /> - - )} - - - - - - - - - {fleetStatus.isReady && ( - - - - - - )} - - -
- ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx deleted file mode 100644 index 1d2f3bd155622..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText, EuiButtonGroup, EuiSteps } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - ShellEnrollmentInstructions, - ManualInstructions, -} from '../../../../../components/enrollment_instructions'; -import { useCore, useGetAgents, useGetOneEnrollmentAPIKey } from '../../../../../hooks'; -import { Loading } from '../../../components'; - -interface Props { - selectedAPIKeyId: string | undefined; -} -function useNewEnrolledAgents() { - // New enrolled agents - const [timestamp] = useState(new Date().toISOString()); - const agentsRequest = useGetAgents( - { - perPage: 100, - page: 1, - showInactive: false, - }, - { - pollIntervalMs: 3000, - } - ); - return React.useMemo(() => { - if (!agentsRequest.data) { - return []; - } - - return agentsRequest.data.list.filter(agent => agent.enrolled_at >= timestamp); - }, [agentsRequest.data, timestamp]); -} - -export const EnrollmentInstructions: React.FunctionComponent = ({ selectedAPIKeyId }) => { - const core = useCore(); - const [installType, setInstallType] = useState<'quickInstall' | 'manual'>('quickInstall'); - - const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId); - - const newAgents = useNewEnrolledAgents(); - if (!apiKey.data) { - return null; - } - - return ( - <> - { - setInstallType(installType === 'manual' ? 'quickInstall' : 'manual'); - }} - buttonSize="m" - isFullWidth - /> - - {installType === 'manual' ? ( - - ) : ( - - ), - }, - { - title: i18n.translate('xpack.ingestManager.agentEnrollment.stepTestAgents', { - defaultMessage: 'Test Agents', - }), - children: ( - - {!newAgents.length ? ( - <> - - - - ) : ( - <> - - - )} - - ), - }, - ]} - /> - )} - - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/key_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/key_selection.tsx deleted file mode 100644 index 67930e51418b0..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/key_selection.tsx +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiSelect, - EuiSpacer, - EuiText, - EuiLink, - EuiFieldText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { AgentConfig } from '../../../../../types'; -import { useInput, useCore, sendRequest, useGetEnrollmentAPIKeys } from '../../../../../hooks'; -import { enrollmentAPIKeyRouteService } from '../../../../../services'; - -interface Props { - onKeyChange: (keyId: string | undefined) => void; - agentConfigs: AgentConfig[]; -} - -function useCreateApiKeyForm(configId: string | undefined, onSuccess: (keyId: string) => void) { - const { notifications } = useCore(); - const [isLoading, setIsLoading] = useState(false); - const apiKeyNameInput = useInput(''); - - const onSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - setIsLoading(true); - try { - const res = await sendRequest({ - method: 'post', - path: enrollmentAPIKeyRouteService.getCreatePath(), - body: JSON.stringify({ - name: apiKeyNameInput.value, - config_id: configId, - }), - }); - apiKeyNameInput.clear(); - setIsLoading(false); - onSuccess(res.data.item.id); - } catch (err) { - notifications.toasts.addError(err as Error, { - title: 'Error', - }); - setIsLoading(false); - } - }; - - return { - isLoading, - onSubmit, - apiKeyNameInput, - }; -} - -export const APIKeySelection: React.FunctionComponent = ({ onKeyChange, agentConfigs }) => { - const enrollmentAPIKeysRequest = useGetEnrollmentAPIKeys({ - page: 1, - perPage: 1000, - }); - - const [selectedState, setSelectedState] = useState<{ - agentConfigId?: string; - enrollmentAPIKeyId?: string; - }>({ - agentConfigId: agentConfigs.length ? agentConfigs[0].id : undefined, - }); - const filteredEnrollmentAPIKeys = React.useMemo(() => { - if (!selectedState.agentConfigId || !enrollmentAPIKeysRequest.data) { - return []; - } - - return enrollmentAPIKeysRequest.data.list.filter( - key => key.config_id === selectedState.agentConfigId - ); - }, [enrollmentAPIKeysRequest.data, selectedState.agentConfigId]); - - // Select first API key when config change - React.useEffect(() => { - if (!selectedState.enrollmentAPIKeyId && filteredEnrollmentAPIKeys.length > 0) { - const enrollmentAPIKeyId = filteredEnrollmentAPIKeys[0].id; - setSelectedState({ - agentConfigId: selectedState.agentConfigId, - enrollmentAPIKeyId, - }); - onKeyChange(enrollmentAPIKeyId); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filteredEnrollmentAPIKeys, selectedState.enrollmentAPIKeyId, selectedState.agentConfigId]); - - const [showAPIKeyForm, setShowAPIKeyForm] = useState(false); - const apiKeyForm = useCreateApiKeyForm(selectedState.agentConfigId, async (keyId: string) => { - const res = await enrollmentAPIKeysRequest.sendRequest(); - setSelectedState({ - ...selectedState, - enrollmentAPIKeyId: res.data?.list.find(key => key.id === keyId)?.id, - }); - setShowAPIKeyForm(false); - }); - - return ( - <> - - - - - - - - } - > - ({ - value: agentConfig.id, - text: agentConfig.name, - }))} - value={selectedState.agentConfigId || undefined} - onChange={e => - setSelectedState({ - agentConfigId: e.target.value, - enrollmentAPIKeyId: undefined, - }) - } - /> - - - - - } - labelAppend={ - - setShowAPIKeyForm(!showAPIKeyForm)} color="primary"> - {showAPIKeyForm ? ( - - ) : ( - - )} - - - } - > - {showAPIKeyForm ? ( -
- - - ) : ( - ({ - value: key.id, - text: key.name, - }))} - value={selectedState.enrollmentAPIKeyId || undefined} - onChange={e => { - setSelectedState({ - ...selectedState, - enrollmentAPIKeyId: e.target.value, - }); - onKeyChange(selectedState.enrollmentAPIKeyId); - }} - /> - )} -
-
-
- - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/index.ts deleted file mode 100644 index c82c82db6f713..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -export { AgentEnrollmentFlyout } from './agent_enrollment_flyout'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 5e7fe745a0c4a..829b0cb69e67b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -25,7 +25,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; import { CSSProperties } from 'styled-components'; -import { AgentEnrollmentFlyout } from './components'; +import { AgentEnrollmentFlyout } from '../components'; import { Agent } from '../../../types'; import { usePagination, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_config_datasource_badges.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_config_datasource_badges.tsx new file mode 100644 index 0000000000000..30bc9dc701427 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_config_datasource_badges.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiText, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { Datasource } from '../../../types'; +import { useGetOneAgentConfig } from '../../../hooks'; +import { PackageIcon } from '../../../components/package_icon'; + +interface Props { + agentConfigId: string; +} + +export const AgentConfigDatasourceBadges: React.FunctionComponent = ({ agentConfigId }) => { + const agentConfigRequest = useGetOneAgentConfig(agentConfigId); + const agentConfig = agentConfigRequest.data ? agentConfigRequest.data.item : null; + + if (!agentConfig) { + return null; + } + return ( + <> + + {agentConfig.datasources.length}, + }} + /> + + + {(agentConfig.datasources as Datasource[]).map((datasource, idx) => { + if (!datasource.package) { + return null; + } + return ( + + + + + + {datasource.package.title} + + + ); + })} + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx new file mode 100644 index 0000000000000..a8cebfdf899a6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSelect, EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui'; +import { AgentConfig } from '../../../../types'; +import { useGetEnrollmentAPIKeys } from '../../../../hooks'; +import { AgentConfigDatasourceBadges } from '../agent_config_datasource_badges'; + +interface Props { + agentConfigs: AgentConfig[]; + onKeyChange: (key: string) => void; +} + +export const EnrollmentStepAgentConfig: React.FC = ({ agentConfigs, onKeyChange }) => { + const [isAuthenticationSettingsOpen, setIsAuthenticationSettingsOpen] = useState(false); + const enrollmentAPIKeysRequest = useGetEnrollmentAPIKeys({ + page: 1, + perPage: 1000, + }); + + const [selectedState, setSelectedState] = useState<{ + agentConfigId?: string; + enrollmentAPIKeyId?: string; + }>({ + agentConfigId: agentConfigs.length ? agentConfigs[0].id : undefined, + }); + const filteredEnrollmentAPIKeys = React.useMemo(() => { + if (!selectedState.agentConfigId || !enrollmentAPIKeysRequest.data) { + return []; + } + + return enrollmentAPIKeysRequest.data.list.filter( + key => key.config_id === selectedState.agentConfigId + ); + }, [enrollmentAPIKeysRequest.data, selectedState.agentConfigId]); + + // Select first API key when config change + React.useEffect(() => { + if (!selectedState.enrollmentAPIKeyId && filteredEnrollmentAPIKeys.length > 0) { + const enrollmentAPIKeyId = filteredEnrollmentAPIKeys[0].id; + setSelectedState({ + agentConfigId: selectedState.agentConfigId, + enrollmentAPIKeyId, + }); + onKeyChange(enrollmentAPIKeyId); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filteredEnrollmentAPIKeys, selectedState.enrollmentAPIKeyId, selectedState.agentConfigId]); + + return ( + <> + + + + } + options={agentConfigs.map(config => ({ + value: config.id, + text: config.name, + }))} + value={selectedState.agentConfigId || undefined} + onChange={e => + setSelectedState({ + agentConfigId: e.target.value, + enrollmentAPIKeyId: undefined, + }) + } + aria-label={i18n.translate( + 'xpack.ingestManager.enrollmentStepAgentConfig.configSelectAriaLabel', + { defaultMessage: 'Agent configuration' } + )} + /> + + {selectedState.agentConfigId && ( + + )} + + setIsAuthenticationSettingsOpen(!isAuthenticationSettingsOpen)} + > + + + {isAuthenticationSettingsOpen && ( + <> + + ({ + value: key.id, + text: key.name, + }))} + value={selectedState.enrollmentAPIKeyId || undefined} + prepend={ + + + + } + onChange={e => { + setSelectedState({ + ...selectedState, + enrollmentAPIKeyId: e.target.value, + }); + onKeyChange(e.target.value); + }} + /> + + )} + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx new file mode 100644 index 0000000000000..002b4772d9216 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiFlyoutFooter, + EuiSteps, + EuiText, + EuiLink, +} from '@elastic/eui'; +import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AgentConfig } from '../../../../types'; +import { EnrollmentStepAgentConfig } from './config_selection'; +import { useGetOneEnrollmentAPIKey, useCore, useGetSettings } from '../../../../hooks'; +import { ManualInstructions } from '../../../../components/enrollment_instructions'; + +interface Props { + onClose: () => void; + agentConfigs: AgentConfig[]; +} + +export const AgentEnrollmentFlyout: React.FunctionComponent = ({ + onClose, + agentConfigs = [], +}) => { + const core = useCore(); + const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); + + const settings = useGetSettings(); + const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId); + + const kibanaUrl = + settings.data?.item?.kibana_url ?? `${window.location.origin}${core.http.basePath.get()}`; + const kibanaCASha256 = settings.data?.item?.kibana_ca_sha256; + + const steps: EuiContainedStepProps[] = [ + { + title: i18n.translate('xpack.ingestManager.agentEnrollment.stepDownloadAgentTitle', { + defaultMessage: 'Download the Elastic Agent', + }), + children: ( + + + + + ), + }} + /> + + ), + }, + { + title: i18n.translate('xpack.ingestManager.agentEnrollment.stepChooseAgentConfigTitle', { + defaultMessage: 'Choose an agent configuration', + }), + children: ( + + ), + }, + { + title: i18n.translate('xpack.ingestManager.agentEnrollment.stepRunAgentTitle', { + defaultMessage: 'Enroll and run the Elastic Agent', + }), + children: apiKey.data && ( + + ), + }, + ]; + + return ( + + + +

+ +

+
+
+ + + + + + + + + + + + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx index 692c60cdce38c..2c103ade31f5b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx @@ -19,17 +19,11 @@ import { EuiSelect, EuiFormRow, EuiText, - EuiBadge, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Datasource, Agent } from '../../../../types'; -import { - useGetOneAgentConfig, - sendPutAgentReassign, - useCore, - useGetAgentConfigs, -} from '../../../../hooks'; -import { PackageIcon } from '../../../../components/package_icon'; +import { Agent } from '../../../../types'; +import { sendPutAgentReassign, useCore, useGetAgentConfigs } from '../../../../hooks'; +import { AgentConfigDatasourceBadges } from '../agent_config_datasource_badges'; interface Props { onClose: () => void; @@ -45,9 +39,6 @@ export const AgentReassignConfigFlyout: React.FunctionComponent = ({ onCl const agentConfigsRequest = useGetAgentConfigs(); const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; - const agentConfigRequest = useGetOneAgentConfig(selectedAgentConfigId); - const agentConfig = agentConfigRequest.data ? agentConfigRequest.data.item : null; - const [isSubmitting, setIsSubmitting] = useState(false); async function onSubmit() { @@ -121,40 +112,9 @@ export const AgentReassignConfigFlyout: React.FunctionComponent = ({ onCl - {agentConfig && ( - - {agentConfig.datasources.length}, - }} - /> - + {selectedAgentConfigId && ( + )} - - {agentConfig && - (agentConfig.datasources as Datasource[]).map((datasource, idx) => { - if (!datasource.package) { - return null; - } - return ( - - - - - - {datasource.package.title} - - - ); - })} @@ -168,7 +128,7 @@ export const AgentReassignConfigFlyout: React.FunctionComponent = ({ onCl ; savedObjects: SavedObjectsServiceStart; isProductionMode: boolean; - serverInfo?: HttpServerInfo; + kibanaVersion: string; cloud?: CloudSetup; + httpSetup?: HttpServiceSetup; } export type IngestManagerSetupContract = void; @@ -108,15 +109,17 @@ export class IngestManagerPlugin private cloud: CloudSetup | undefined; private isProductionMode: boolean; - private serverInfo: HttpServerInfo | undefined; + private kibanaVersion: string; + private httpSetup: HttpServiceSetup | undefined; constructor(private readonly initializerContext: PluginInitializerContext) { this.config$ = this.initializerContext.config.create(); this.isProductionMode = this.initializerContext.env.mode.prod; + this.kibanaVersion = this.initializerContext.env.packageInfo.version; } public async setup(core: CoreSetup, deps: IngestManagerSetupDeps) { - this.serverInfo = core.http.getServerInfo(); + this.httpSetup = core.http; this.licensing$ = deps.licensing.license$; if (deps.security) { this.security = deps.security; @@ -179,7 +182,6 @@ export class IngestManagerPlugin registerEnrollmentApiKeyRoutes(router); registerInstallScriptRoutes({ router, - serverInfo: core.http.getServerInfo(), basePath: core.http.basePath, }); } @@ -197,7 +199,8 @@ export class IngestManagerPlugin config$: this.config$, savedObjects: core.savedObjects, isProductionMode: this.isProductionMode, - serverInfo: this.serverInfo, + kibanaVersion: this.kibanaVersion, + httpSetup: this.httpSetup, cloud: this.cloud, }); licenseService.start(this.licensing$); diff --git a/x-pack/plugins/ingest_manager/server/routes/install_script/index.ts b/x-pack/plugins/ingest_manager/server/routes/install_script/index.ts index b007e61594e9d..2a8d4fdbec497 100644 --- a/x-pack/plugins/ingest_manager/server/routes/install_script/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/install_script/index.ts @@ -4,27 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ import url from 'url'; -import { IRouter, BasePath, HttpServerInfo, KibanaRequest } from 'src/core/server'; +import { IRouter, BasePath, KibanaRequest } from 'src/core/server'; import { INSTALL_SCRIPT_API_ROUTES } from '../../constants'; import { getScript } from '../../services/install_script'; import { InstallScriptRequestSchema } from '../../types'; +import { appContextService, settingsService } from '../../services'; + +function getInternalUserSOClient(request: KibanaRequest) { + // soClient as kibana internal users, be carefull on how you use it, security is not enabled + return appContextService.getSavedObjects().getScopedClient(request, { + excludedWrappers: ['security'], + }); +} export const registerRoutes = ({ router, - basePath, - serverInfo, }: { router: IRouter; basePath: Pick; - serverInfo: HttpServerInfo; }) => { - const kibanaUrl = url.format({ - protocol: serverInfo.protocol, - hostname: serverInfo.host, - port: serverInfo.port, - pathname: basePath.serverBasePath, - }); - router.get( { path: INSTALL_SCRIPT_API_ROUTES, @@ -36,6 +34,19 @@ export const registerRoutes = ({ request: KibanaRequest<{ osType: 'macos' }>, response ) { + const soClient = getInternalUserSOClient(request); + const http = appContextService.getHttpSetup(); + const serverInfo = http.getServerInfo(); + const basePath = http.basePath; + const kibanaUrl = + (await settingsService.getSettings(soClient)).kibana_url || + url.format({ + protocol: serverInfo.protocol, + hostname: serverInfo.host, + port: serverInfo.port, + pathname: basePath.serverBasePath, + }); + const script = getScript(request.params.osType, kibanaUrl); return response.ok({ body: script }); diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts index 542dfa9cefe8f..abe5f3620d214 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts @@ -13,7 +13,7 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re try { const isAdminUserSetup = (await outputService.getAdminUser(soClient)) !== null; const isApiKeysEnabled = await appContextService.getSecurity().authc.areAPIKeysEnabled(); - const isTLSEnabled = appContextService.getServerInfo().protocol === 'https'; + const isTLSEnabled = appContextService.getHttpSetup().getServerInfo().protocol === 'https'; const isProductionMode = appContextService.getIsProductionMode(); const isCloud = appContextService.getCloud()?.isCloudEnabled ?? false; const isTLSCheckDisabled = appContextService.getConfig()?.fleet?.tlsCheckDisabled ?? false; diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index 5e538ad84b4c2..6da0a137fa087 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -5,7 +5,7 @@ */ import { BehaviorSubject, Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { SavedObjectsServiceStart, HttpServerInfo } from 'src/core/server'; +import { SavedObjectsServiceStart, HttpServiceSetup } from 'src/core/server'; import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../../security/server'; import { IngestManagerConfigType } from '../../common'; @@ -18,17 +18,19 @@ class AppContextService { private config$?: Observable; private configSubject$?: BehaviorSubject; private savedObjects: SavedObjectsServiceStart | undefined; - private serverInfo: HttpServerInfo | undefined; private isProductionMode: boolean = false; + private kibanaVersion: string | undefined; private cloud?: CloudSetup; + private httpSetup?: HttpServiceSetup; public async start(appContext: IngestManagerAppContext) { this.encryptedSavedObjects = appContext.encryptedSavedObjects; this.security = appContext.security; this.savedObjects = appContext.savedObjects; - this.serverInfo = appContext.serverInfo; this.isProductionMode = appContext.isProductionMode; this.cloud = appContext.cloud; + this.kibanaVersion = appContext.kibanaVersion; + this.httpSetup = appContext.httpSetup; if (appContext.config$) { this.config$ = appContext.config$; @@ -77,11 +79,18 @@ class AppContextService { return this.isProductionMode; } - public getServerInfo() { - if (!this.serverInfo) { - throw new Error('Server info not set.'); + public getHttpSetup() { + if (!this.httpSetup) { + throw new Error('HttpServiceSetup not set.'); } - return this.serverInfo; + return this.httpSetup; + } + + public getKibanaVersion() { + if (!this.kibanaVersion) { + throw new Error('Kibana version is not set.'); + } + return this.kibanaVersion; } } diff --git a/x-pack/plugins/ingest_manager/server/services/install_script/index.ts b/x-pack/plugins/ingest_manager/server/services/install_script/index.ts index 7e7f8d2a3734b..02386531f5d61 100644 --- a/x-pack/plugins/ingest_manager/server/services/install_script/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/install_script/index.ts @@ -4,14 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { appContextService } from '../app_context'; import { macosInstallTemplate } from './install_templates/macos'; +import { linuxInstallTemplate } from './install_templates/linux'; -export function getScript(osType: 'macos', kibanaUrl: string): string { - const variables = { kibanaUrl }; +export function getScript(osType: 'macos' | 'linux', kibanaUrl: string): string { + const variables = { kibanaUrl, kibanaVersion: appContextService.getKibanaVersion() }; switch (osType) { case 'macos': return macosInstallTemplate(variables); + case 'linux': + return linuxInstallTemplate(variables); default: throw new Error(`${osType} is not supported.`); } diff --git a/x-pack/plugins/ingest_manager/server/services/install_script/install_templates/linux.ts b/x-pack/plugins/ingest_manager/server/services/install_script/install_templates/linux.ts new file mode 100644 index 0000000000000..0bb68c40bc580 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/install_script/install_templates/linux.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InstallTemplateFunction } from './types'; + +export const linuxInstallTemplate: InstallTemplateFunction = variables => { + const artifact = `elastic-agent-${variables.kibanaVersion}-linux-x86_64`; + + return `#!/bin/sh + +set -e +curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/${artifact}.tar.gz +tar -xzvf ${artifact}.tar.gz +cd ${artifact} +./elastic-agent enroll ${variables.kibanaUrl} $API_KEY --force +./elastic-agent run +`; +}; diff --git a/x-pack/plugins/ingest_manager/server/services/install_script/install_templates/macos.ts b/x-pack/plugins/ingest_manager/server/services/install_script/install_templates/macos.ts index e59dc6174b40f..11bb58d184d33 100644 --- a/x-pack/plugins/ingest_manager/server/services/install_script/install_templates/macos.ts +++ b/x-pack/plugins/ingest_manager/server/services/install_script/install_templates/macos.ts @@ -4,12 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve } from 'path'; import { InstallTemplateFunction } from './types'; -const PROJECT_ROOT = resolve(__dirname, '../../../../'); -export const macosInstallTemplate: InstallTemplateFunction = variables => `#!/bin/sh +export const macosInstallTemplate: InstallTemplateFunction = variables => { + const artifact = `elastic-agent-${variables.kibanaVersion}-darwin-x86_64`; -eval "node ${PROJECT_ROOT}/scripts/dev_agent --enrollmentApiKey=$API_KEY --kibanaUrl=${variables.kibanaUrl}" + return `#!/bin/sh +set -e +curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/${artifact}.tar.gz +tar -xzvf ${artifact}.tar.gz +cd ${artifact} +./elastic-agent enroll ${variables.kibanaUrl} $API_KEY --force +./elastic-agent run `; +}; diff --git a/x-pack/plugins/ingest_manager/server/services/install_script/install_templates/types.ts b/x-pack/plugins/ingest_manager/server/services/install_script/install_templates/types.ts index a478beaa96cfc..65d57f8ac7dbf 100644 --- a/x-pack/plugins/ingest_manager/server/services/install_script/install_templates/types.ts +++ b/x-pack/plugins/ingest_manager/server/services/install_script/install_templates/types.ts @@ -4,4 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export type InstallTemplateFunction = (variables: { kibanaUrl: string }) => string; +export type InstallTemplateFunction = (variables: { + kibanaUrl: string; + kibanaVersion: string; +}) => string; diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 3619628bd4f8b..22acce8d4a51c 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import url from 'url'; import uuid from 'uuid'; import { SavedObjectsClientContract } from 'src/core/server'; import { CallESAsCurrentUser } from '../types'; @@ -38,10 +39,21 @@ export async function setupIngestManager( agentConfigService.ensureDefaultAgentConfig(soClient), settingsService.getSettings(soClient).catch((e: any) => { if (e.isBoom && e.output.statusCode === 404) { + const http = appContextService.getHttpSetup(); + const serverInfo = http.getServerInfo(); + const basePath = http.basePath; + + const defaultKibanaUrl = url.format({ + protocol: serverInfo.protocol, + hostname: serverInfo.host, + port: serverInfo.port, + pathname: basePath.serverBasePath, + }); + return settingsService.saveSettings(soClient, { agent_auto_upgrade: true, package_auto_upgrade: true, - kibana_url: appContextService.getConfig()?.fleet?.kibana?.host, + kibana_url: appContextService.getConfig()?.fleet?.kibana?.host ?? defaultKibanaUrl, }); } diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/install_script.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/install_script.ts index cf676129cce7a..f872efc006b76 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/install_script.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/install_script.ts @@ -8,6 +8,6 @@ import { schema } from '@kbn/config-schema'; export const InstallScriptRequestSchema = { params: schema.object({ - osType: schema.oneOf([schema.literal('macos')]), + osType: schema.oneOf([schema.literal('macos'), schema.literal('linux')]), }), }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index aeb86144635d9..aab3636e2375b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8219,18 +8219,9 @@ "xpack.ingestManager.agentDetails.statusLabel": "ステータス", "xpack.ingestManager.agentDetails.unexceptedErrorTitle": "エージェントを読み込む間にエラーが発生しました", "xpack.ingestManager.agentDetails.userProvidedMetadataSectionSubtitle": "ユーザー提供メタデータ", - "xpack.ingestManager.agentEnrollment.apiKeySelectionDescription": "ご希望のエージェント構成とプラットフォームをすばやく選択できます。次いで、以下の手順に従ってエージェントをセットアップして登録します。", "xpack.ingestManager.agentEnrollment.cancelButtonLabel": "キャンセル", "xpack.ingestManager.agentEnrollment.continueButtonLabel": "続行", "xpack.ingestManager.agentEnrollment.flyoutTitle": "新しいエージェントを登録", - "xpack.ingestManager.agentEnrollment.installManuallyTitle": "手動でインストール", - "xpack.ingestManager.agentEnrollment.newAgentsMessage": "{count, plural, one {# 新規エージェント} other {# 新規エージェント}}。", - "xpack.ingestManager.agentEnrollment.quickInstallTitle": "簡易インストール", - "xpack.ingestManager.agentEnrollment.selectAgentConfig": "エージェント構成", - "xpack.ingestManager.agentEnrollment.selectAPIKeyTitle": "登録 API キー", - "xpack.ingestManager.agentEnrollment.stepSetupAgents": "Beats エージェントのセットアップ", - "xpack.ingestManager.agentEnrollment.stepTestAgents": "エージェントのテスト", - "xpack.ingestManager.agentEnrollment.testAgentLoadingMessage": "新しいエージェントの登録を待っています", "xpack.ingestManager.agentEventsList.messageColumnTitle": "メッセージ", "xpack.ingestManager.agentEventsList.refreshButton": "更新", "xpack.ingestManager.agentEventsList.subtypeColumnTitle": "サブタイプ", @@ -8340,9 +8331,6 @@ "xpack.ingestManager.deleteDatasource.successSingleNotificationTitle": "データソース「{id}」を削除しました", "xpack.ingestManager.disabledSecurityDescription": "Elastic Fleet を使用するには、Kibana と Elasticsearch でセキュリティを有効にする必要があります。", "xpack.ingestManager.disabledSecurityTitle": "セキュリティが有効ではありません", - "xpack.ingestManager.enrollmentApiKeyForm.namePlaceholder": "名前を選択", - "xpack.ingestManager.enrollmentApiKeyList.createNewButton": "新規キーを作成", - "xpack.ingestManager.enrollmentApiKeyList.useExistingsButton": "既存のキーを使用", "xpack.ingestManager.epm.addDatasourceButtonText": "データソースを作成", "xpack.ingestManager.epm.pageSubtitle": "人気のアプリやサービスのパッケージを参照する", "xpack.ingestManager.epm.pageTitle": "Elastic Package Manager", @@ -8371,8 +8359,6 @@ "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "読み込み中...", "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "エージェントの登録解除エラー", "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "エージェント「{id}」の登録を解除しました", - "xpack.ingestManager.yamlConfig.instructionDescription": "この構成でエージェントを登録するには、ホストで次のコマンドをコピーして実行します。", - "xpack.ingestManager.yamlConfig.instructionTittle": "フリートに登録", "xpack.lens.app.docLoadingError": "保存されたドキュメントの保存中にエラーが発生", "xpack.lens.app.docSavingError": "ドキュメントの保存中にエラーが発生", "xpack.lens.app.indexPatternLoadingError": "インデックスパターンの読み込み中にエラーが発生", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 104d53a93088a..853d56fbdf7a9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8225,18 +8225,9 @@ "xpack.ingestManager.agentDetails.statusLabel": "状态", "xpack.ingestManager.agentDetails.unexceptedErrorTitle": "加载代理时发生错误", "xpack.ingestManager.agentDetails.userProvidedMetadataSectionSubtitle": "用户提供的元数据", - "xpack.ingestManager.agentEnrollment.apiKeySelectionDescription": "快速选择所需的代理配置和平台。然后,根据下面的说明设置和注册代理。", "xpack.ingestManager.agentEnrollment.cancelButtonLabel": "取消", "xpack.ingestManager.agentEnrollment.continueButtonLabel": "继续", "xpack.ingestManager.agentEnrollment.flyoutTitle": "注册新代理", - "xpack.ingestManager.agentEnrollment.installManuallyTitle": "手动安装", - "xpack.ingestManager.agentEnrollment.newAgentsMessage": "{count, plural, one {# 个新代理} other {# 个新代理}}.", - "xpack.ingestManager.agentEnrollment.quickInstallTitle": "快速安装", - "xpack.ingestManager.agentEnrollment.selectAgentConfig": "代理配置", - "xpack.ingestManager.agentEnrollment.selectAPIKeyTitle": "注册 API 密钥", - "xpack.ingestManager.agentEnrollment.stepSetupAgents": "设置 Beats 代理", - "xpack.ingestManager.agentEnrollment.stepTestAgents": "测试代理", - "xpack.ingestManager.agentEnrollment.testAgentLoadingMessage": "正在等候新代理注册", "xpack.ingestManager.agentEventsList.messageColumnTitle": "消息", "xpack.ingestManager.agentEventsList.refreshButton": "刷新", "xpack.ingestManager.agentEventsList.subtypeColumnTitle": "子类型", @@ -8346,9 +8337,6 @@ "xpack.ingestManager.deleteDatasource.successSingleNotificationTitle": "已删除数据源“{id}”", "xpack.ingestManager.disabledSecurityDescription": "必须在 Kibana 和 Elasticsearch 启用安全性,才能使用 Elastic Fleet。", "xpack.ingestManager.disabledSecurityTitle": "安全性未启用", - "xpack.ingestManager.enrollmentApiKeyForm.namePlaceholder": "选择名称", - "xpack.ingestManager.enrollmentApiKeyList.createNewButton": "创建新密钥", - "xpack.ingestManager.enrollmentApiKeyList.useExistingsButton": "使用现有密钥", "xpack.ingestManager.epm.addDatasourceButtonText": "创建数据源", "xpack.ingestManager.epm.pageSubtitle": "浏览热门应用和服务的软件。", "xpack.ingestManager.epm.pageTitle": "Elastic Package Manager", @@ -8377,8 +8365,6 @@ "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "正在加载……", "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "取消注册代理时出错", "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "已取消注册代理“{id}”", - "xpack.ingestManager.yamlConfig.instructionDescription": "要将代理注册到此配置,请在您的主机上复制并运行以下命令。", - "xpack.ingestManager.yamlConfig.instructionTittle": "注册到 fleet", "xpack.lens.app.docLoadingError": "加载已保存文档时出错", "xpack.lens.app.docSavingError": "保存文档时出错", "xpack.lens.app.indexPatternLoadingError": "加载索引模式时出错", From ba3534eca3afed4a039368349d7091dc0779fa7f Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 5 May 2020 13:53:09 +0200 Subject: [PATCH 13/75] disable plugins. they could access ES via SO repository (#65242) --- .../server/http/integration_tests/core_services.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 5726486a0930a..c7925f5b6d821 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -43,7 +43,7 @@ describe('http service', () => { describe('auth', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ plugins: { initialize: false } }); }, 30000); afterEach(async () => { @@ -192,7 +192,7 @@ describe('http service', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ plugins: { initialize: false } }); }, 30000); afterEach(async () => { @@ -326,7 +326,7 @@ describe('http service', () => { describe('#basePath()', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ plugins: { initialize: false } }); }, 30000); afterEach(async () => await root.shutdown()); @@ -355,7 +355,7 @@ describe('http service', () => { describe('elasticsearch', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ plugins: { initialize: false } }); }, 30000); afterEach(async () => { From 4142f575e1baadbbb75f72de687908e4224197a8 Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Tue, 5 May 2020 13:54:22 +0200 Subject: [PATCH 14/75] [Ingest] Datastream list: add icons and dashboard links (#65048) * Read package saved objects in data stream handler. * Render package icon. * Make TableRowAction more generic * Add Actions column to data stream list * Disable dashboard link if no dashboards present. * Data stream list: link to first dashbord found * Update i18n strings * Add nested context menu to link to dashboards * introduces a separate TableRowActionsNested component * moves TableRowActions back into agent config components * Fix i18n label. * Re-add translated strings removed by mistake * Fix i18n issues * Add helper to read a saved object installed by EPM * Display titles from within dashboard saved objects --- .../common/types/models/data_stream.ts | 5 ++ .../components/table_row_actions_nested.tsx | 38 +++++++++ .../ingest_manager/hooks/use_kibana_link.ts | 14 ++++ .../components/data_stream_row_actions.tsx | 82 +++++++++++++++++++ .../sections/data_stream/list_page/index.tsx | 31 ++++++- .../server/routes/data_streams/handlers.ts | 59 ++++++++++++- .../server/services/epm/packages/get.ts | 23 ++++-- 7 files changed, 242 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/table_row_actions_nested.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx diff --git a/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts index 7da9bbad1b170..abc9ffcf6be6a 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts @@ -10,6 +10,11 @@ export interface DataStream { namespace: string; type: string; package: string; + package_version: string; last_activity: string; size_in_bytes: number; + dashboards: Array<{ + id: string; + title: string; + }>; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/table_row_actions_nested.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/table_row_actions_nested.tsx new file mode 100644 index 0000000000000..56f010e2fa774 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/table_row_actions_nested.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useState } from 'react'; +import { EuiButtonIcon, EuiContextMenu, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiContextMenuProps } from '@elastic/eui/src/components/context_menu/context_menu'; + +export const TableRowActionsNested = React.memo<{ panels: EuiContextMenuProps['panels'] }>( + ({ panels }) => { + const [isOpen, setIsOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); + const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); + + return ( + + } + isOpen={isOpen} + closePopover={handleCloseMenu} + > + + + ); + } +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts new file mode 100644 index 0000000000000..f6c5b8bc03fce --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCore } from './'; + +const BASE_PATH = '/app/kibana'; + +export function useKibanaLink(path: string = '/') { + const core = useCore(); + return core.http.basePath.prepend(`${BASE_PATH}#${path}`); +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx new file mode 100644 index 0000000000000..ac47387cd7ab3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useKibanaLink } from '../../../../hooks/use_kibana_link'; +import { DataStream } from '../../../../types'; +import { TableRowActionsNested } from '../../../../components/table_row_actions_nested'; + +export const DataStreamRowActions = memo<{ datastream: DataStream }>(({ datastream }) => { + const { dashboards } = datastream; + const panels = []; + const actionNameSingular = ( + + ); + const actionNamePlural = ( + + ); + + const panelTitle = i18n.translate('xpack.ingestManager.dataStreamList.viewDashboardsPanelTitle', { + defaultMessage: 'View dashboards', + }); + + if (!dashboards || dashboards.length === 0) { + panels.push({ + id: 0, + items: [ + { + icon: 'dashboardApp', + disabled: true, + name: actionNameSingular, + }, + ], + }); + } else if (dashboards.length === 1) { + panels.push({ + id: 0, + items: [ + { + icon: 'dashboardApp', + href: useKibanaLink(`/dashboard/${dashboards[0].id || ''}`), + name: actionNameSingular, + }, + ], + }); + } else { + panels.push({ + id: 0, + items: [ + { + icon: 'dashboardApp', + panel: 1, + name: actionNamePlural, + }, + ], + }); + panels.push({ + id: 1, + title: panelTitle, + items: dashboards.map(dashboard => { + return { + icon: 'dashboardApp', + href: useKibanaLink(`/dashboard/${dashboard.id || ''}`), + name: dashboard.title, + }; + }), + }); + } + + return ; +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx index d7a3e933f3bb5..cff138c6a16ca 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -20,6 +20,8 @@ import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { DataStream } from '../../../types'; import { WithHeaderLayout } from '../../../layouts'; import { useGetDataStreams, useStartDeps, usePagination } from '../../../hooks'; +import { PackageIcon } from '../../../components/package_icon'; +import { DataStreamRowActions } from './components/data_stream_row_actions'; const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => ( = () => { const { pagination, pageSizeOptions } = usePagination(); - // Fetch agent configs + // Fetch data streams const { isLoading, data: dataStreamsData, sendRequest } = useGetDataStreams(); // Some configs retrieved, set up table props @@ -102,6 +104,23 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', { defaultMessage: 'Integration', }), + render(pkg: DataStream['package'], datastream: DataStream) { + return ( + + {datastream.package_version && ( + + + + )} + {pkg} + + ); + }, }, { field: 'last_activity', @@ -135,6 +154,16 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { } }, }, + { + name: i18n.translate('xpack.ingestManager.dataStreamList.actionsColumnTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + render: (datastream: DataStream) => , + }, + ], + }, ]; return cols; }, [fieldFormats]); diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts index 0d2909edf00c4..ad81076e34e4b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -3,9 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'src/core/server'; +import { RequestHandler, SavedObjectsClientContract } from 'src/core/server'; import { DataStream } from '../../types'; -import { GetDataStreamsResponse } from '../../../common'; +import { GetDataStreamsResponse, KibanaAssetType } from '../../../common'; +import { getPackageSavedObjects, getKibanaSavedObject } from '../../services/epm/packages/get'; const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*'; @@ -100,7 +101,10 @@ export const getListHandler: RequestHandler = async (context, request, response) index: { buckets: indexResults }, } = aggregations; - const dataStreams: DataStream[] = (indexResults as any[]).map(result => { + const packageSavedObjects = await getPackageSavedObjects(context.core.savedObjects.client); + const packageMetadata: any = {}; + + const dataStreamsPromises = (indexResults as any[]).map(async result => { const { key: indexName, dataset: { buckets: datasetBuckets }, @@ -109,17 +113,46 @@ export const getListHandler: RequestHandler = async (context, request, response) package: { buckets: packageBuckets }, last_activity: { value_as_string: lastActivity }, } = result; + + const pkg = packageBuckets.length ? packageBuckets[0].key : ''; + const pkgSavedObject = packageSavedObjects.saved_objects.filter(p => p.id === pkg); + + // if + // - the datastream is associated with a package + // - and the package has been installed through EPM + // - and we didn't pick the metadata in an earlier iteration of this map() + if (pkg !== '' && pkgSavedObject.length > 0 && !packageMetadata[pkg]) { + // then pick the dashboards from the package saved object + const dashboards = + pkgSavedObject[0].attributes?.installed?.filter( + o => o.type === KibanaAssetType.dashboard + ) || []; + // and then pick the human-readable titles from the dashboard saved objects + const enhancedDashboards = await getEnhancedDashboards( + context.core.savedObjects.client, + dashboards + ); + + packageMetadata[pkg] = { + version: pkgSavedObject[0].attributes?.version || '', + dashboards: enhancedDashboards, + }; + } return { index: indexName, dataset: datasetBuckets.length ? datasetBuckets[0].key : '', namespace: namespaceBuckets.length ? namespaceBuckets[0].key : '', type: typeBuckets.length ? typeBuckets[0].key : '', - package: packageBuckets.length ? packageBuckets[0].key : '', + package: pkg, + package_version: packageMetadata[pkg] ? packageMetadata[pkg].version : '', last_activity: lastActivity, size_in_bytes: indexStats[indexName] ? indexStats[indexName].total.store.size_in_bytes : 0, + dashboards: packageMetadata[pkg] ? packageMetadata[pkg].dashboards : [], }; }); + const dataStreams: DataStream[] = await Promise.all(dataStreamsPromises); + body.data_streams = dataStreams; return response.ok({ @@ -132,3 +165,21 @@ export const getListHandler: RequestHandler = async (context, request, response) }); } }; + +const getEnhancedDashboards = async ( + savedObjectsClient: SavedObjectsClientContract, + dashboards: any[] +) => { + const dashboardsPromises = dashboards.map(async db => { + const dbSavedObject: any = await getKibanaSavedObject( + savedObjectsClient, + KibanaAssetType.dashboard, + db.id + ); + return { + id: db.id, + title: dbSavedObject.attributes?.title || db.id, + }; + }); + return await Promise.all(dashboardsPromises); +}; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index da8d79a04b97c..6db08e344b3da 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -6,7 +6,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; -import { Installation, InstallationStatus, PackageInfo } from '../../../types'; +import { Installation, InstallationStatus, PackageInfo, KibanaAssetType } from '../../../types'; import * as Registry from '../registry'; import { createInstallableFrom } from './index'; @@ -32,11 +32,10 @@ export async function getPackages( ); }); // get the installed packages - const results = await savedObjectsClient.find({ - type: PACKAGES_SAVED_OBJECT_TYPE, - }); + const packageSavedObjects = await getPackageSavedObjects(savedObjectsClient); + // filter out any internal packages - const savedObjectsVisible = results.saved_objects.filter(o => !o.attributes.internal); + const savedObjectsVisible = packageSavedObjects.saved_objects.filter(o => !o.attributes.internal); const packageList = registryItems .map(item => createInstallableFrom( @@ -48,6 +47,12 @@ export async function getPackages( return packageList; } +export async function getPackageSavedObjects(savedObjectsClient: SavedObjectsClientContract) { + return savedObjectsClient.find({ + type: PACKAGES_SAVED_OBJECT_TYPE, + }); +} + export async function getPackageKeysByStatus( savedObjectsClient: SavedObjectsClientContract, status: InstallationStatus @@ -114,3 +119,11 @@ function sortByName(a: { name: string }, b: { name: string }) { return 0; } } + +export async function getKibanaSavedObject( + savedObjectsClient: SavedObjectsClientContract, + type: KibanaAssetType, + id: string +) { + return savedObjectsClient.get(type, id); +} From 8adc272f0c31a41cd6c26751c163ac9931fb4bd7 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Tue, 5 May 2020 05:31:19 -0700 Subject: [PATCH 15/75] [Metrics UI] Remove APM Hard Dependency (#64952) * [Metrics UI] Remove APM Hard Dependency * removing unused variable Co-authored-by: Elastic Machine --- x-pack/plugins/infra/kibana.json | 1 - .../infra/server/routes/metadata/index.ts | 6 +-- .../routes/metadata/lib/has_apm_data.ts | 54 ------------------- .../api_integration/apis/infra/metadata.ts | 41 -------------- 4 files changed, 1 insertion(+), 101 deletions(-) delete mode 100644 x-pack/plugins/infra/server/routes/metadata/lib/has_apm_data.ts diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index a15465a0cde66..ea66ae7a46d4e 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -4,7 +4,6 @@ "kibanaVersion": "kibana", "requiredPlugins": [ "features", - "apm", "usageCollection", "spaces", "home", diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index fe142aa93dcda..7e3a30e1e6918 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -18,7 +18,6 @@ import { import { InfraBackendLibs } from '../../lib/infra_types'; import { getMetricMetadata } from './lib/get_metric_metadata'; import { pickFeatureName } from './lib/pick_feature_name'; -import { hasAPMData } from './lib/has_apm_data'; import { getCloudMetricsMetadata } from './lib/get_cloud_metric_metadata'; import { getNodeInfo } from './lib/get_node_info'; import { throwErrors } from '../../../common/runtime_types'; @@ -74,16 +73,13 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map( nameToFeature('metrics') ); - const hasAPM = await hasAPMData(framework, requestContext, configuration, nodeId, nodeType); - const apmMetricFeatures = hasAPM ? [{ name: 'apm.transaction', source: 'apm' }] : []; - const id = metricsMetadata.id; const name = metricsMetadata.name || id; return response.ok({ body: InfraMetadataRT.encode({ id, name, - features: [...metricFeatures, ...cloudMetricsFeatures, ...apmMetricFeatures], + features: [...metricFeatures, ...cloudMetricsFeatures], info, }), }); diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/has_apm_data.ts b/x-pack/plugins/infra/server/routes/metadata/lib/has_apm_data.ts deleted file mode 100644 index 1f8029db80d86..0000000000000 --- a/x-pack/plugins/infra/server/routes/metadata/lib/has_apm_data.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { RequestHandlerContext } from 'src/core/server'; - -import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; -import { InfraSourceConfiguration } from '../../../lib/sources'; -import { findInventoryFields } from '../../../../common/inventory_models'; -import { InventoryItemType } from '../../../../common/inventory_models/types'; - -export const hasAPMData = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, - sourceConfiguration: InfraSourceConfiguration, - nodeId: string, - nodeType: InventoryItemType -) => { - const apmIndices = await framework.plugins.apm.getApmIndices(); - const apmIndex = apmIndices['apm_oss.transactionIndices'] || 'apm-*'; - const fields = findInventoryFields(nodeType, sourceConfiguration.fields); - - // There is a bug in APM ECS data where host.name is not set. - // This will fixed with: https://github.com/elastic/apm-server/issues/2502 - const nodeFieldName = nodeType === 'host' ? 'host.hostname' : fields.id; - const params = { - allowNoIndices: true, - ignoreUnavailable: true, - terminateAfter: 1, - index: apmIndex, - body: { - size: 0, - query: { - bool: { - filter: [ - { - match: { [nodeFieldName]: nodeId }, - }, - { - exists: { field: 'service.name' }, - }, - { - exists: { field: 'transaction.type' }, - }, - ], - }, - }, - }, - }; - const response = await framework.callWithRequest<{}, {}>(requestContext, 'search', params); - return response.hits.total.value !== 0; -}; diff --git a/x-pack/test/api_integration/apis/infra/metadata.ts b/x-pack/test/api_integration/apis/infra/metadata.ts index 5187cc5e3ec26..d3551a139bf17 100644 --- a/x-pack/test/api_integration/apis/infra/metadata.ts +++ b/x-pack/test/api_integration/apis/infra/metadata.ts @@ -29,11 +29,6 @@ const timeRange800withAws = { to: DATES[`8.0.0`].logs_and_metrics_with_aws.max, }; -const timeRange800 = { - from: DATES['8.0.0'].logs_and_metrics.min, - to: DATES[`8.0.0`].logs_and_metrics.max, -}; - export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); @@ -269,42 +264,6 @@ export default function({ getService }: FtrProviderContext) { } }); }); - describe('APM metrics', () => { - const archiveName = 'infra/8.0.0/metrics_and_apm'; - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - it('host without APM data', async () => { - const metadata = await fetchMetadata({ - sourceId: 'default', - nodeId: 'gke-observability-8--observability-8--bc1afd95-f0zc', - nodeType: 'host', - timeRange: timeRange800, - }); - if (metadata) { - expect( - metadata.features.some(f => f.name === 'apm.transaction' && f.source === 'apm') - ).to.be(false); - } else { - throw new Error('Metadata should never be empty'); - } - }); - it('pod with APM data', async () => { - const metadata = await fetchMetadata({ - sourceId: 'default', - nodeId: 'c1031331-9ae0-11e9-9a96-42010a84004d', - nodeType: 'pod', - timeRange: timeRange800, - }); - if (metadata) { - expect( - metadata.features.some(f => f.name === 'apm.transaction' && f.source === 'apm') - ).to.be(true); - } else { - throw new Error('Metadata should never be empty'); - } - }); - }); }); }); } From 62574f30b33464caf1d5ccdfb31fc5b4ebb16515 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 5 May 2020 14:33:52 +0200 Subject: [PATCH 16/75] [Uptime] Set ML anomaly look-back to 2w (from 24h) / Add spinner (#65055) --- .../__snapshots__/ml_integerations.test.tsx.snap | 6 ++---- .../__snapshots__/ml_manage_job.test.tsx.snap | 6 ++---- .../public/components/monitor/ml/manage_ml_job.tsx | 11 ++++++++++- .../components/monitor/ml/ml_flyout_container.tsx | 7 +++++-- .../public/components/monitor/ml/ml_integeration.tsx | 3 +++ x-pack/plugins/uptime/public/state/api/ml_anomaly.ts | 2 +- 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap index fa9b59e13c34e..9ad61f50b0521 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap @@ -15,10 +15,8 @@ exports[`ML Integrations renders without errors 1`] = ` -