From 7e3c68bb7a4ea660db2eb30b7176994ae1192ed8 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 8 Apr 2020 00:44:38 +0100 Subject: [PATCH 01/81] chore(NA): removes server imports from canvas src plugin (#62783) * chore(NA): remove server imports from canvas src plugin * chore(NA): correctly import types for demodata --- .../functions/server/demodata/demo_rows_types.ts | 10 ++++++++++ .../functions/server/demodata/get_demo_rows.ts | 6 +----- .../functions/server/demodata/index.ts | 3 ++- .../plugins/canvas/i18n/functions/dict/demodata.ts | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/demo_rows_types.ts diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/demo_rows_types.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/demo_rows_types.ts new file mode 100644 index 0000000000000..e92dc79fba8c3 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/demo_rows_types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum DemoRows { + CI = 'ci', + SHIRTS = 'shirts', +} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts index 02f8efcfde95d..58a2354b5cf38 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts @@ -6,14 +6,10 @@ import { cloneDeep } from 'lodash'; import ci from './ci.json'; +import { DemoRows } from './demo_rows_types'; import shirts from './shirts.json'; import { getFunctionErrors } from '../../../../i18n'; -export enum DemoRows { - CI = 'ci', - SHIRTS = 'shirts', -} - export function getDemoRows(arg: string | null) { if (arg === DemoRows.CI) { return cloneDeep(ci); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts index 826c49d328f21..5cebae5bb669f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts @@ -8,7 +8,8 @@ import { sortBy } from 'lodash'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; // @ts-ignore unconverted lib file import { queryDatatable } from '../../../../common/lib/datatable/query'; -import { DemoRows, getDemoRows } from './get_demo_rows'; +import { DemoRows } from './demo_rows_types'; +import { getDemoRows } from './get_demo_rows'; import { Filter, Datatable, DatatableColumn, DatatableRow } from '../../../../types'; import { getFunctionHelp } from '../../../../i18n'; diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/demodata.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/demodata.ts index caedbfdec5be4..35a5b86f752dc 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/demodata.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/demodata.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { demodata } from '../../../canvas_plugin_src/functions/server/demodata'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; -import { DemoRows } from '../../../canvas_plugin_src/functions/server/demodata/get_demo_rows'; +import { DemoRows } from '../../../canvas_plugin_src/functions/server/demodata/demo_rows_types'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.demodataHelpText', { From 5218e3048706236d0a0baddb0d32cf9f0d7536a8 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 7 Apr 2020 19:37:47 -0600 Subject: [PATCH 02/81] [SIEM][Detection Engine] Fixes TypeScript types and adds format to time range query ## Summary * Fixes the Type Script types so we don't have to use non-null-assertions * Adds null checks where needed * Changes the time range query to have a format of epoch to avoid mapping issues ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../detection_engine/rules/types.ts | 2 +- .../rules/create/helpers.test.ts | 10 +++++----- .../detection_engine/rules/create/helpers.ts | 2 +- .../pages/detection_engine/rules/helpers.tsx | 2 +- .../notifications/build_signals_query.test.ts | 1 + .../notifications/build_signals_query.ts | 1 + .../rules_notification_alert_type.ts | 6 +++--- .../schedule_notification_actions.ts | 2 +- .../detection_engine/notifications/utils.ts | 2 +- .../routes/rules/patch_rules_bulk_route.ts | 6 +++--- .../routes/rules/patch_rules_route.ts | 6 +++--- .../lib/detection_engine/routes/utils.ts | 2 +- .../create_rule_actions_saved_object.ts | 9 +++++++-- .../delete_rule_actions_saved_object.ts | 2 +- .../get_rule_actions_saved_object.ts | 8 +++++++- ...ate_or_create_rule_actions_saved_object.ts | 2 +- .../update_rule_actions_saved_object.ts | 9 +++++++-- .../detection_engine/rule_actions/utils.ts | 19 +++++++++++++++---- .../rules/update_rule_actions.ts | 8 ++++++-- .../rules/update_rules_notifications.ts | 5 +++-- .../signals/signal_rule_alert_type.test.ts | 2 +- .../signals/signal_rule_alert_type.ts | 9 ++++++--- .../lib/detection_engine/signals/types.ts | 2 +- .../siem/server/lib/detection_engine/types.ts | 9 +++++++-- 24 files changed, 84 insertions(+), 42 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index bc559c5ac4972..f89d21ef1aeb1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -70,7 +70,7 @@ const MetaRule = t.intersection([ }), t.partial({ throttle: t.string, - kibanaSiemAppUrl: t.string, + kibana_siem_app_url: t.string, }), ]); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts index efb601b6bd207..8d793f39afa99 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts @@ -515,7 +515,7 @@ describe('helpers', () => { actions: [], enabled: false, meta: { - kibanaSiemAppUrl: 'http://localhost:5601/app/siem', + kibana_siem_app_url: 'http://localhost:5601/app/siem', }, throttle: 'no_actions', }; @@ -533,7 +533,7 @@ describe('helpers', () => { actions: [], enabled: false, meta: { - kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl, + kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, }, throttle: 'no_actions', }; @@ -566,7 +566,7 @@ describe('helpers', () => { ], enabled: false, meta: { - kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl, + kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, }, throttle: 'rule', }; @@ -599,7 +599,7 @@ describe('helpers', () => { ], enabled: false, meta: { - kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl, + kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, }, throttle: mockStepData.throttle, }; @@ -631,7 +631,7 @@ describe('helpers', () => { ], enabled: false, meta: { - kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl, + kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, }, throttle: 'no_actions', }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts index 151e3a9bdf4d6..7ad116c313361 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -155,7 +155,7 @@ export const formatActionsStepData = (actionsStepData: ActionsStepRule): Actions enabled, throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, meta: { - kibanaSiemAppUrl, + kibana_siem_app_url: kibanaSiemAppUrl, }, }; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index db1f2298b5ea7..58a1b0fd3133e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -64,7 +64,7 @@ export const getActionsStepsData = ( actions: actions?.map(transformRuleToAlertAction), isNew: false, throttle, - kibanaSiemAppUrl: meta?.kibanaSiemAppUrl, + kibanaSiemAppUrl: meta?.kibana_siem_app_url, enabled, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.test.ts index 189c596a77125..c6923283bec16 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.test.ts @@ -41,6 +41,7 @@ describe('buildSignalsSearchQuery', () => { '@timestamp': { gt: from, lte: to, + format: 'epoch_millis', }, }, }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.ts index b973d4c5f4e98..be0943c014225 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.ts @@ -32,6 +32,7 @@ export const buildSignalsSearchQuery = ({ ruleId, index, from, to }: BuildSignal '@timestamp': { gt: from, lte: to, + format: 'epoch_millis', }, }, }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index 546488caa5ee7..ced81098c9f8e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -53,7 +53,7 @@ export const rulesNotificationAlertType = ({ from: fromInMs, to: toInMs, index: ruleParams.outputIndex, - ruleId: ruleParams.ruleId!, + ruleId: ruleParams.ruleId, callCluster: services.callCluster, }); @@ -61,14 +61,14 @@ export const rulesNotificationAlertType = ({ from: fromInMs, to: toInMs, id: ruleAlertSavedObject.id, - kibanaSiemAppUrl: ruleAlertParams.meta?.kibanaSiemAppUrl as string, + kibanaSiemAppUrl: ruleAlertParams.meta?.kibana_siem_app_url, }); logger.info( `Found ${signalsCount} signals using signal rule name: "${ruleParams.name}", id: "${params.ruleAlertId}", rule_id: "${ruleParams.ruleId}" in "${ruleParams.outputIndex}" index` ); - if (signalsCount) { + if (signalsCount !== 0) { const alertInstance = services.alertInstanceFactory(alertId); scheduleNotificationActions({ alertInstance, signalsCount, resultsLink, ruleParams }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts index 749b892ef506f..9f145af79ca90 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts @@ -8,7 +8,7 @@ import { mapKeys, snakeCase } from 'lodash/fp'; import { AlertInstance } from '../../../../../../../plugins/alerting/server'; import { RuleTypeParams } from '../types'; -type NotificationRuleTypeParams = RuleTypeParams & { +export type NotificationRuleTypeParams = RuleTypeParams & { name: string; id: string; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts index 5dc7e7fc30b7f..c91c4490e8eba 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts @@ -10,7 +10,7 @@ export const getNotificationResultsLink = ({ from, to, }: { - kibanaSiemAppUrl: string; + kibanaSiemAppUrl?: string; id: string; from?: string; to?: string; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 85255594ee480..8c0fceb7a5f29 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -121,15 +121,15 @@ export const patchRulesBulkRoute = (router: IRouter) => { anomalyThreshold, machineLearningJobId, }); - if (rule != null) { + if (rule != null && rule.enabled != null && rule.name != null) { const ruleActions = await updateRulesNotifications({ ruleAlertId: rule.id, alertsClient, savedObjectsClient, - enabled: rule.enabled!, + enabled: rule.enabled, actions, throttle, - name: rule.name!, + name: rule.name, }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index f553ccd2c6f81..9c5000d70e5fe 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -117,15 +117,15 @@ export const patchRulesRoute = (router: IRouter) => { anomalyThreshold, machineLearningJobId, }); - if (rule != null) { + if (rule != null && rule.enabled != null && rule.name != null) { const ruleActions = await updateRulesNotifications({ ruleAlertId: rule.id, alertsClient, savedObjectsClient, - enabled: rule.enabled!, + enabled: rule.enabled, actions, throttle, - name: rule.name!, + name: rule.name, }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 8d7360bae8eb9..e4015ad8bafa4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -309,7 +309,7 @@ export const validateLicenseForRuleType = ({ }: { license: ILicense; ruleType: RuleType; -}) => { +}): void => { if (isMlRule(ruleType) && !license.hasAtLeast(MINIMUM_ML_LICENSE)) { const message = i18n.translate('xpack.siem.licensing.unsupportedMachineLearningMessage', { defaultMessage: diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts index 23c99b36cb4a7..97cfc1d2d9ea7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts @@ -14,7 +14,7 @@ interface CreateRuleActionsSavedObject { ruleAlertId: string; savedObjectsClient: AlertServices['savedObjectsClient']; actions: RuleAlertAction[] | undefined; - throttle: string | undefined; + throttle: string | null | undefined; } export const createRuleActionsSavedObject = async ({ @@ -22,7 +22,12 @@ export const createRuleActionsSavedObject = async ({ savedObjectsClient, actions = [], throttle, -}: CreateRuleActionsSavedObject) => { +}: CreateRuleActionsSavedObject): Promise<{ + id: string; + actions: RuleAlertAction[]; + alertThrottle: string | null; + ruleThrottle: string; +}> => { const ruleActionsSavedObject = await savedObjectsClient.create< IRuleActionsAttributesSavedObjectAttributes >(ruleActionsSavedObjectType, { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts index 4e8781dd45692..864281da5bafd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts @@ -16,7 +16,7 @@ interface DeleteRuleActionsSavedObject { export const deleteRuleActionsSavedObject = async ({ ruleAlertId, savedObjectsClient, -}: DeleteRuleActionsSavedObject) => { +}: DeleteRuleActionsSavedObject): Promise<{} | null> => { const ruleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); if (!ruleActions) return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts index 3ae9090526d69..61b544db5a18a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; @@ -17,7 +18,12 @@ interface GetRuleActionsSavedObject { export const getRuleActionsSavedObject = async ({ ruleAlertId, savedObjectsClient, -}: GetRuleActionsSavedObject) => { +}: GetRuleActionsSavedObject): Promise<{ + id: string; + actions: RuleAlertAction[]; + alertThrottle: string | null; + ruleThrottle: string; +} | null> => { const { saved_objects } = await savedObjectsClient.find< IRuleActionsAttributesSavedObjectAttributes >({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts index 3856f75255262..adc87150f89a7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts @@ -15,7 +15,7 @@ interface UpdateOrCreateRuleActionsSavedObject { ruleAlertId: string; savedObjectsClient: AlertServices['savedObjectsClient']; actions: RuleAlertAction[] | undefined; - throttle: string | undefined; + throttle: string | null | undefined; } export const updateOrCreateRuleActionsSavedObject = async ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts index 56bce3c8b67a3..a15005110c56b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts @@ -15,7 +15,7 @@ interface DeleteRuleActionsSavedObject { ruleAlertId: string; savedObjectsClient: AlertServices['savedObjectsClient']; actions: RuleAlertAction[] | undefined; - throttle: string | undefined; + throttle: string | null | undefined; } export const updateRuleActionsSavedObject = async ({ @@ -23,7 +23,12 @@ export const updateRuleActionsSavedObject = async ({ savedObjectsClient, actions, throttle, -}: DeleteRuleActionsSavedObject) => { +}: DeleteRuleActionsSavedObject): Promise<{ + ruleThrottle: string; + alertThrottle: string | null; + actions: RuleAlertAction[]; + id: string; +} | null> => { const ruleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); if (!ruleActions) return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts index 3c297ed848555..554c2b4a3dd90 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts @@ -5,16 +5,27 @@ */ import { SavedObjectsUpdateResponse } from 'kibana/server'; +import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; -export const getThrottleOptions = (throttle = 'no_actions') => ({ - ruleThrottle: throttle, - alertThrottle: ['no_actions', 'rule'].includes(throttle) ? null : throttle, +export const getThrottleOptions = ( + throttle: string | undefined | null = 'no_actions' +): { + ruleThrottle: string; + alertThrottle: string | null; +} => ({ + ruleThrottle: throttle ?? 'no_actions', + alertThrottle: ['no_actions', 'rule'].includes(throttle ?? 'no_actions') ? null : throttle, }); export const getRuleActionsFromSavedObject = ( savedObject: SavedObjectsUpdateResponse -) => ({ +): { + id: string; + actions: RuleAlertAction[]; + alertThrottle: string | null; + ruleThrottle: string; +} => ({ id: savedObject.id, actions: savedObject.attributes.actions || [], alertThrottle: savedObject.attributes.alertThrottle || null, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts index ac10143c1d8d0..e6ee1e6a29764 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertsClient, AlertServices } from '../../../../../../../plugins/alerting/server'; +import { + AlertsClient, + AlertServices, + PartialAlert, +} from '../../../../../../../plugins/alerting/server'; import { getRuleActionsSavedObject } from '../rule_actions/get_rule_actions_saved_object'; import { readRules } from './read_rules'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; @@ -19,7 +23,7 @@ export const updateRuleActions = async ({ alertsClient, savedObjectsClient, ruleAlertId, -}: UpdateRuleActions) => { +}: UpdateRuleActions): Promise => { const rule = await readRules({ alertsClient, id: ruleAlertId }); if (rule == null) { return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts index f70c591243a76..bb66a5ee1342f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts @@ -9,13 +9,14 @@ import { AlertsClient, AlertServices } from '../../../../../../../plugins/alerti import { updateOrCreateRuleActionsSavedObject } from '../rule_actions/update_or_create_rule_actions_saved_object'; import { updateNotifications } from '../notifications/update_notifications'; import { updateRuleActions } from './update_rule_actions'; +import { RuleActions } from '../rule_actions/types'; interface UpdateRulesNotifications { alertsClient: AlertsClient; savedObjectsClient: AlertServices['savedObjectsClient']; ruleAlertId: string; actions: RuleAlertAction[] | undefined; - throttle: string | undefined; + throttle: string | null | undefined; enabled: boolean; name: string; } @@ -28,7 +29,7 @@ export const updateRulesNotifications = async ({ enabled, name, throttle, -}: UpdateRulesNotifications) => { +}: UpdateRulesNotifications): Promise => { const ruleActions = await updateOrCreateRuleActionsSavedObject({ savedObjectsClient, ruleAlertId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 11d31f1805440..3d6f443ce60d6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -47,7 +47,7 @@ const getPayload = ( interval: ruleAlert.schedule.interval, name: ruleAlert.name, tags: ruleAlert.tags, - throttle: ruleAlert.throttle!, + throttle: ruleAlert.throttle, scrollSize: 10, scrollLock: '0', }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 417fcbbe42a56..faac4a547fc17 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -24,7 +24,10 @@ import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; import { findMlSignals } from './find_ml_signals'; import { bulkCreateMlSignals } from './bulk_create_ml_signals'; -import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; +import { + scheduleNotificationActions, + NotificationRuleTypeParams, +} from '../notifications/schedule_notification_actions'; import { ruleStatusServiceFactory } from './rule_status_service'; import { buildRuleMessageFactory } from './rule_messages'; import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects_client'; @@ -246,7 +249,7 @@ export const signalRulesAlertType = ({ if (result.success) { if (actions.length) { - const notificationRuleParams = { + const notificationRuleParams: NotificationRuleTypeParams = { ...ruleParams, name, id: savedObject.id, @@ -259,7 +262,7 @@ export const signalRulesAlertType = ({ from: fromInMs, to: toInMs, id: savedObject.id, - kibanaSiemAppUrl: meta?.kibanaSiemAppUrl as string, + kibanaSiemAppUrl: meta?.kibana_siem_app_url, }); logger.info( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index 543e8bf0619b0..d4469351de544 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -162,5 +162,5 @@ export interface AlertAttributes { } export interface RuleAlertAttributes extends AlertAttributes { - params: RuleAlertParams; + params: Omit & { ruleId: string }; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index efa0a92cc573b..d3fa98fd73d3a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -29,6 +29,11 @@ export interface ThreatParams { // We don't have the input types defined through io-ts just yet but as we being introducing types from there we will more and more remove // types and share them between input and output schema but have an input Rule Schema and an output Rule Schema. +export interface Meta { + [key: string]: {} | string | undefined | null; + kibana_siem_app_url?: string | undefined; +} + export interface RuleAlertParams { actions: RuleAlertAction[]; anomalyThreshold: number | undefined; @@ -51,7 +56,7 @@ export interface RuleAlertParams { query: string | undefined | null; references: string[]; savedId?: string | undefined | null; - meta: Record | undefined | null; + meta: Meta | undefined | null; severity: string; tags: string[]; to: string; @@ -60,7 +65,7 @@ export interface RuleAlertParams { threat: ThreatParams[] | undefined | null; type: RuleType; version: number; - throttle: string; + throttle: string | undefined | null; lists: ListsDefaultArraySchema | null | undefined; } From d212102bf56e18813efd6fc993b6f175ed0407ef Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Wed, 8 Apr 2020 07:44:23 +0200 Subject: [PATCH 03/81] fixing region map click filter (#61949) --- .../core_plugins/region_map/public/region_map_visualization.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/legacy/core_plugins/region_map/public/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/region_map_visualization.js index 25641ea76809d..72f9d66e7d2bf 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/region_map_visualization.js @@ -164,7 +164,8 @@ export function createRegionMapVisualization({ serviceSettings, $injector, uiSet } this._choroplethLayer.on('select', event => { - const rowIndex = this._chartData.rows.findIndex(row => row[0] === event); + const { rows, columns } = this._chartData; + const rowIndex = rows.findIndex(row => row[columns[0].id] === event); this._vis.API.events.filter({ table: this._chartData, column: 0, From 028313a8fe9a03ec79182a9bd510245c872c4d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 8 Apr 2020 09:08:59 +0100 Subject: [PATCH 04/81] [Telemetry] Add possibility of registering exclusive collectors for each collection (#62665) * [Telemetry] Add posibility of regitering exclusive collectors for collections * [Telemetry] Filter unwanted fields from the kibana.os telemetry payload * Filter the collectors properly in bulkFetch * Move "kibana" usage collector from Monitoring to OSS Telemetry * Remove exclusivity of the "kibana_settings" collector * Unify "kibana_stats" collector from Monitoring and Legacy * Remove unused legacy constants * Proper type for UsageCollectionSetup in monitoring * Missed one undo * Add unit tests to the migrated collectors Co-authored-by: Elastic Machine --- .../collectors/get_ops_stats_collector.js | 56 -------- src/legacy/server/status/index.js | 2 - src/plugins/telemetry/common/constants.ts | 11 ++ .../telemetry/server/collectors/index.ts | 2 + .../kibana/get_saved_object_counts.test.ts | 61 ++++++++ .../kibana/get_saved_object_counts.ts | 82 +++++++++++ .../server/collectors/kibana/index.test.ts | 76 ++++++++++ .../server/collectors/kibana/index.ts} | 2 +- .../kibana/kibana_usage_collector.ts | 65 +++++++++ .../__snapshots__/index.test.ts.snap | 43 ++++++ .../server/collectors/ops_stats/index.test.ts | 132 ++++++++++++++++++ .../server/collectors/ops_stats/index.ts} | 2 +- .../ops_stats/ops_stats_collector.ts | 71 ++++++++++ src/plugins/telemetry/server/plugin.ts | 19 ++- .../server/telemetry_collection/get_kibana.ts | 4 + x-pack/plugins/monitoring/common/constants.ts | 6 - .../collectors/get_kibana_usage_collector.ts | 86 ------------ .../collectors/get_ops_stats_collector.ts | 46 ------ .../kibana_monitoring/collectors/index.ts | 14 +- x-pack/plugins/monitoring/server/plugin.ts | 7 +- 20 files changed, 568 insertions(+), 219 deletions(-) delete mode 100644 src/legacy/server/status/collectors/get_ops_stats_collector.js create mode 100644 src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.test.ts create mode 100644 src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.ts create mode 100644 src/plugins/telemetry/server/collectors/kibana/index.test.ts rename src/{legacy/server/status/constants.js => plugins/telemetry/server/collectors/kibana/index.ts} (90%) create mode 100644 src/plugins/telemetry/server/collectors/kibana/kibana_usage_collector.ts create mode 100644 src/plugins/telemetry/server/collectors/ops_stats/__snapshots__/index.test.ts.snap create mode 100644 src/plugins/telemetry/server/collectors/ops_stats/index.test.ts rename src/{legacy/server/status/collectors/index.js => plugins/telemetry/server/collectors/ops_stats/index.ts} (91%) create mode 100644 src/plugins/telemetry/server/collectors/ops_stats/ops_stats_collector.ts delete mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.ts delete mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.ts diff --git a/src/legacy/server/status/collectors/get_ops_stats_collector.js b/src/legacy/server/status/collectors/get_ops_stats_collector.js deleted file mode 100644 index b733e2e721e3a..0000000000000 --- a/src/legacy/server/status/collectors/get_ops_stats_collector.js +++ /dev/null @@ -1,56 +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 { KIBANA_STATS_TYPE } from '../constants'; -import { getKibanaInfoForStats } from '../lib'; - -/* - * Initialize a collector for Kibana Ops Stats - * - * NOTE this collector's fetch method returns the latest stats from the - * Hapi/Good/Even-Better ops event listener. Therefore, the stats reset - * every 5 seconds (the default value of the ops.interval configuration - * setting). That makes it geared for providing the latest "real-time" - * stats. In the long-term, fetch should return stats that constantly - * accumulate over the server's uptime for better machine readability. - * Since the data is captured, timestamped and stored, the historical - * data can provide "real-time" stats by calculating a derivative of - * the metrics. - * See PR comment in https://github.com/elastic/kibana/pull/20577/files#r202416647 - */ -export function getOpsStatsCollector(usageCollection, server, kbnServer) { - return usageCollection.makeStatsCollector({ - type: KIBANA_STATS_TYPE, - fetch: () => { - return { - kibana: getKibanaInfoForStats(server, kbnServer), - ...kbnServer.metrics, // latest metrics captured from the ops event listener in src/legacy/server/status/index - }; - }, - isReady: () => true, - ignoreForInternalUploader: true, // Ignore this one from internal uploader. A different stats collector is used there. - }); -} - -export function registerOpsStatsCollector(usageCollection, server, kbnServer) { - if (usageCollection) { - const collector = getOpsStatsCollector(usageCollection, server, kbnServer); - usageCollection.registerCollector(collector); - } -} diff --git a/src/legacy/server/status/index.js b/src/legacy/server/status/index.js index df02b3c45ec2f..5bd1efa99eb2c 100644 --- a/src/legacy/server/status/index.js +++ b/src/legacy/server/status/index.js @@ -20,7 +20,6 @@ import ServerStatus from './server_status'; import { Metrics } from './lib/metrics'; import { registerStatusPage, registerStatusApi, registerStatsApi } from './routes'; -import { registerOpsStatsCollector } from './collectors'; import Oppsy from 'oppsy'; import { cloneDeep } from 'lodash'; import { getOSInfo } from './lib/get_os_info'; @@ -28,7 +27,6 @@ import { getOSInfo } from './lib/get_os_info'; export function statusMixin(kbnServer, server, config) { kbnServer.status = new ServerStatus(kbnServer.server); const { usageCollection } = server.newPlatform.setup.plugins; - registerOpsStatsCollector(usageCollection, server, kbnServer); const metrics = new Metrics(config, server); diff --git a/src/plugins/telemetry/common/constants.ts b/src/plugins/telemetry/common/constants.ts index babd009143c5e..fd32862896528 100644 --- a/src/plugins/telemetry/common/constants.ts +++ b/src/plugins/telemetry/common/constants.ts @@ -80,3 +80,14 @@ export const APPLICATION_USAGE_TYPE = 'application_usage'; * The type name used within the Monitoring index to publish management stats. */ export const KIBANA_STACK_MANAGEMENT_STATS_TYPE = 'stack_management'; + +/** + * The type name used to publish Kibana usage stats. + * NOTE: this string shows as-is in the stats API as a field name for the kibana usage stats + */ +export const KIBANA_USAGE_TYPE = 'kibana'; + +/** + * The type name used to publish Kibana usage stats in the formatted as bulk. + */ +export const KIBANA_STATS_TYPE = 'kibana_stats'; diff --git a/src/plugins/telemetry/server/collectors/index.ts b/src/plugins/telemetry/server/collectors/index.ts index 6eeda080bb3ab..a874f8dd6202c 100644 --- a/src/plugins/telemetry/server/collectors/index.ts +++ b/src/plugins/telemetry/server/collectors/index.ts @@ -22,3 +22,5 @@ export { registerUiMetricUsageCollector } from './ui_metric'; export { registerTelemetryPluginUsageCollector } from './telemetry_plugin'; export { registerManagementUsageCollector } from './management'; export { registerApplicationUsageCollector } from './application_usage'; +export { registerKibanaUsageCollector } from './kibana'; +export { registerOpsStatsCollector } from './ops_stats'; diff --git a/src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.test.ts b/src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.test.ts new file mode 100644 index 0000000000000..a7681e1766427 --- /dev/null +++ b/src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { getSavedObjectsCounts } from './get_saved_object_counts'; + +describe('getSavedObjectsCounts', () => { + test('Get all the saved objects equal to 0 because no results were found', async () => { + const callCluster = jest.fn(() => ({})); + + const results = await getSavedObjectsCounts(callCluster as any, '.kibana'); + expect(results).toStrictEqual({ + dashboard: { total: 0 }, + visualization: { total: 0 }, + search: { total: 0 }, + index_pattern: { total: 0 }, + graph_workspace: { total: 0 }, + timelion_sheet: { total: 0 }, + }); + }); + + test('Merge the zeros with the results', async () => { + const callCluster = jest.fn(() => ({ + aggregations: { + types: { + buckets: [ + { key: 'dashboard', doc_count: 1 }, + { key: 'timelion-sheet', doc_count: 2 }, + { key: 'index-pattern', value: 2 }, // Malformed on purpose + { key: 'graph_workspace', doc_count: 3 }, // already snake_cased + ], + }, + }, + })); + + const results = await getSavedObjectsCounts(callCluster as any, '.kibana'); + expect(results).toStrictEqual({ + dashboard: { total: 1 }, + visualization: { total: 0 }, + search: { total: 0 }, + index_pattern: { total: 0 }, + graph_workspace: { total: 3 }, + timelion_sheet: { total: 2 }, + }); + }); +}); diff --git a/src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.ts b/src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.ts new file mode 100644 index 0000000000000..804c8b0ed2026 --- /dev/null +++ b/src/plugins/telemetry/server/collectors/kibana/get_saved_object_counts.ts @@ -0,0 +1,82 @@ +/* + * 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. + */ + +/** + * Moved from /x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.ts + * + * The PR https://github.com/elastic/kibana/pull/62665 proved what the issue https://github.com/elastic/kibana/issues/58249 + * was claiming: the structure and payload for common telemetry bits differs between Monitoring and OSS/X-Pack collections. + * + * Unifying this logic from Monitoring that makes sense to have in OSS here and we will import it on the monitoring side to reuse it. + */ + +import { snakeCase } from 'lodash'; +import { APICaller } from 'kibana/server'; + +const TYPES = [ + 'dashboard', + 'visualization', + 'search', + 'index-pattern', + 'graph-workspace', + 'timelion-sheet', +]; + +export interface KibanaSavedObjectCounts { + [pluginName: string]: { + total: number; + }; +} + +export async function getSavedObjectsCounts( + callCluster: APICaller, + kibanaIndex: string // Typically '.kibana'. We might need a way to obtain it from the SavedObjects client (or the SavedObjects client to provide a way to run aggregations?) +): Promise { + const savedObjectCountSearchParams = { + index: kibanaIndex, + ignoreUnavailable: true, + filterPath: 'aggregations.types.buckets', + body: { + size: 0, + query: { + terms: { type: TYPES }, + }, + aggs: { + types: { + terms: { field: 'type', size: TYPES.length }, + }, + }, + }, + }; + const resp = await callCluster('search', savedObjectCountSearchParams); + const buckets: Array<{ key: string; doc_count: number }> = + resp.aggregations?.types?.buckets || []; + + // Initialise the object with all zeros for all the types + const allZeros: KibanaSavedObjectCounts = TYPES.reduce( + (acc, type) => ({ ...acc, [snakeCase(type)]: { total: 0 } }), + {} + ); + + // Add the doc_count from each bucket + return buckets.reduce( + (acc, { key, doc_count: total }) => (total ? { ...acc, [snakeCase(key)]: { total } } : acc), + allZeros + ); +} diff --git a/src/plugins/telemetry/server/collectors/kibana/index.test.ts b/src/plugins/telemetry/server/collectors/kibana/index.test.ts new file mode 100644 index 0000000000000..91ede686ded3d --- /dev/null +++ b/src/plugins/telemetry/server/collectors/kibana/index.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { UsageCollectionSetup } from '../../../../../plugins/usage_collection/server'; +import { pluginInitializerContextConfigMock } from '../../../../../core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { CollectorOptions } from '../../../../../plugins/usage_collection/server/collector/collector'; + +import { registerKibanaUsageCollector } from './'; + +describe('telemetry_kibana', () => { + let collector: CollectorOptions; + + const usageCollectionMock: jest.Mocked = { + makeUsageCollector: jest.fn().mockImplementation(config => (collector = config)), + registerCollector: jest.fn(), + } as any; + + const legacyConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; + const callCluster = jest.fn().mockImplementation(() => ({})); + + beforeAll(() => registerKibanaUsageCollector(usageCollectionMock, legacyConfig$)); + afterAll(() => jest.clearAllTimers()); + + test('registered collector is set', () => { + expect(collector).not.toBeUndefined(); + expect(collector.type).toBe('kibana'); + }); + + test('fetch', async () => { + expect(await collector.fetch(callCluster)).toStrictEqual({ + index: '.kibana-tests', + dashboard: { total: 0 }, + visualization: { total: 0 }, + search: { total: 0 }, + index_pattern: { total: 0 }, + graph_workspace: { total: 0 }, + timelion_sheet: { total: 0 }, + }); + }); + + test('formatForBulkUpload', async () => { + const resultFromFetch = { + index: '.kibana-tests', + dashboard: { total: 0 }, + visualization: { total: 0 }, + search: { total: 0 }, + index_pattern: { total: 0 }, + graph_workspace: { total: 0 }, + timelion_sheet: { total: 0 }, + }; + + expect(collector.formatForBulkUpload!(resultFromFetch)).toStrictEqual({ + type: 'kibana_stats', + payload: { + usage: resultFromFetch, + }, + }); + }); +}); diff --git a/src/legacy/server/status/constants.js b/src/plugins/telemetry/server/collectors/kibana/index.ts similarity index 90% rename from src/legacy/server/status/constants.js rename to src/plugins/telemetry/server/collectors/kibana/index.ts index 3bb23749bae87..695d972346b8a 100644 --- a/src/legacy/server/status/constants.js +++ b/src/plugins/telemetry/server/collectors/kibana/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export const KIBANA_STATS_TYPE = 'oss_kibana_stats'; // kibana stats per 5s intervals +export { registerKibanaUsageCollector } from './kibana_usage_collector'; diff --git a/src/plugins/telemetry/server/collectors/kibana/kibana_usage_collector.ts b/src/plugins/telemetry/server/collectors/kibana/kibana_usage_collector.ts new file mode 100644 index 0000000000000..ccf6f7b1033c9 --- /dev/null +++ b/src/plugins/telemetry/server/collectors/kibana/kibana_usage_collector.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { SharedGlobalConfig } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { KIBANA_STATS_TYPE, KIBANA_USAGE_TYPE } from '../../../common/constants'; +import { getSavedObjectsCounts } from './get_saved_object_counts'; + +export function getKibanaUsageCollector( + usageCollection: UsageCollectionSetup, + legacyConfig$: Observable +) { + return usageCollection.makeUsageCollector({ + type: KIBANA_USAGE_TYPE, + isReady: () => true, + async fetch(callCluster) { + const { + kibana: { index }, + } = await legacyConfig$.pipe(take(1)).toPromise(); + return { + index, + ...(await getSavedObjectsCounts(callCluster, index)), + }; + }, + + /* + * Format the response data into a model for internal upload + * 1. Make this data part of the "kibana_stats" type + * 2. Organize the payload in the usage namespace of the data payload (usage.index, etc) + */ + formatForBulkUpload: result => { + return { + type: KIBANA_STATS_TYPE, + payload: { + usage: result, + }, + }; + }, + }); +} + +export function registerKibanaUsageCollector( + usageCollection: UsageCollectionSetup, + legacyConfig$: Observable +) { + usageCollection.registerCollector(getKibanaUsageCollector(usageCollection, legacyConfig$)); +} diff --git a/src/plugins/telemetry/server/collectors/ops_stats/__snapshots__/index.test.ts.snap b/src/plugins/telemetry/server/collectors/ops_stats/__snapshots__/index.test.ts.snap new file mode 100644 index 0000000000000..678237ffb6ea2 --- /dev/null +++ b/src/plugins/telemetry/server/collectors/ops_stats/__snapshots__/index.test.ts.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`telemetry_ops_stats should return something when there is a metric 1`] = ` +Object { + "concurrent_connections": 20, + "os": Object { + "load": Object { + "15m": 3, + "1m": 0.5, + "5m": 1, + }, + "memory": Object { + "free_in_bytes": 10, + "total_in_bytes": 10, + "used_in_bytes": 10, + }, + "platform": "darwin", + "platformRelease": "test", + "uptime_in_millis": 1000, + }, + "process": Object { + "event_loop_delay": 10, + "memory": Object { + "heap": Object { + "size_limit": 0, + "total_in_bytes": 0, + "used_in_bytes": 0, + }, + "resident_set_size_in_bytes": 0, + }, + "uptime_in_millis": 1000, + }, + "requests": Object { + "disconnects": 10, + "total": 100, + }, + "response_times": Object { + "average": 100, + "max": 200, + }, + "timestamp": Any, +} +`; diff --git a/src/plugins/telemetry/server/collectors/ops_stats/index.test.ts b/src/plugins/telemetry/server/collectors/ops_stats/index.test.ts new file mode 100644 index 0000000000000..92e0e40776eb8 --- /dev/null +++ b/src/plugins/telemetry/server/collectors/ops_stats/index.test.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 { Subject } from 'rxjs'; +import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { CollectorOptions } from '../../../../../plugins/usage_collection/server/collector/collector'; + +import { registerOpsStatsCollector } from './'; +import { OpsMetrics } from '../../../../../core/server'; + +describe('telemetry_ops_stats', () => { + let collector: CollectorOptions; + + const usageCollectionMock: jest.Mocked = { + makeStatsCollector: jest.fn().mockImplementation(config => (collector = config)), + registerCollector: jest.fn(), + } as any; + + const metrics$ = new Subject(); + const callCluster = jest.fn(); + + const metric: OpsMetrics = { + process: { + memory: { + heap: { + total_in_bytes: 0, + used_in_bytes: 0, + size_limit: 0, + }, + resident_set_size_in_bytes: 0, + }, + event_loop_delay: 10, + pid: 10, + uptime_in_millis: 1000, + }, + os: { + platform: 'darwin', + platformRelease: 'test', + load: { + '1m': 0.5, + '5m': 1, + '15m': 3, + }, + memory: { + total_in_bytes: 10, + free_in_bytes: 10, + used_in_bytes: 10, + }, + uptime_in_millis: 1000, + }, + response_times: { avg_in_millis: 100, max_in_millis: 200 }, + requests: { + disconnects: 10, + total: 100, + statusCodes: { 200: 100 }, + }, + concurrent_connections: 20, + }; + + beforeAll(() => registerOpsStatsCollector(usageCollectionMock, metrics$)); + afterAll(() => jest.clearAllTimers()); + + test('registered collector is set', () => { + expect(collector).not.toBeUndefined(); + expect(collector.type).toBe('kibana_stats'); + }); + + test('isReady should return false because no metrics have been provided yet', () => { + expect(collector.isReady()).toBe(false); + }); + + test('should return something when there is a metric', async () => { + metrics$.next(metric); + expect(collector.isReady()).toBe(true); + expect(await collector.fetch(callCluster)).toMatchSnapshot({ + concurrent_connections: 20, + os: { + load: { + '15m': 3, + '1m': 0.5, + '5m': 1, + }, + memory: { + free_in_bytes: 10, + total_in_bytes: 10, + used_in_bytes: 10, + }, + platform: 'darwin', + platformRelease: 'test', + uptime_in_millis: 1000, + }, + process: { + event_loop_delay: 10, + memory: { + heap: { + size_limit: 0, + total_in_bytes: 0, + used_in_bytes: 0, + }, + resident_set_size_in_bytes: 0, + }, + uptime_in_millis: 1000, + }, + requests: { + disconnects: 10, + total: 100, + }, + response_times: { + average: 100, + max: 200, + }, + timestamp: expect.any(String), + }); + }); +}); diff --git a/src/legacy/server/status/collectors/index.js b/src/plugins/telemetry/server/collectors/ops_stats/index.ts similarity index 91% rename from src/legacy/server/status/collectors/index.js rename to src/plugins/telemetry/server/collectors/ops_stats/index.ts index 92d9e601bbb35..443a25749d200 100644 --- a/src/legacy/server/status/collectors/index.js +++ b/src/plugins/telemetry/server/collectors/ops_stats/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { registerOpsStatsCollector } from './get_ops_stats_collector'; +export { registerOpsStatsCollector } from './ops_stats_collector'; diff --git a/src/plugins/telemetry/server/collectors/ops_stats/ops_stats_collector.ts b/src/plugins/telemetry/server/collectors/ops_stats/ops_stats_collector.ts new file mode 100644 index 0000000000000..4967e20006ddd --- /dev/null +++ b/src/plugins/telemetry/server/collectors/ops_stats/ops_stats_collector.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { cloneDeep } from 'lodash'; +import moment from 'moment'; +import { OpsMetrics } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { KIBANA_STATS_TYPE } from '../../../common/constants'; + +interface OpsStatsMetrics extends Omit { + timestamp: string; + response_times: { + average: number; + max: number; + }; +} + +/** + * Initialize a collector for Kibana Ops Stats + */ +export function getOpsStatsCollector( + usageCollection: UsageCollectionSetup, + metrics$: Observable +) { + let lastMetrics: OpsStatsMetrics | null = null; + metrics$.subscribe(_metrics => { + const metrics = cloneDeep(_metrics); + // Ensure we only include the same data that Metricbeat collection would get + delete metrics.process.pid; + const responseTimes = { + average: metrics.response_times.avg_in_millis, + max: metrics.response_times.max_in_millis, + }; + delete metrics.requests.statusCodes; + lastMetrics = { + ...metrics, + response_times: responseTimes, + timestamp: moment.utc().toISOString(), + }; + }); + + return usageCollection.makeStatsCollector({ + type: KIBANA_STATS_TYPE, + isReady: () => !!lastMetrics, + fetch: () => lastMetrics, + }); +} + +export function registerOpsStatsCollector( + usageCollection: UsageCollectionSetup, + metrics$: Observable +) { + usageCollection.registerCollector(getOpsStatsCollector(usageCollection, metrics$)); +} diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 77036b4ea7ddc..1df6a665e4d76 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -32,6 +32,8 @@ import { SavedObjectsClient, Plugin, Logger, + SharedGlobalConfig, + MetricsServiceSetup, } from '../../../core/server'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; @@ -41,6 +43,8 @@ import { registerTelemetryPluginUsageCollector, registerManagementUsageCollector, registerApplicationUsageCollector, + registerKibanaUsageCollector, + registerOpsStatsCollector, } from './collectors'; import { TelemetryConfigType } from './config'; import { FetcherTask } from './fetcher'; @@ -61,6 +65,7 @@ export class TelemetryPlugin implements Plugin { private readonly logger: Logger; private readonly currentKibanaVersion: string; private readonly config$: Observable; + private readonly legacyConfig$: Observable; private readonly isDev: boolean; private readonly fetcherTask: FetcherTask; private savedObjectsClient?: ISavedObjectsRepository; @@ -71,6 +76,7 @@ export class TelemetryPlugin implements Plugin { this.isDev = initializerContext.env.mode.dev; this.currentKibanaVersion = initializerContext.env.packageInfo.version; this.config$ = initializerContext.config.create(); + this.legacyConfig$ = initializerContext.config.legacy.globalConfig$; this.fetcherTask = new FetcherTask({ ...initializerContext, logger: this.logger, @@ -78,15 +84,15 @@ export class TelemetryPlugin implements Plugin { } public async setup( - core: CoreSetup, + { elasticsearch, http, savedObjects, metrics }: CoreSetup, { usageCollection, telemetryCollectionManager }: TelemetryPluginsSetup ) { const currentKibanaVersion = this.currentKibanaVersion; const config$ = this.config$; const isDev = this.isDev; - registerCollection(telemetryCollectionManager, core.elasticsearch.dataClient); - const router = core.http.createRouter(); + registerCollection(telemetryCollectionManager, elasticsearch.dataClient); + const router = http.createRouter(); registerRoutes({ config$, @@ -96,8 +102,8 @@ export class TelemetryPlugin implements Plugin { telemetryCollectionManager, }); - this.registerMappings(opts => core.savedObjects.registerType(opts)); - this.registerUsageCollectors(usageCollection, opts => core.savedObjects.registerType(opts)); + this.registerMappings(opts => savedObjects.registerType(opts)); + this.registerUsageCollectors(usageCollection, metrics, opts => savedObjects.registerType(opts)); } public async start(core: CoreStart, { telemetryCollectionManager }: TelemetryPluginsStart) { @@ -153,11 +159,14 @@ export class TelemetryPlugin implements Plugin { private registerUsageCollectors( usageCollection: UsageCollectionSetup, + metrics: MetricsServiceSetup, registerType: SavedObjectsRegisterType ) { const getSavedObjectsClient = () => this.savedObjectsClient; const getUiSettingsClient = () => this.uiSettingsClient; + registerOpsStatsCollector(usageCollection, metrics.getOpsMetrics$()); + registerKibanaUsageCollector(usageCollection, this.legacyConfig$); registerTelemetryPluginUsageCollector(usageCollection, { currentKibanaVersion: this.currentKibanaVersion, config$: this.config$, diff --git a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts index 86c6731e11d37..a17f1b8232a22 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts @@ -55,6 +55,10 @@ export function handleKibanaStats( ...kibanaStats.os, }; const formattedOsStats = Object.entries(os).reduce((acc, [key, value]) => { + if (typeof value !== 'string') { + // There are new fields reported now from the "os" property like "load", "memory", etc. They are objects. + return acc; + } return { ...acc, [`${key}s`]: [{ [key]: value, count: 1 }], diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index 3a4c7b71dcd03..edd6142455dfb 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -28,12 +28,6 @@ export const KIBANA_STATS_TYPE_MONITORING = 'kibana_stats'; // similar to KIBANA * @type {string} */ export const KIBANA_SETTINGS_TYPE = 'kibana_settings'; -/** - * The type name used within the Monitoring index to publish Kibana usage stats. - * NOTE: this string shows as-is in the stats API as a field name for the kibana usage stats - * @type {string} - */ -export const KIBANA_USAGE_TYPE = 'kibana'; /* * Key for the localStorage service diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.ts deleted file mode 100644 index 2c40ac56e19ec..0000000000000 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.ts +++ /dev/null @@ -1,86 +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 { get, snakeCase } from 'lodash'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { KIBANA_USAGE_TYPE, KIBANA_STATS_TYPE_MONITORING } from '../../../common/constants'; - -const TYPES = [ - 'dashboard', - 'visualization', - 'search', - 'index-pattern', - 'graph-workspace', - 'timelion-sheet', -]; - -/** - * Fetches saved object counts by querying the .kibana index - */ -export function getKibanaUsageCollector(usageCollection: any, kibanaIndex: string) { - return usageCollection.makeUsageCollector({ - type: KIBANA_USAGE_TYPE, - isReady: () => true, - async fetch(callCluster: CallCluster) { - const savedObjectCountSearchParams = { - index: kibanaIndex, - ignoreUnavailable: true, - filterPath: 'aggregations.types.buckets', - body: { - size: 0, - query: { - terms: { type: TYPES }, - }, - aggs: { - types: { - terms: { field: 'type', size: TYPES.length }, - }, - }, - }, - }; - - const resp = await callCluster('search', savedObjectCountSearchParams); - const buckets: any = get(resp, 'aggregations.types.buckets', []); - - // get the doc_count from each bucket - const bucketCounts = buckets.reduce( - (acc: any, bucket: any) => ({ - ...acc, - [bucket.key]: bucket.doc_count, - }), - {} - ); - - return { - index: kibanaIndex, - ...TYPES.reduce( - (acc, type) => ({ - // combine the bucketCounts and 0s for types that don't have documents - ...acc, - [snakeCase(type)]: { - total: bucketCounts[type] || 0, - }, - }), - {} - ), - }; - }, - - /* - * Format the response data into a model for internal upload - * 1. Make this data part of the "kibana_stats" type - * 2. Organize the payload in the usage namespace of the data payload (usage.index, etc) - */ - formatForBulkUpload: (result: any) => { - return { - type: KIBANA_STATS_TYPE_MONITORING, - payload: { - usage: result, - }, - }; - }, - }); -} diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.ts deleted file mode 100644 index 85357f786ddc1..0000000000000 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Observable } from 'rxjs'; -import { cloneDeep } from 'lodash'; -import moment from 'moment'; -import { OpsMetrics } from 'kibana/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { KIBANA_STATS_TYPE_MONITORING } from '../../../common/constants'; - -interface MonitoringOpsMetrics extends OpsMetrics { - timestamp: string; -} - -/* - * Initialize a collector for Kibana Ops Stats - */ -export function getOpsStatsCollector( - usageCollection: UsageCollectionSetup, - metrics$: Observable -) { - let lastMetrics: MonitoringOpsMetrics | null = null; - metrics$.subscribe(_metrics => { - const metrics: any = cloneDeep(_metrics); - // Ensure we only include the same data that Metricbeat collection would get - delete metrics.process.pid; - metrics.response_times = { - average: metrics.response_times.avg_in_millis, - max: metrics.response_times.max_in_millis, - }; - delete metrics.requests.statusCodes; - lastMetrics = { - ...metrics, - timestamp: moment.utc().toISOString(), - }; - }); - - return usageCollection.makeStatsCollector({ - type: KIBANA_STATS_TYPE_MONITORING, - isReady: () => !!lastMetrics, - fetch: () => lastMetrics, - }); -} diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts index e41b1512f1b29..dcd35b0d323eb 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts @@ -3,20 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Observable } from 'rxjs'; -import { OpsMetrics } from 'kibana/server'; -import { getKibanaUsageCollector } from './get_kibana_usage_collector'; -import { getOpsStatsCollector } from './get_ops_stats_collector'; + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { getSettingsCollector } from './get_settings_collector'; import { MonitoringConfig } from '../../config'; export function registerCollectors( - usageCollection: any, - config: MonitoringConfig, - opsMetrics$: Observable, - kibanaIndex: string + usageCollection: UsageCollectionSetup, + config: MonitoringConfig ) { - usageCollection.registerCollector(getOpsStatsCollector(usageCollection, opsMetrics$)); - usageCollection.registerCollector(getKibanaUsageCollector(usageCollection, kibanaIndex)); usageCollection.registerCollector(getSettingsCollector(usageCollection, config)); } diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 784226dca66fe..920accd17ec8b 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -177,12 +177,7 @@ export class Plugin { // Register collector objects for stats to show up in the APIs if (plugins.usageCollection) { - registerCollectors( - plugins.usageCollection, - config, - core.metrics.getOpsMetrics$(), - get(legacyConfig, 'kibana.index') - ); + registerCollectors(plugins.usageCollection, config); } // If collection is enabled, create the bulk uploader From 267f22c2ee174af6026773b04d782faf565d654c Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 8 Apr 2020 10:21:06 +0200 Subject: [PATCH 05/81] 2Mb --> 53kB (#62781) --- x-pack/plugins/watcher/public/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/watcher/public/plugin.ts b/x-pack/plugins/watcher/public/plugin.ts index 9bee7e60273e4..6de21bc27d48c 100644 --- a/x-pack/plugins/watcher/public/plugin.ts +++ b/x-pack/plugins/watcher/public/plugin.ts @@ -12,7 +12,6 @@ import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { LicenseStatus } from '../common/types/license_status'; import { ILicense } from '../../licensing/public'; -import { TimeBuckets } from './legacy'; import { PLUGIN } from '../common/constants'; import { Dependencies } from './types'; @@ -41,6 +40,7 @@ export class WatcherUIPlugin implements Plugin { const [core] = await getStartServices(); const { i18n: i18nDep, docLinks, savedObjects } = core; const { boot } = await import('./application/boot'); + const { TimeBuckets } = await import('./legacy'); return boot({ // Skip the first license status, because that's already been used to determine From 314c4f782f434dc799a732036f273bbc9d5c5f3a Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Wed, 8 Apr 2020 12:00:13 +0300 Subject: [PATCH 06/81] [NP] Vis Default Editor plugin (#62475) * Move the default_editor to NP * Fix paths * Import styles through the visualize * Other fixes * Fix ip_ranges exhaustive-deps array * Fix filters and extend bounds * Other fixes * Fix date_ranges tests * Use useMount on first render Co-authored-by: Elastic Machine --- .eslintrc.js | 20 ---- .i18nrc.json | 2 +- .../public/components/editor/controls_tab.tsx | 2 +- .../public/components/editor/options_tab.tsx | 2 +- .../kibana/public/visualize/_index.scss | 3 + .../public/visualize/kibana_services.ts | 2 +- .../kibana/public/visualize/plugin.ts | 2 +- .../public/components/region_map_options.tsx | 2 +- .../region_map/public/region_map_type.js | 2 +- .../public/components/tile_map_options.tsx | 2 +- .../tile_map/public/tile_map_type.js | 2 +- .../core_plugins/vis_default_editor/index.ts | 42 -------- .../vis_default_editor/package.json | 4 - .../public/markdown_options.tsx | 2 +- .../vis_type_markdown/public/markdown_vis.ts | 2 +- .../public/settings_options.tsx | 2 +- .../public/components/metric_vis_options.tsx | 2 +- .../public/metric_vis_fn.test.ts | 4 - .../public/metric_vis_type.test.ts | 4 - .../vis_type_metric/public/metric_vis_type.ts | 2 +- .../public/components/table_vis_options.tsx | 2 +- .../vis_type_table/public/table_vis_type.ts | 2 +- .../public/components/tag_cloud_options.tsx | 2 +- .../public/tag_cloud_type.ts | 2 +- .../public/components/timelion_interval.tsx | 2 +- .../public/timelion_options.tsx | 2 +- .../public/timelion_vis_type.tsx | 2 +- .../public/components/vega_vis_editor.tsx | 2 +- .../vis_type_vega/public/vega_type.ts | 3 +- .../vis_type_vislib/public/area.ts | 2 +- .../components/common/basic_options.tsx | 2 +- .../public/components/common/color_ranges.tsx | 5 +- .../public/components/common/color_schema.tsx | 2 +- .../components/common/validation_wrapper.tsx | 2 +- .../public/components/options/gauge/index.tsx | 2 +- .../components/options/heatmap/index.tsx | 2 +- .../options/heatmap/labels_panel.tsx | 2 +- .../metrics_axes/category_axis_panel.tsx | 2 +- .../public/components/options/pie.tsx | 2 +- .../options/point_series/grid_panel.tsx | 2 +- .../vis_type_vislib/public/gauge.ts | 2 +- .../vis_type_vislib/public/goal.ts | 2 +- .../vis_type_vislib/public/heatmap.ts | 2 +- .../vis_type_vislib/public/histogram.ts | 2 +- .../vis_type_vislib/public/horizontal_bar.ts | 2 +- .../vis_type_vislib/public/line.ts | 2 +- .../vis_type_vislib/public/pie.ts | 2 +- .../public/utils/common_config.tsx | 2 +- src/plugins/vis_default_editor/README.md | 16 ++++ .../vis_default_editor/public/_agg.scss | 0 .../public/_agg_params.scss | 0 .../vis_default_editor/public/_default.scss | 0 .../vis_default_editor/public/_sidebar.scss | 0 .../__snapshots__/agg.test.tsx.snap | 0 .../__snapshots__/agg_group.test.tsx.snap | 0 .../public/components/agg.test.tsx | 0 .../public/components/agg.tsx | 3 +- .../public/components/agg_add.tsx | 2 +- .../public/components/agg_common_props.ts | 0 .../public/components/agg_group.test.tsx | 0 .../public/components/agg_group.tsx | 3 +- .../components/agg_group_helper.test.ts | 0 .../public/components/agg_group_helper.tsx | 0 .../public/components/agg_group_state.tsx | 0 .../public/components/agg_param.tsx | 0 .../public/components/agg_param_props.ts | 0 .../public/components/agg_params.test.tsx | 4 +- .../public/components/agg_params.tsx | 4 +- .../components/agg_params_helper.test.ts | 0 .../public/components/agg_params_helper.ts | 2 +- .../public/components/agg_params_map.ts | 7 +- .../public/components/agg_params_state.ts | 0 .../public/components/agg_select.tsx | 2 +- .../extended_bounds.test.tsx.snap | 0 .../__snapshots__/metric_agg.test.tsx.snap | 0 .../controls/__snapshots__/size.test.tsx.snap | 0 .../__snapshots__/top_aggregate.test.tsx.snap | 0 .../components/controls/agg_control_props.tsx | 0 .../components/controls/agg_utils.test.tsx | 0 .../components/controls/auto_precision.tsx | 0 .../controls/components/from_to_list.tsx | 74 ++++++++------- .../controls/components/input_list.tsx | 95 +++++++++++-------- .../controls/components/mask_list.tsx | 72 +++++++------- .../__snapshots__/number_list.test.tsx.snap | 0 .../__snapshots__/number_row.test.tsx.snap | 0 .../controls/components/number_list/index.ts | 0 .../number_list/number_list.test.tsx | 0 .../components/number_list/number_list.tsx | 4 +- .../number_list/number_row.test.tsx | 0 .../components/number_list/number_row.tsx | 0 .../components/number_list/range.test.ts | 1 - .../controls/components/number_list/range.ts | 0 .../components/number_list/utils.test.ts | 0 .../controls/components/number_list/utils.ts | 0 .../components/controls/date_ranges.test.tsx | 4 +- .../components/controls/date_ranges.tsx | 37 +++++--- .../components/controls/drop_partials.tsx | 0 .../controls/extended_bounds.test.tsx | 0 .../components/controls/extended_bounds.tsx | 0 .../public/components/controls/field.test.tsx | 0 .../public/components/controls/field.tsx | 8 +- .../public/components/controls/filter.tsx | 2 +- .../public/components/controls/filters.tsx | 9 +- .../controls/has_extended_bounds.tsx | 19 +++- .../public/components/controls/index.ts | 0 .../components/controls/ip_range_type.tsx | 0 .../public/components/controls/ip_ranges.tsx | 28 ++++-- .../controls/is_filtered_by_collar.tsx | 0 .../components/controls/metric_agg.test.tsx | 0 .../public/components/controls/metric_agg.tsx | 0 .../components/controls/min_doc_count.tsx | 0 .../components/controls/missing_bucket.tsx | 9 +- .../components/controls/number_interval.tsx | 2 +- .../public/components/controls/order.tsx | 2 +- .../components/controls/order_agg.test.tsx | 0 .../public/components/controls/order_agg.tsx | 4 +- .../public/components/controls/order_by.tsx | 10 +- .../components/controls/other_bucket.tsx | 0 .../components/controls/percentile_ranks.tsx | 0 .../components/controls/percentiles.test.tsx | 0 .../components/controls/percentiles.tsx | 0 .../public/components/controls/precision.tsx | 2 +- .../controls/radius_ratio_option.tsx | 7 +- .../components/controls/range_control.tsx | 0 .../public/components/controls/ranges.tsx | 63 ++++++------ .../public/components/controls/raw_json.tsx | 0 .../components/controls/rows_or_columns.tsx | 0 .../components/controls/scale_metrics.tsx | 0 .../public/components/controls/size.test.tsx | 0 .../public/components/controls/size.tsx | 2 +- .../public/components/controls/string.tsx | 2 +- .../public/components/controls/sub_agg.tsx | 5 +- .../public/components/controls/sub_metric.tsx | 9 +- .../public/components/controls/switch.tsx | 0 .../public/components/controls/test_utils.ts | 0 .../components/controls/time_interval.tsx | 4 +- .../controls/top_aggregate.test.tsx | 0 .../components/controls/top_aggregate.tsx | 4 +- .../public/components/controls/top_field.tsx | 0 .../public/components/controls/top_size.tsx | 0 .../components/controls/top_sort_field.tsx | 0 .../components/controls/use_geocentroid.tsx | 0 .../components/controls/utils/agg_utils.ts | 0 .../public/components/controls/utils/index.ts | 0 .../controls/utils/inline_comp_wrapper.tsx | 0 .../strings/comma_separated_list.test.ts | 0 .../utils/strings/comma_separated_list.ts | 0 .../controls/utils/strings/index.ts | 0 .../controls/utils/strings/prose.test.ts | 0 .../controls/utils/strings/prose.ts | 0 .../components/controls/utils/use_handlers.ts | 0 .../public/components/sidebar/controls.tsx | 2 +- .../public/components/sidebar/data_tab.tsx | 4 +- .../public/components/sidebar/index.ts | 0 .../public/components/sidebar/navbar.tsx | 0 .../public/components/sidebar/sidebar.tsx | 7 +- .../components/sidebar/sidebar_title.tsx | 4 +- .../components/sidebar/state/actions.ts | 0 .../components/sidebar/state/constants.ts | 0 .../sidebar/state/editor_form_state.ts | 0 .../public/components/sidebar/state/index.ts | 2 +- .../components/sidebar/state/reducers.ts | 2 +- .../public/components/utils/editor_config.ts | 0 .../public/components/utils/index.ts | 0 .../public/default_editor.tsx | 4 +- .../public/default_editor_controller.tsx | 6 +- .../vis_default_editor/public/editor_size.ts | 0 .../vis_default_editor/public/index.scss | 0 .../vis_default_editor/public/index.ts | 2 + .../vis_default_editor/public/schemas.ts | 2 +- .../vis_default_editor/public/types.ts | 0 .../vis_default_editor/public/utils.test.ts | 0 .../vis_default_editor/public/utils.ts | 0 .../public/vis_options_props.tsx | 0 .../public/vis_type_agg_filter.ts | 2 +- .../self_changing_editor.tsx | 2 +- 176 files changed, 366 insertions(+), 359 deletions(-) delete mode 100644 src/legacy/core_plugins/vis_default_editor/index.ts delete mode 100644 src/legacy/core_plugins/vis_default_editor/package.json create mode 100644 src/plugins/vis_default_editor/README.md rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/_agg.scss (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/_agg_params.scss (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/_default.scss (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/_sidebar.scss (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg.tsx (98%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_add.tsx (98%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_common_props.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_group.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_group.tsx (97%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_group_helper.test.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_group_helper.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_group_state.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_param.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_param_props.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_params.test.tsx (96%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_params.tsx (98%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_params_helper.test.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_params_helper.ts (99%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_params_map.ts (96%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_params_state.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/agg_select.tsx (98%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/__snapshots__/extended_bounds.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/__snapshots__/metric_agg.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/__snapshots__/size.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/__snapshots__/top_aggregate.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/agg_control_props.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/agg_utils.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/auto_precision.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/from_to_list.tsx (68%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/input_list.tsx (75%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/mask_list.tsx (62%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_list.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_row.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/number_list/index.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/number_list/number_list.tsx (98%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/number_list/number_row.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/number_list/range.test.ts (98%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/number_list/range.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/number_list/utils.test.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/components/number_list/utils.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/date_ranges.test.tsx (95%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/date_ranges.tsx (90%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/drop_partials.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/extended_bounds.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/extended_bounds.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/field.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/field.tsx (97%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/filter.tsx (99%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/filters.tsx (96%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/has_extended_bounds.tsx (73%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/index.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/ip_range_type.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/ip_ranges.tsx (79%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/is_filtered_by_collar.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/metric_agg.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/metric_agg.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/min_doc_count.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/missing_bucket.tsx (93%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/number_interval.tsx (99%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/order.tsx (98%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/order_agg.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/order_agg.tsx (97%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/order_by.tsx (94%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/other_bucket.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/percentile_ranks.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/percentiles.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/percentiles.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/precision.tsx (95%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/radius_ratio_option.tsx (95%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/range_control.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/ranges.tsx (91%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/raw_json.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/rows_or_columns.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/scale_metrics.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/size.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/size.tsx (98%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/string.tsx (98%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/sub_agg.tsx (97%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/sub_metric.tsx (96%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/switch.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/test_utils.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/time_interval.tsx (98%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/top_aggregate.test.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/top_aggregate.tsx (97%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/top_field.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/top_size.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/top_sort_field.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/use_geocentroid.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/utils/agg_utils.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/utils/index.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/utils/inline_comp_wrapper.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.test.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/utils/strings/index.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/utils/strings/prose.test.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/utils/strings/prose.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/controls/utils/use_handlers.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/sidebar/controls.tsx (98%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/sidebar/data_tab.tsx (97%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/sidebar/index.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/sidebar/navbar.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/sidebar/sidebar.tsx (96%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/sidebar/sidebar_title.tsx (97%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/sidebar/state/actions.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/sidebar/state/constants.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/sidebar/state/editor_form_state.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/sidebar/state/index.ts (96%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/sidebar/state/reducers.ts (99%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/utils/editor_config.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/components/utils/index.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/default_editor.tsx (94%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/default_editor_controller.tsx (92%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/editor_size.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/index.scss (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/index.ts (97%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/schemas.ts (96%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/types.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/utils.test.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/utils.ts (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/vis_options_props.tsx (100%) rename src/{legacy/core_plugins => plugins}/vis_default_editor/public/vis_type_agg_filter.ts (97%) diff --git a/.eslintrc.js b/.eslintrc.js index 3c173e5244009..2ce6d279d93a9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -69,26 +69,6 @@ module.exports = { 'jsx-a11y/no-onchange': 'off', }, }, - { - files: ['src/legacy/core_plugins/expressions/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, - { - files: [ - 'src/legacy/core_plugins/vis_default_editor/public/components/controls/**/*.{ts,tsx}', - ], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, - { - files: ['src/legacy/ui/public/vis/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, { files: ['src/plugins/es_ui_shared/**/*.{js,ts,tsx}'], rules: { diff --git a/.i18nrc.json b/.i18nrc.json index c293b3103a39c..3b0b40b40792e 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -43,7 +43,7 @@ "tileMap": "src/legacy/core_plugins/tile_map", "timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion", "src/plugins/timelion"], "uiActions": "src/plugins/ui_actions", - "visDefaultEditor": "src/legacy/core_plugins/vis_default_editor", + "visDefaultEditor": "src/plugins/vis_default_editor", "visTypeMarkdown": "src/legacy/core_plugins/vis_type_markdown", "visTypeMetric": "src/legacy/core_plugins/vis_type_metric", "visTypeTable": "src/legacy/core_plugins/vis_type_table", diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx index 029e1f149d052..b7114f1029ef2 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx @@ -30,7 +30,7 @@ import { EuiSelect, } from '@elastic/eui'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { IIndexPattern } from 'src/plugins/data/public'; import { ControlEditor } from './control_editor'; import { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx index 95b14619c3416..cdff6cabad8ba 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx @@ -23,7 +23,7 @@ import { EuiForm, EuiFormRow, EuiSwitch } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSwitchEvent } from '@elastic/eui'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; interface OptionsTabParams { updateFiltersOnChange: boolean; diff --git a/src/legacy/core_plugins/kibana/public/visualize/_index.scss b/src/legacy/core_plugins/kibana/public/visualize/_index.scss index 0632831578bd0..079d82936bb57 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/_index.scss +++ b/src/legacy/core_plugins/kibana/public/visualize/_index.scss @@ -1,2 +1,5 @@ // Visualize plugin styles @import 'np_ready/index'; + +// should be removed while moving the visualize into NP +@import '../../../../../plugins/vis_default_editor/public/index' diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index addc608efd57d..d5440c4677d8a 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -36,7 +36,7 @@ import { VisualizationsStart } from '../../../../../plugins/visualizations/publi import { SavedVisualizations } from './np_ready/types'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; -import { DefaultEditorController } from '../../../vis_default_editor/public'; +import { DefaultEditorController } from '../../../../../plugins/vis_default_editor/public'; export interface VisualizeKibanaServices { pluginInitializerContext: PluginInitializerContext; diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 81d8458010f19..4ffbc307c69a8 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -51,7 +51,7 @@ import { HomePublicPluginSetup, } from '../../../../../plugins/home/public'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; -import { DefaultEditorController } from '../../../vis_default_editor/public'; +import { DefaultEditorController } from '../../../../../plugins/vis_default_editor/public'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; diff --git a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx index 4219e2e715150..61cfbf00ded9e 100644 --- a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx +++ b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { FileLayerField, VectorLayer, ServiceSettings } from 'ui/vis/map/service_settings'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { NumberInputOption, SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; import { WmsOptions } from '../../../tile_map/public/components/wms_options'; import { RegionMapVisParams } from '../types'; diff --git a/src/legacy/core_plugins/region_map/public/region_map_type.js b/src/legacy/core_plugins/region_map/public/region_map_type.js index 4faa3f92abb5a..9174b03cf843c 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_type.js +++ b/src/legacy/core_plugins/region_map/public/region_map_type.js @@ -22,7 +22,7 @@ import { mapToLayerWithId } from './util'; import { createRegionMapVisualization } from './region_map_visualization'; import { RegionMapOptions } from './components/region_map_options'; import { truncatedColorSchemas } from '../../../../plugins/charts/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; // TODO: reference to TILE_MAP plugin should be removed import { ORIGIN } from '../../tile_map/common/origin'; diff --git a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx index 168f56b771b7e..9169647aa2aef 100644 --- a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx @@ -21,7 +21,7 @@ import React, { useEffect } from 'react'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { BasicOptions, RangeOption, diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_type.js b/src/legacy/core_plugins/tile_map/public/tile_map_type.js index 39d39a4c8f8fc..fe82ad5c7352b 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_type.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_type.js @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { createTileMapVisualization } from './tile_map_visualization'; import { TileMapOptions } from './components/tile_map_options'; import { MapTypes } from './map_types'; diff --git a/src/legacy/core_plugins/vis_default_editor/index.ts b/src/legacy/core_plugins/vis_default_editor/index.ts deleted file mode 100644 index ee7b5ee4a62ff..0000000000000 --- a/src/legacy/core_plugins/vis_default_editor/index.ts +++ /dev/null @@ -1,42 +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 { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const vidDefaultEditorPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'vis_default_editor', - require: [], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default vidDefaultEditorPluginInitializer; diff --git a/src/legacy/core_plugins/vis_default_editor/package.json b/src/legacy/core_plugins/vis_default_editor/package.json deleted file mode 100644 index 77dcaff41da6b..0000000000000 --- a/src/legacy/core_plugins/vis_default_editor/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "vis_default_editor", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx b/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx index 8a4297d3b8149..a6349793619a0 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx +++ b/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx @@ -30,7 +30,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { MarkdownVisParams } from './types'; function MarkdownOptions({ stateParams, setValue }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts b/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts index b84d9638eb973..57ea6d9c9bb3d 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { MarkdownVisWrapper } from './markdown_vis_controller'; import { MarkdownOptions } from './markdown_options'; import { SettingsOptions } from './settings_options'; -import { DefaultEditorSize } from '../../vis_default_editor/public'; +import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; export const markdownVisDefinition = { name: 'markdown', diff --git a/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx b/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx index ac1d4bcc82cec..552fd63373554 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx +++ b/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { RangeOption, SwitchOption } from '../../vis_type_vislib/public'; import { MarkdownVisParams } from './types'; diff --git a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx b/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx index 661f16d6497ba..5c3032511f09a 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx +++ b/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx @@ -29,7 +29,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { ColorModes, ColorRanges, diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts index 22c32895d6803..4094cd4eff060 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts @@ -23,10 +23,6 @@ import { functionWrapper } from '../../../../plugins/expressions/common/expressi jest.mock('ui/new_platform'); -jest.mock('../../vis_default_editor/public', () => ({ - Schemas: class {}, -})); - describe('interpreter/functions#metric', () => { const fn = functionWrapper(createMetricVisFn()); const context = { diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts index 459da47556307..706693eff1007 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts @@ -22,10 +22,6 @@ import { MetricVisComponent } from './components/metric_vis_component'; jest.mock('ui/new_platform'); -jest.mock('../../vis_default_editor/public', () => ({ - Schemas: class {}, -})); - describe('metric_vis - createMetricVisTypeDefinition', () => { it('has metric vis component set', () => { const def = createMetricVisTypeDefinition(); diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts index f29164f7e540d..3bbb8964122e5 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts @@ -24,7 +24,7 @@ import { MetricVisOptions } from './components/metric_vis_options'; import { ColorModes } from '../../vis_type_vislib/public'; import { ColorSchemas, colorSchemas } from '../../../../plugins/charts/public'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; export const createMetricVisTypeDefinition = () => ({ name: 'metric', diff --git a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx index 30a9526273166..d01ab31e0a843 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx @@ -23,7 +23,7 @@ import { EuiIconTip, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { search } from '../../../../../plugins/data/public'; import { NumberInputOption, SwitchOption, SelectOption } from '../../../vis_type_vislib/public'; import { TableVisParams } from '../types'; diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts index d26e860e51272..43816121bc23b 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { Vis } from '../../../../plugins/visualizations/public'; import { tableVisResponseHandler } from './table_vis_response_handler'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index a9e816f70cf53..80e4e1de7ddab 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -20,8 +20,8 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { ValidatedDualRange } from '../../../../../../src/plugins/kibana_react/public'; -import { VisOptionsProps } from '../../../vis_default_editor/public'; import { SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; import { TagCloudVisParams } from '../types'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts index 5a8cc3004a315..b7dfa62c93fb9 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { TagCloudOptions } from './components/tag_cloud_options'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx index 6e29b111d422a..8a8e1b22fb78d 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx +++ b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { search } from '../../../../../plugins/data/public'; const { isValidEsInterval } = search.aggs; -import { useValidation } from '../../../vis_default_editor/public'; +import { useValidation } from '../../../../../plugins/vis_default_editor/public'; const intervalOptions = [ { diff --git a/src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx b/src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx index b7c40e15c11fd..afffcf7ccaf7a 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx +++ b/src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx @@ -20,9 +20,9 @@ import React, { useCallback } from 'react'; import { EuiPanel } from '@elastic/eui'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { VisParams } from './timelion_vis_fn'; import { TimelionInterval, TimelionExpressionInput } from './components'; -import { VisOptionsProps } from '../../vis_default_editor/public'; function TimelionOptions({ stateParams, setValue, setValidity }: VisOptionsProps) { const setInterval = useCallback((value: VisParams['interval']) => setValue('interval', value), [ diff --git a/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx b/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx index 6679553004097..5be77b3e51a6a 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx +++ b/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { KibanaContextProvider } from '../../../../plugins/kibana_react/public'; -import { DefaultEditorSize } from '../../vis_default_editor/public'; +import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; import { getTimelionRequestHandler } from './helpers/timelion_request_handler'; import { TimelionVisComponent, TimelionVisComponentProp } from './components'; import { TimelionOptions } from './timelion_options'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx b/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx index 707a6830b5ba4..31144065d7b40 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx +++ b/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx @@ -24,11 +24,11 @@ import compactStringify from 'json-stringify-pretty-compact'; import hjson from 'hjson'; import { i18n } from '@kbn/i18n'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { getNotifications } from '../services'; import { VisParams } from '../vega_fn'; import { VegaHelpMenu } from './vega_help_menu'; import { VegaActionsMenu } from './vega_actions_menu'; -import { VisOptionsProps } from '../../../vis_default_editor/public'; const aceOptions = { maxLines: Infinity, diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts index b0ec90d2c378f..5d9ed5c8df91c 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts @@ -18,8 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { DefaultEditorSize } from '../../vis_default_editor/public'; +import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/area.ts b/src/legacy/core_plugins/vis_type_vislib/public/area.ts index e79555470298b..68decacaaa040 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/area.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/area.ts @@ -24,7 +24,7 @@ import { palettes } from '@elastic/eui/lib/services'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { Positions, ChartTypes, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx index 1138f66d21cfa..baf3e8ecd1b28 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from '../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { SwitchOption } from './switch'; import { SelectOption } from './select'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_ranges.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_ranges.tsx index 2c9b1b543e8c2..84c70f10b12da 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_ranges.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_ranges.tsx @@ -22,7 +22,10 @@ import { last } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { RangeValues, RangesParamEditor } from '../../../../vis_default_editor/public'; +import { + RangeValues, + RangesParamEditor, +} from '../../../../../../plugins/vis_default_editor/public'; export type SetColorRangeValue = (paramName: string, value: RangeValues[]) => void; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_schema.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_schema.tsx index 06ce0a2b4af64..35d7f7b13235e 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_schema.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_schema.tsx @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { SelectOption } from './select'; import { SwitchOption } from './switch'; import { ColorSchemaVislibParams } from '../../types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx index c069d4c935669..718056fd85492 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx @@ -19,7 +19,7 @@ import React, { useEffect, useState, useCallback } from 'react'; -import { VisOptionsProps } from '../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; export interface ValidationVisOptionsProps extends VisOptionsProps { setMultipleValidity(paramName: string, isValid: boolean): void; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/index.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/index.tsx index 706035a7b814e..6109b548f9412 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/index.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/index.tsx @@ -20,7 +20,7 @@ import React, { useCallback } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { GaugeVisParams } from '../../../gauge'; import { RangesPanel } from './ranges_panel'; import { StylePanel } from './style_panel'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx index 452b9ed9bdbb1..715b5902b69da 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx @@ -23,7 +23,7 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { BasicOptions, ColorRanges, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx index c74f0ef765c8d..38811bd836459 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx @@ -23,7 +23,7 @@ import { EuiColorPicker, EuiFormRow, EuiPanel, EuiSpacer, EuiTitle } from '@elas import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { ValueAxis } from '../../../types'; import { HeatmapVisParams } from '../../../heatmap'; import { SwitchOption } from '../../common'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx index 049df0cdd77be..915885388640c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx @@ -23,7 +23,7 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { Axis } from '../../../types'; import { SelectOption, SwitchOption } from '../../common'; import { LabelOptions, SetAxisLabel } from './label_options'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx index 2182edafb3ebf..4c0be456aad64 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx @@ -22,7 +22,7 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { BasicOptions, TruncateLabelsOption, SwitchOption } from '../common'; import { PieVisParams } from '../../pie'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx index db9acafac305c..bb2b3f8fddb49 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx @@ -22,7 +22,7 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { SelectOption, SwitchOption } from '../../common'; import { BasicVislibParams, ValueAxis } from '../../../types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts b/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts index 4610bd37db5f1..7ad821dbf2f30 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { RangeValues, Schemas } from '../../vis_default_editor/public'; +import { RangeValues, Schemas } from '../../../../plugins/vis_default_editor/public'; import { AggGroupNames } from '../../../../plugins/data/public'; import { GaugeOptions } from './components/options'; import { getGaugeCollections, Alignments, ColorModes, GaugeTypes } from './utils/collections'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/goal.ts b/src/legacy/core_plugins/vis_type_vislib/public/goal.ts index c918128d01f11..6c311bebe0717 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/goal.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/goal.ts @@ -25,7 +25,7 @@ import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { ColorSchemas } from '../../../../plugins/charts/public'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'goal', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts b/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts index 39a583f3c9641..88b4f0fcaf87e 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { RangeValues, Schemas } from '../../vis_default_editor/public'; +import { RangeValues, Schemas } from '../../../../plugins/vis_default_editor/public'; import { AggGroupNames } from '../../../../plugins/data/public'; import { AxisTypes, getHeatmapCollections, Positions, ScaleTypes } from './utils/collections'; import { HeatmapOptions } from './components/options'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts b/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts index 15ef369e5150e..54ec8f98203e2 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts @@ -24,7 +24,7 @@ import { palettes } from '@elastic/eui/lib/services'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { Positions, ChartTypes, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts b/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts index 8b5811628855c..dc47252ccd44f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts @@ -24,7 +24,7 @@ import { palettes } from '@elastic/eui/lib/services'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { Positions, ChartTypes, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/line.ts b/src/legacy/core_plugins/vis_type_vislib/public/line.ts index ac4cda869fe29..885ab295d11e1 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/line.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/line.ts @@ -24,7 +24,7 @@ import { palettes } from '@elastic/eui/lib/services'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { Positions, ChartTypes, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie.ts b/src/legacy/core_plugins/vis_type_vislib/public/pie.ts index 0f1bd93f5b5bd..2774836baa381 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/pie.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../vis_default_editor/public'; +import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { PieOptions } from './components/options'; import { getPositions, Positions } from './utils/collections'; import { createVislibVisController } from './vis_controller'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx b/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx index 6da40686a8b50..de867dc72bba7 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from '../../../vis_default_editor/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { PointSeriesOptions, MetricsAxisOptions } from '../components/options'; import { ValidationWrapper } from '../components/common'; import { BasicVislibParams } from '../types'; diff --git a/src/plugins/vis_default_editor/README.md b/src/plugins/vis_default_editor/README.md new file mode 100644 index 0000000000000..8607dfe486ace --- /dev/null +++ b/src/plugins/vis_default_editor/README.md @@ -0,0 +1,16 @@ +# Visualization Deafult Editor plugin + +The default editor is used in most primary visualizations, e.x. `Area`, `Data table`, `Pie`, etc. +It acts as a container for a particular visualization and options tabs. Contains the default "Data" tab in `public/components/sidebar/data_tab.tsx`. +The plugin exposes the static `DefaultEditorController` class to consume. + +```ts +import { DefaultEditorController } from '../../vis_default_editor/public'; + +const editor = new DefaultEditorController( + element, + vis, + eventEmitter, + embeddableHandler +); +``` \ No newline at end of file diff --git a/src/legacy/core_plugins/vis_default_editor/public/_agg.scss b/src/plugins/vis_default_editor/public/_agg.scss similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/_agg.scss rename to src/plugins/vis_default_editor/public/_agg.scss diff --git a/src/legacy/core_plugins/vis_default_editor/public/_agg_params.scss b/src/plugins/vis_default_editor/public/_agg_params.scss similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/_agg_params.scss rename to src/plugins/vis_default_editor/public/_agg_params.scss diff --git a/src/legacy/core_plugins/vis_default_editor/public/_default.scss b/src/plugins/vis_default_editor/public/_default.scss similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/_default.scss rename to src/plugins/vis_default_editor/public/_default.scss diff --git a/src/legacy/core_plugins/vis_default_editor/public/_sidebar.scss b/src/plugins/vis_default_editor/public/_sidebar.scss similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/_sidebar.scss rename to src/plugins/vis_default_editor/public/_sidebar.scss diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap b/src/plugins/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg.test.tsx b/src/plugins/vis_default_editor/public/components/agg.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg.test.tsx rename to src/plugins/vis_default_editor/public/components/agg.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx b/src/plugins/vis_default_editor/public/components/agg.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx rename to src/plugins/vis_default_editor/public/components/agg.tsx index 83fbf70c9099e..c7e3e609490f9 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx +++ b/src/plugins/vis_default_editor/public/components/agg.tsx @@ -28,14 +28,13 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IAggConfig } from 'src/plugins/data/public'; +import { IAggConfig, TimeRange } from 'src/plugins/data/public'; import { DefaultEditorAggParams } from './agg_params'; import { DefaultEditorAggCommonProps } from './agg_common_props'; import { AGGS_ACTION_KEYS, AggsAction } from './agg_group_state'; import { RowsOrColumnsControl } from './controls/rows_or_columns'; import { RadiusRatioOptionControl } from './controls/radius_ratio_option'; import { getSchemaByName } from '../schemas'; -import { TimeRange } from '../../../../../plugins/data/public'; import { buildAggDescription } from './agg_params_helper'; export interface DefaultEditorAggProps extends DefaultEditorAggCommonProps { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_add.tsx b/src/plugins/vis_default_editor/public/components/agg_add.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_add.tsx rename to src/plugins/vis_default_editor/public/components/agg_add.tsx index 9df4ea58e0f07..f2c2f8b4d0b94 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_add.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_add.tsx @@ -29,7 +29,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { IAggConfig, AggGroupNames } from '../../../../../plugins/data/public'; +import { IAggConfig, AggGroupNames } from '../../../data/public'; import { Schema } from '../schemas'; interface DefaultEditorAggAddProps { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_common_props.ts b/src/plugins/vis_default_editor/public/components/agg_common_props.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_common_props.ts rename to src/plugins/vis_default_editor/public/components/agg_common_props.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.test.tsx b/src/plugins/vis_default_editor/public/components/agg_group.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_group.test.tsx rename to src/plugins/vis_default_editor/public/components/agg_group.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx b/src/plugins/vis_default_editor/public/components/agg_group.tsx similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx rename to src/plugins/vis_default_editor/public/components/agg_group.tsx index 792595fd421f6..ecbc41f28003c 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_group.tsx @@ -30,7 +30,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AggGroupNames, search, IAggConfig } from '../../../../../plugins/data/public'; +import { AggGroupNames, search, IAggConfig, TimeRange } from '../../../data/public'; import { DefaultEditorAgg } from './agg'; import { DefaultEditorAggAdd } from './agg_add'; import { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from './agg_common_props'; @@ -42,7 +42,6 @@ import { } from './agg_group_helper'; import { aggGroupReducer, initAggsState, AGGS_ACTION_KEYS } from './agg_group_state'; import { Schema } from '../schemas'; -import { TimeRange } from '../../../../../plugins/data/public'; export interface DefaultEditorAggGroupProps extends DefaultEditorAggCommonProps { schemas: Schema[]; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.test.ts b/src/plugins/vis_default_editor/public/components/agg_group_helper.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.test.ts rename to src/plugins/vis_default_editor/public/components/agg_group_helper.test.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.tsx b/src/plugins/vis_default_editor/public/components/agg_group_helper.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.tsx rename to src/plugins/vis_default_editor/public/components/agg_group_helper.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_state.tsx b/src/plugins/vis_default_editor/public/components/agg_group_state.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_group_state.tsx rename to src/plugins/vis_default_editor/public/components/agg_group_state.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_param.tsx b/src/plugins/vis_default_editor/public/components/agg_param.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_param.tsx rename to src/plugins/vis_default_editor/public/components/agg_param.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_param_props.ts b/src/plugins/vis_default_editor/public/components/agg_param_props.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_param_props.ts rename to src/plugins/vis_default_editor/public/components/agg_param_props.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.test.tsx b/src/plugins/vis_default_editor/public/components/agg_params.test.tsx similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_params.test.tsx rename to src/plugins/vis_default_editor/public/components/agg_params.test.tsx index 1c49ebf40640e..cac1b0851b92d 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.test.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_params.test.tsx @@ -25,8 +25,8 @@ import { DefaultEditorAggParams as PureDefaultEditorAggParams, DefaultEditorAggParamsProps, } from './agg_params'; -import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; -import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; +import { KibanaContextProvider } from '../../../kibana_react/public'; +import { dataPluginMock } from '../../../data/public/mocks'; import { EditorVisState } from './sidebar/state/reducers'; const mockEditorConfig = { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.tsx b/src/plugins/vis_default_editor/public/components/agg_params.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_params.tsx rename to src/plugins/vis_default_editor/public/components/agg_params.tsx index b1555b76500d0..3674e39b558d2 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_params.tsx @@ -22,7 +22,7 @@ import { EuiForm, EuiAccordion, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import useUnmount from 'react-use/lib/useUnmount'; -import { IAggConfig, IndexPattern, AggGroupNames } from '../../../../../plugins/data/public'; +import { IAggConfig, IndexPattern, AggGroupNames } from '../../../data/public'; import { DefaultEditorAggSelect } from './agg_select'; import { DefaultEditorAggParam } from './agg_param'; @@ -40,7 +40,7 @@ import { import { DefaultEditorCommonProps } from './agg_common_props'; import { EditorParamConfig, TimeIntervalParam, FixedParam, getEditorConfig } from './utils'; import { Schema, getSchemaByName } from '../schemas'; -import { useKibana } from '../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../kibana_react/public'; import { VisDefaultEditorKibanaServices } from '../types'; const FIXED_VALUE_PROP = 'fixedValue'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.test.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.test.ts rename to src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts similarity index 99% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts rename to src/plugins/vis_default_editor/public/components/agg_params_helper.ts index 073cb7d5ac66c..a32bd76bafa5a 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -34,7 +34,7 @@ import { AggParamEditorProps } from './agg_param_props'; import { aggParamsMap } from './agg_params_map'; import { EditorConfig } from './utils'; import { Schema, getSchemaByName } from '../schemas'; -import { search } from '../../../../../plugins/data/public'; +import { search } from '../../../data/public'; import { EditorVisState } from './sidebar/state/reducers'; interface ParamInstanceBase { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_map.ts b/src/plugins/vis_default_editor/public/components/agg_params_map.ts similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_params_map.ts rename to src/plugins/vis_default_editor/public/components/agg_params_map.ts index 4517313b6fd6e..5af3cfc5b0928 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_map.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_map.ts @@ -18,12 +18,7 @@ */ import * as controls from './controls'; -import { - AggGroupNames, - BUCKET_TYPES, - METRIC_TYPES, - search, -} from '../../../../../plugins/data/public'; +import { AggGroupNames, BUCKET_TYPES, METRIC_TYPES, search } from '../../../data/public'; import { wrapWithInlineComp } from './controls/utils'; const { siblingPipelineType, parentPipelineType } = search.aggs; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_state.ts b/src/plugins/vis_default_editor/public/components/agg_params_state.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_params_state.ts rename to src/plugins/vis_default_editor/public/components/agg_params_state.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx b/src/plugins/vis_default_editor/public/components/agg_select.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx rename to src/plugins/vis_default_editor/public/components/agg_select.tsx index 7ee432946f3c8..6cb76b18e24a6 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_select.tsx @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { IAggType, IndexPattern } from 'src/plugins/data/public'; -import { useKibana } from '../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../kibana_react/public'; import { ComboBoxGroupedOptions } from '../utils'; import { AGG_TYPE_ACTION_KEYS, AggTypeAction } from './agg_params_state'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/extended_bounds.test.tsx.snap b/src/plugins/vis_default_editor/public/components/controls/__snapshots__/extended_bounds.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/extended_bounds.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/controls/__snapshots__/extended_bounds.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/metric_agg.test.tsx.snap b/src/plugins/vis_default_editor/public/components/controls/__snapshots__/metric_agg.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/metric_agg.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/controls/__snapshots__/metric_agg.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/size.test.tsx.snap b/src/plugins/vis_default_editor/public/components/controls/__snapshots__/size.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/size.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/controls/__snapshots__/size.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/top_aggregate.test.tsx.snap b/src/plugins/vis_default_editor/public/components/controls/__snapshots__/top_aggregate.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/__snapshots__/top_aggregate.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/controls/__snapshots__/top_aggregate.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_control_props.tsx b/src/plugins/vis_default_editor/public/components/controls/agg_control_props.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_control_props.tsx rename to src/plugins/vis_default_editor/public/components/controls/agg_control_props.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_utils.test.tsx b/src/plugins/vis_default_editor/public/components/controls/agg_utils.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_utils.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/agg_utils.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/auto_precision.tsx b/src/plugins/vis_default_editor/public/components/controls/auto_precision.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/auto_precision.tsx rename to src/plugins/vis_default_editor/public/components/controls/auto_precision.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx similarity index 68% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx index e52b2c85b63fa..b874459a8e7d3 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx @@ -17,11 +17,11 @@ * under the License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiFieldText, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Ipv4Address } from '../../../../../../../plugins/kibana_utils/public'; +import { Ipv4Address } from '../../../../../kibana_utils/public'; import { InputList, InputListConfig, InputModel, InputObject, InputItem } from './input_list'; const EMPTY_STRING = ''; @@ -44,38 +44,42 @@ interface FromToListProps { setValidity(isValid: boolean): void; } -function FromToList({ showValidation, onBlur, ...rest }: FromToListProps) { - const fromToListConfig: InputListConfig = { - defaultValue: { - from: { value: '0.0.0.0', model: '0.0.0.0', isInvalid: false }, - to: { value: '255.255.255.255', model: '255.255.255.255', isInvalid: false }, +const defaultConfig = { + defaultValue: { + from: { value: '0.0.0.0', model: '0.0.0.0', isInvalid: false }, + to: { value: '255.255.255.255', model: '255.255.255.255', isInvalid: false }, + }, + validateClass: Ipv4Address, + getModelValue: (item: FromToObject = {}) => ({ + from: { + value: item.from || EMPTY_STRING, + model: item.from || EMPTY_STRING, + isInvalid: false, }, - validateClass: Ipv4Address, - getModelValue: (item: FromToObject = {}) => ({ - from: { - value: item.from || EMPTY_STRING, - model: item.from || EMPTY_STRING, - isInvalid: false, - }, - to: { value: item.to || EMPTY_STRING, model: item.to || EMPTY_STRING, isInvalid: false }, + to: { value: item.to || EMPTY_STRING, model: item.to || EMPTY_STRING, isInvalid: false }, + }), + getRemoveBtnAriaLabel: (item: FromToModel) => + i18n.translate('visDefaultEditor.controls.ipRanges.removeRangeAriaLabel', { + defaultMessage: 'Remove the range of {from} to {to}', + values: { from: item.from.value || '*', to: item.to.value || '*' }, }), - getRemoveBtnAriaLabel: (item: FromToModel) => - i18n.translate('visDefaultEditor.controls.ipRanges.removeRangeAriaLabel', { - defaultMessage: 'Remove the range of {from} to {to}', - values: { from: item.from.value || '*', to: item.to.value || '*' }, - }), - onChangeFn: ({ from, to }: FromToModel) => { - const result: FromToObject = {}; - if (from.model) { - result.from = from.model; - } - if (to.model) { - result.to = to.model; - } - return result; - }, - hasInvalidValuesFn: ({ from, to }: FromToModel) => from.isInvalid || to.isInvalid, - renderInputRow: (item: FromToModel, index, onChangeValue) => ( + onChangeFn: ({ from, to }: FromToModel) => { + const result: FromToObject = {}; + if (from.model) { + result.from = from.model; + } + if (to.model) { + result.to = to.model; + } + return result; + }, + hasInvalidValuesFn: ({ from, to }: FromToModel) => from.isInvalid || to.isInvalid, + modelNames: ['from', 'to'], +}; + +function FromToList({ showValidation, onBlur, ...rest }: FromToListProps) { + const renderInputRow = useCallback( + (item: FromToModel, index, onChangeValue) => ( <> ), - modelNames: ['from', 'to'], + [onBlur, showValidation] + ); + const fromToListConfig: InputListConfig = { + ...defaultConfig, + renderInputRow, }; return ; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/input_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/input_list.tsx similarity index 75% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/input_list.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/input_list.tsx index cc80d0073c904..639b69cd3d33c 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/input_list.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/components/input_list.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useState, useEffect, Fragment } from 'react'; +import React, { useState, useEffect, Fragment, useCallback } from 'react'; import { isEmpty, isEqual, mapValues, omit, pick } from 'lodash'; import { EuiButtonIcon, @@ -70,7 +70,10 @@ interface InputListProps { } const generateId = htmlIdGenerator(); -const validateValue = (inputValue: string | undefined, config: InputListConfig) => { +const validateValue = ( + inputValue: string | undefined, + InputObject: InputListConfig['validateClass'] +) => { const result = { model: inputValue || '', isInvalid: false, @@ -80,7 +83,6 @@ const validateValue = (inputValue: string | undefined, config: InputListConfig) return result; } try { - const InputObject = config.validateClass; result.model = new InputObject(inputValue).toString(); result.isInvalid = false; return result; @@ -91,47 +93,60 @@ const validateValue = (inputValue: string | undefined, config: InputListConfig) }; function InputList({ config, list, onChange, setValidity }: InputListProps) { + const { defaultValue, getModelValue, modelNames, onChangeFn, validateClass } = config; const [models, setModels] = useState(() => list.map( item => ({ id: generateId(), - ...config.getModelValue(item), + ...getModelValue(item), } as InputModel) ) ); const hasInvalidValues = models.some(config.hasInvalidValuesFn); - const updateValues = (modelList: InputModel[]) => { - setModels(modelList); - onChange(modelList.map(config.onChangeFn)); - }; - const onChangeValue = (index: number, value: string, modelName: string) => { - const { model, isInvalid } = validateValue(value, config); - updateValues( - models.map((range, arrayIndex) => - arrayIndex === index - ? { - ...range, - [modelName]: { - value, - model, - isInvalid, - }, - } - : range - ) - ); - }; - const onDelete = (id: string) => updateValues(models.filter(model => model.id !== id)); - const onAdd = () => - updateValues([ - ...models, - { - id: generateId(), - ...config.getModelValue(), - } as InputModel, - ]); + const updateValues = useCallback( + (modelList: InputModel[]) => { + setModels(modelList); + onChange(modelList.map(onChangeFn)); + }, + [onChangeFn, onChange] + ); + const onChangeValue = useCallback( + (index: number, value: string, modelName: string) => { + const { model, isInvalid } = validateValue(value, validateClass); + updateValues( + models.map((range, arrayIndex) => + arrayIndex === index + ? { + ...range, + [modelName]: { + value, + model, + isInvalid, + }, + } + : range + ) + ); + }, + [models, updateValues, validateClass] + ); + const onDelete = useCallback( + (id: string) => updateValues(models.filter(model => model.id !== id)), + [models, updateValues] + ); + const onAdd = useCallback( + () => + updateValues([ + ...models, + { + id: generateId(), + ...getModelValue(), + } as InputModel, + ]), + [getModelValue, models, updateValues] + ); useEffect(() => { // resposible for setting up an initial value when there is no default value @@ -139,15 +154,15 @@ function InputList({ config, list, onChange, setValidity }: InputListProps) { updateValues([ { id: generateId(), - ...config.defaultValue, + ...defaultValue, } as InputModel, ]); } - }, []); + }, [defaultValue, list.length, updateValues]); useEffect(() => { setValidity(!hasInvalidValues); - }, [hasInvalidValues]); + }, [hasInvalidValues, setValidity]); useEffect(() => { // responsible for discarding changes @@ -155,7 +170,7 @@ function InputList({ config, list, onChange, setValidity }: InputListProps) { list.length !== models.length || list.some((item, index) => { // make model to be the same shape as stored value - const model: InputObject = mapValues(pick(models[index], config.modelNames), 'model'); + const model: InputObject = mapValues(pick(models[index], modelNames), 'model'); // we need to skip empty values since they are not stored in saved object return !isEqual(item, omit(model, isEmpty)); @@ -166,12 +181,12 @@ function InputList({ config, list, onChange, setValidity }: InputListProps) { item => ({ id: generateId(), - ...config.getModelValue(item), + ...getModelValue(item), } as InputModel) ) ); } - }, [list]); + }, [getModelValue, list, modelNames, models]); return ( <> diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/mask_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/mask_list.tsx similarity index 62% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/mask_list.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/mask_list.tsx index f6edecbbcbd70..560213fc08ff0 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/mask_list.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/components/mask_list.tsx @@ -17,12 +17,12 @@ * under the License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiFieldText, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { InputList, InputListConfig, InputObject, InputModel, InputItem } from './input_list'; -import { search } from '../../../../../../../plugins/data/public'; +import { search } from '../../../../../data/public'; const EMPTY_STRING = ''; @@ -42,36 +42,40 @@ interface MaskListProps { setValidity(isValid: boolean): void; } -function MaskList({ showValidation, onBlur, ...rest }: MaskListProps) { - const maskListConfig: InputListConfig = { - defaultValue: { - mask: { model: '0.0.0.0/1', value: '0.0.0.0/1', isInvalid: false }, +const defaultConfig = { + defaultValue: { + mask: { model: '0.0.0.0/1', value: '0.0.0.0/1', isInvalid: false }, + }, + validateClass: search.aggs.CidrMask, + getModelValue: (item: MaskObject = {}) => ({ + mask: { + model: item.mask || EMPTY_STRING, + value: item.mask || EMPTY_STRING, + isInvalid: false, }, - validateClass: search.aggs.CidrMask, - getModelValue: (item: MaskObject = {}) => ({ - mask: { - model: item.mask || EMPTY_STRING, - value: item.mask || EMPTY_STRING, - isInvalid: false, - }, - }), - getRemoveBtnAriaLabel: (item: MaskModel) => - item.mask.value - ? i18n.translate('visDefaultEditor.controls.ipRanges.removeCidrMaskButtonAriaLabel', { - defaultMessage: 'Remove the CIDR mask value of {mask}', - values: { mask: item.mask.value }, - }) - : i18n.translate('visDefaultEditor.controls.ipRanges.removeEmptyCidrMaskButtonAriaLabel', { - defaultMessage: 'Remove the CIDR mask default value', - }), - onChangeFn: ({ mask }: MaskModel) => { - if (mask.model) { - return { mask: mask.model }; - } - return {}; - }, - hasInvalidValuesFn: ({ mask }) => mask.isInvalid, - renderInputRow: ({ mask }: MaskModel, index, onChangeValue) => ( + }), + getRemoveBtnAriaLabel: (item: MaskModel) => + item.mask.value + ? i18n.translate('visDefaultEditor.controls.ipRanges.removeCidrMaskButtonAriaLabel', { + defaultMessage: 'Remove the CIDR mask value of {mask}', + values: { mask: item.mask.value }, + }) + : i18n.translate('visDefaultEditor.controls.ipRanges.removeEmptyCidrMaskButtonAriaLabel', { + defaultMessage: 'Remove the CIDR mask default value', + }), + onChangeFn: ({ mask }: MaskModel) => { + if (mask.model) { + return { mask: mask.model }; + } + return {}; + }, + hasInvalidValuesFn: ({ mask }: MaskModel) => mask.isInvalid, + modelNames: 'mask', +}; + +function MaskList({ showValidation, onBlur, ...rest }: MaskListProps) { + const renderInputRow = useCallback( + ({ mask }: MaskModel, index, onChangeValue) => ( ), - modelNames: 'mask', + [onBlur, showValidation] + ); + const maskListConfig: InputListConfig = { + ...defaultConfig, + renderInputRow, }; return ; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_list.test.tsx.snap b/src/plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_list.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_list.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_list.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_row.test.tsx.snap b/src/plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_row.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_row.test.tsx.snap rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/__snapshots__/number_row.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/index.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/index.ts rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/index.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx b/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx index a43c66c2e08cc..b4683e47979ce 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx @@ -79,7 +79,7 @@ function NumberList({ if (!numberArray.length) { onChange([models[0].value as number]); } - }, []); + }, [models, numberArray.length, onChange]); const isValid = !hasInvalidValues(models); useValidation(setValidity, isValid); @@ -109,7 +109,7 @@ function NumberList({ }) ); }, - [numberRange, models, onUpdate] + [models, onUpdate] ); // Add an item to the end of the list diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx b/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/number_row.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_row.tsx b/src/plugins/vis_default_editor/public/components/controls/components/number_list/number_row.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_row.tsx rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/number_row.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/range.test.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/range.test.ts similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/range.test.ts rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/range.test.ts index e9090e5b38ef7..a19034d3c8e92 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/range.test.ts +++ b/src/plugins/vis_default_editor/public/components/controls/components/number_list/range.test.ts @@ -108,7 +108,6 @@ describe('Range parsing utility', () => { }; forOwn(tests, (spec, str: any) => { - // eslint-disable-next-line jest/valid-describe describe(str, () => { const range = parseRange(str); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/range.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/range.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/range.ts rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/range.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts rename to src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx b/src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx similarity index 95% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx index 6b1a4dca7b84f..b844fdfb82256 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx @@ -20,8 +20,8 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DateRangesParamEditor } from './date_ranges'; -import { KibanaContextProvider } from '../../../../../../plugins/kibana_react/public'; -import { docLinksServiceMock } from '../../../../../../core/public/mocks'; +import { KibanaContextProvider } from '../../../../kibana_react/public'; +import { docLinksServiceMock } from '../../../../../core/public/mocks'; describe('DateRangesParamEditor component', () => { let setValue: jest.Mock; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx b/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx similarity index 90% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx rename to src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx index 15e864bfd026d..57f4c7d04019b 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useState, useEffect, useCallback } from 'react'; import { htmlIdGenerator, EuiButtonIcon, @@ -36,8 +36,9 @@ import dateMath from '@elastic/datemath'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { isEqual, omit } from 'lodash'; +import { useMount } from 'react-use'; -import { useKibana } from '../../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../../kibana_react/public'; import { AggParamEditorProps } from '../agg_param_props'; const FROM_PLACEHOLDER = '\u2212\u221E'; @@ -72,12 +73,26 @@ function DateRangesParamEditor({ ({ from, to }) => (!from && !to) || !validateDateMath(from) || !validateDateMath(to) ); - // set up an initial range when there is no default range - useEffect(() => { + const updateRanges = useCallback( + (rangeValues: DateRangeValuesModel[]) => { + // do not set internal id parameter into saved object + setValue(rangeValues.map(range => omit(range, 'id'))); + setRanges(rangeValues); + }, + [setValue] + ); + + const onAddRange = useCallback(() => updateRanges([...ranges, { id: generateId() }]), [ + ranges, + updateRanges, + ]); + + useMount(() => { + // set up an initial range when there is no default range if (!value.length) { onAddRange(); } - }, []); + }); useEffect(() => { // responsible for discarding changes @@ -87,18 +102,12 @@ function DateRangesParamEditor({ ) { setRanges(value.map(range => ({ ...range, id: generateId() }))); } - }, [value]); + }, [ranges, value]); useEffect(() => { setValidity(!hasInvalidRange); - }, [hasInvalidRange]); - - const updateRanges = (rangeValues: DateRangeValuesModel[]) => { - // do not set internal id parameter into saved object - setValue(rangeValues.map(range => omit(range, 'id'))); - setRanges(rangeValues); - }; - const onAddRange = () => updateRanges([...ranges, { id: generateId() }]); + }, [hasInvalidRange, setValidity]); + const onRemoveRange = (id: string) => updateRanges(ranges.filter(range => range.id !== id)); const onChangeRange = (id: string, key: string, newValue: string) => updateRanges( diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/drop_partials.tsx b/src/plugins/vis_default_editor/public/components/controls/drop_partials.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/drop_partials.tsx rename to src/plugins/vis_default_editor/public/components/controls/drop_partials.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/extended_bounds.test.tsx b/src/plugins/vis_default_editor/public/components/controls/extended_bounds.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/extended_bounds.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/extended_bounds.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/extended_bounds.tsx b/src/plugins/vis_default_editor/public/components/controls/extended_bounds.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/extended_bounds.tsx rename to src/plugins/vis_default_editor/public/components/controls/extended_bounds.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.test.tsx b/src/plugins/vis_default_editor/public/components/controls/field.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/field.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/field.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx b/src/plugins/vis_default_editor/public/components/controls/field.tsx similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx rename to src/plugins/vis_default_editor/public/components/controls/field.tsx index fc1cbb51b70a7..42086004a12dc 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/field.tsx @@ -18,7 +18,8 @@ */ import { get } from 'lodash'; -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useState, useCallback } from 'react'; +import { useMount } from 'react-use'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -84,8 +85,7 @@ function FieldParamEditor({ const showErrorMessage = (showValidation || !indexedFields.length) && !isValid; useValidation(setValidity, isValid); - - useEffect(() => { + useMount(() => { // set field if only one available if (indexedFields.length !== 1) { return; @@ -98,7 +98,7 @@ function FieldParamEditor({ } else if (indexedField.options.length === 1) { setValue(indexedField.options[0].target); } - }, []); + }); const onSearchChange = useCallback(searchValue => setIsDirty(Boolean(searchValue)), []); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/filter.tsx b/src/plugins/vis_default_editor/public/components/controls/filter.tsx similarity index 99% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/filter.tsx rename to src/plugins/vis_default_editor/public/components/controls/filter.tsx index e2e7c2895093e..16aaff47f49c3 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/filter.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/filter.tsx @@ -21,7 +21,7 @@ import React, { useState } from 'react'; import { EuiForm, EuiButtonIcon, EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IAggConfig, Query, QueryStringInput } from '../../../../../../plugins/data/public'; +import { IAggConfig, Query, QueryStringInput } from '../../../../data/public'; interface FilterRowProps { id: string; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/filters.tsx b/src/plugins/vis_default_editor/public/components/controls/filters.tsx similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/filters.tsx rename to src/plugins/vis_default_editor/public/components/controls/filters.tsx index be4c62ab08aa2..d4e698f655c5e 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/filters.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/filters.tsx @@ -21,9 +21,10 @@ import React, { useState, useEffect } from 'react'; import { omit, isEqual } from 'lodash'; import { htmlIdGenerator, EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useMount } from 'react-use'; import { Query } from 'src/plugins/data/public'; -import { useKibana } from '../../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../../kibana_react/public'; import { FilterRow } from './filter'; import { AggParamEditorProps } from '../agg_param_props'; @@ -40,10 +41,10 @@ function FiltersParamEditor({ agg, value = [], setValue }: AggParamEditorProps ({ ...filter, id: generateId() })) ); - useEffect(() => { + useMount(() => { // set parsed values into model after initialization setValue(filters.map(filter => omit({ ...filter, input: filter.input }, 'id'))); - }, []); + }); useEffect(() => { // responsible for discarding changes @@ -53,7 +54,7 @@ function FiltersParamEditor({ agg, value = [], setValue }: AggParamEditorProps ({ ...filter, id: generateId() }))); } - }, [value]); + }, [filters, value]); const updateFilters = (updatedFilters: FilterValue[]) => { // do not set internal id parameter into saved object diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx b/src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx similarity index 73% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx rename to src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx index 90b7cb03b7a5b..a316a087c8bcb 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx @@ -17,22 +17,32 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { i18n } from '@kbn/i18n'; -import { search } from '../../../../../../plugins/data/public'; +import { search } from '../../../../data/public'; import { SwitchParamEditor } from './switch'; import { AggParamEditorProps } from '../agg_param_props'; const { isType } = search.aggs; function HasExtendedBoundsParamEditor(props: AggParamEditorProps) { + const { agg, setValue, value } = props; + const minDocCount = useRef(agg.params.min_doc_count); + useEffect(() => { - props.setValue(props.value && props.agg.params.min_doc_count); - }, [props.agg.params.min_doc_count]); + if (minDocCount.current !== agg.params.min_doc_count) { + // The "Extend bounds" param is only enabled when "Show empty buckets" is turned on. + // So if "Show empty buckets" is changed, "Extend bounds" should reflect changes + minDocCount.current = agg.params.min_doc_count; + + setValue(value && agg.params.min_doc_count); + } + }, [agg.params.min_doc_count, setValue, value]); return ( ) { !props.agg.params.min_doc_count || !(isType('number')(props.agg) || isType('date')(props.agg)) } - {...props} /> ); } diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/index.ts b/src/plugins/vis_default_editor/public/components/controls/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/index.ts rename to src/plugins/vis_default_editor/public/components/controls/index.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/ip_range_type.tsx b/src/plugins/vis_default_editor/public/components/controls/ip_range_type.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/ip_range_type.tsx rename to src/plugins/vis_default_editor/public/components/controls/ip_range_type.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/ip_ranges.tsx b/src/plugins/vis_default_editor/public/components/controls/ip_ranges.tsx similarity index 79% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/ip_ranges.tsx rename to src/plugins/vis_default_editor/public/components/controls/ip_ranges.tsx index c4b90649aaaae..5ffa8088a9546 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/ip_ranges.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/ip_ranges.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { FromToList, FromToObject } from './components/from_to_list'; @@ -38,12 +38,22 @@ function IpRangesParamEditor({ setValidity, showValidation, }: AggParamEditorProps) { - const handleChange = (modelName: IpRangeTypes, items: Array) => { - setValue({ - ...value, - [modelName]: items, - }); - }; + const handleMaskListChange = useCallback( + (items: MaskObject[]) => + setValue({ + ...value, + [IpRangeTypes.MASK]: items, + }), + [setValue, value] + ); + const handleFromToListChange = useCallback( + (items: FromToObject[]) => + setValue({ + ...value, + [IpRangeTypes.FROM_TO]: items, + }), + [setValue, value] + ); return ( @@ -52,7 +62,7 @@ function IpRangesParamEditor({ list={value.mask} showValidation={showValidation} onBlur={setTouched} - onChange={items => handleChange(IpRangeTypes.MASK, items)} + onChange={handleMaskListChange} setValidity={setValidity} /> ) : ( @@ -60,7 +70,7 @@ function IpRangesParamEditor({ list={value.fromTo} showValidation={showValidation} onBlur={setTouched} - onChange={items => handleChange(IpRangeTypes.FROM_TO, items)} + onChange={handleFromToListChange} setValidity={setValidity} /> )} diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/is_filtered_by_collar.tsx b/src/plugins/vis_default_editor/public/components/controls/is_filtered_by_collar.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/is_filtered_by_collar.tsx rename to src/plugins/vis_default_editor/public/components/controls/is_filtered_by_collar.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/metric_agg.test.tsx b/src/plugins/vis_default_editor/public/components/controls/metric_agg.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/metric_agg.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/metric_agg.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/metric_agg.tsx b/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/metric_agg.tsx rename to src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/min_doc_count.tsx b/src/plugins/vis_default_editor/public/components/controls/min_doc_count.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/min_doc_count.tsx rename to src/plugins/vis_default_editor/public/components/controls/min_doc_count.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/missing_bucket.tsx b/src/plugins/vis_default_editor/public/components/controls/missing_bucket.tsx similarity index 93% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/missing_bucket.tsx rename to src/plugins/vis_default_editor/public/components/controls/missing_bucket.tsx index 7010f0d53e569..7d4d2230bd766 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/missing_bucket.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/missing_bucket.tsx @@ -21,20 +21,22 @@ import React, { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { SwitchParamEditor } from './switch'; -import { search } from '../../../../../../plugins/data/public'; +import { search } from '../../../../data/public'; import { AggParamEditorProps } from '../agg_param_props'; function MissingBucketParamEditor(props: AggParamEditorProps) { const fieldTypeIsNotString = !search.aggs.isStringType(props.agg); + const { setValue } = props; useEffect(() => { if (fieldTypeIsNotString) { - props.setValue(false); + setValue(false); } - }, [fieldTypeIsNotString]); + }, [fieldTypeIsNotString, setValue]); return ( ) { } )} disabled={fieldTypeIsNotString} - {...props} /> ); } diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/number_interval.tsx b/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx similarity index 99% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/number_interval.tsx rename to src/plugins/vis_default_editor/public/components/controls/number_interval.tsx index 6ab5ee2d260a1..02bf680734526 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/number_interval.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx @@ -61,7 +61,7 @@ function NumberIntervalParamEditor({ useEffect(() => { setValidity(isValid); - }, [isValid]); + }, [isValid, setValidity]); const onChange = useCallback( ({ target }: React.ChangeEvent) => diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order.tsx b/src/plugins/vis_default_editor/public/components/controls/order.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/order.tsx rename to src/plugins/vis_default_editor/public/components/controls/order.tsx index 8f63662d928c1..e609bf9adf790 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order.tsx @@ -39,7 +39,7 @@ function OrderParamEditor({ useEffect(() => { setValidity(isValid); - }, [isValid]); + }, [isValid, setValidity]); // @ts-ignore return ( diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.test.tsx b/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.tsx b/src/plugins/vis_default_editor/public/components/controls/order_agg.tsx similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.tsx rename to src/plugins/vis_default_editor/public/components/controls/order_agg.tsx index 41672bc192fab..c5a35cbbd7ab1 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order_agg.tsx @@ -20,7 +20,7 @@ import React, { useEffect } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { AggParamType, IAggConfig, AggGroupNames } from '../../../../../../plugins/data/public'; +import { AggParamType, IAggConfig, AggGroupNames } from '../../../../data/public'; import { useSubAggParamsHandlers } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; import { DefaultEditorAggParams } from '../agg_params'; @@ -47,7 +47,7 @@ function OrderAggParamEditor({ if (orderBy !== 'custom' && value) { setValue(undefined); } - }, [orderBy]); + }, [agg, aggParam, orderBy, setValue, value]); const { onAggTypeChange, setAggParamValue } = useSubAggParamsHandlers( agg, diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_by.tsx b/src/plugins/vis_default_editor/public/components/controls/order_by.tsx similarity index 94% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/order_by.tsx rename to src/plugins/vis_default_editor/public/components/controls/order_by.tsx index 9f1aaa54a8ca3..47b12f4340d42 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_by.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order_by.tsx @@ -17,9 +17,10 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useMount } from 'react-use'; import { isCompatibleAggregation, @@ -28,7 +29,7 @@ import { useValidation, } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; -import { search } from '../../../../../../plugins/data/public'; +import { search } from '../../../../data/public'; const { termsAggFilter } = search.aggs; const DEFAULT_VALUE = '_key'; @@ -58,8 +59,7 @@ function OrderByParamEditor({ const isValid = !!value; useValidation(setValidity, isValid); - - useEffect(() => { + useMount(() => { // setup the initial value of orderBy if (!value) { let respAgg = { id: DEFAULT_VALUE }; @@ -70,7 +70,7 @@ function OrderByParamEditor({ setValue(respAgg.id); } - }, []); + }); useFallbackMetric(setValue, termsAggFilter, metricAggs, value, DEFAULT_VALUE); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/other_bucket.tsx b/src/plugins/vis_default_editor/public/components/controls/other_bucket.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/other_bucket.tsx rename to src/plugins/vis_default_editor/public/components/controls/other_bucket.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx b/src/plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx rename to src/plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx b/src/plugins/vis_default_editor/public/components/controls/percentiles.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/percentiles.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.tsx b/src/plugins/vis_default_editor/public/components/controls/percentiles.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.tsx rename to src/plugins/vis_default_editor/public/components/controls/percentiles.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/precision.tsx b/src/plugins/vis_default_editor/public/components/controls/precision.tsx similarity index 95% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/precision.tsx rename to src/plugins/vis_default_editor/public/components/controls/precision.tsx index df71e72ee6c61..ad2011513b171 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/precision.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/precision.tsx @@ -22,7 +22,7 @@ import React from 'react'; import { EuiRange, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../../kibana_react/public'; import { AggParamEditorProps } from '../agg_param_props'; function PrecisionParamEditor({ agg, value, setValue }: AggParamEditorProps) { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx b/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx similarity index 95% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx rename to src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx index c64b079e4f802..86c4431b6d5ed 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx @@ -17,10 +17,11 @@ * under the License. */ -import React, { useEffect, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { EuiFormRow, EuiIconTip, EuiRange, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useMount } from 'react-use'; import { AggControlProps } from './agg_control_props'; @@ -44,11 +45,11 @@ function RadiusRatioOptionControl({ editorStateParams, setStateParamValue }: Agg ); - useEffect(() => { + useMount(() => { if (!editorStateParams.radiusRatio) { setStateParamValue(PARAM_NAME, DEFAULT_VALUE); } - }, []); + }); const onChange = useCallback( (e: React.ChangeEvent | React.MouseEvent) => diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/range_control.tsx b/src/plugins/vis_default_editor/public/components/controls/range_control.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/range_control.tsx rename to src/plugins/vis_default_editor/public/components/controls/range_control.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/ranges.tsx b/src/plugins/vis_default_editor/public/components/controls/ranges.tsx similarity index 91% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/ranges.tsx rename to src/plugins/vis_default_editor/public/components/controls/ranges.tsx index 27de9dfe68ee0..5c6438b400408 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/ranges.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/ranges.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useCallback, useState, useEffect } from 'react'; import { htmlIdGenerator, EuiButtonIcon, @@ -76,13 +76,44 @@ function RangesParamEditor({ validateRange, }: RangesParamEditorProps) { const [ranges, setRanges] = useState(() => value.map(range => ({ ...range, id: generateId() }))); + const updateRanges = useCallback( + (rangeValues: RangeValuesModel[]) => { + // do not set internal id parameter into saved object + setValue(rangeValues.map(range => omit(range, 'id'))); + setRanges(rangeValues); + + if (setTouched) { + setTouched(true); + } + }, + [setTouched, setValue] + ); + const onAddRange = useCallback( + () => + addRangeValues + ? updateRanges([...ranges, { ...addRangeValues(), id: generateId() }]) + : updateRanges([...ranges, { id: generateId() }]), + [addRangeValues, ranges, updateRanges] + ); + const onRemoveRange = (id: string) => updateRanges(ranges.filter(range => range.id !== id)); + const onChangeRange = (id: string, key: string, newValue: string) => + updateRanges( + ranges.map(range => + range.id === id + ? { + ...range, + [key]: newValue === '' ? undefined : parseFloat(newValue), + } + : range + ) + ); // set up an initial range when there is no default range useEffect(() => { if (!value.length) { onAddRange(); } - }, []); + }, [onAddRange, value.length]); useEffect(() => { // responsible for discarding changes @@ -92,33 +123,7 @@ function RangesParamEditor({ ) { setRanges(value.map(range => ({ ...range, id: generateId() }))); } - }, [value]); - - const updateRanges = (rangeValues: RangeValuesModel[]) => { - // do not set internal id parameter into saved object - setValue(rangeValues.map(range => omit(range, 'id'))); - setRanges(rangeValues); - - if (setTouched) { - setTouched(true); - } - }; - const onAddRange = () => - addRangeValues - ? updateRanges([...ranges, { ...addRangeValues(), id: generateId() }]) - : updateRanges([...ranges, { id: generateId() }]); - const onRemoveRange = (id: string) => updateRanges(ranges.filter(range => range.id !== id)); - const onChangeRange = (id: string, key: string, newValue: string) => - updateRanges( - ranges.map(range => - range.id === id - ? { - ...range, - [key]: newValue === '' ? undefined : parseFloat(newValue), - } - : range - ) - ); + }, [ranges, value]); const hasInvalidRange = validateRange && diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/raw_json.tsx b/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/raw_json.tsx rename to src/plugins/vis_default_editor/public/components/controls/raw_json.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/rows_or_columns.tsx b/src/plugins/vis_default_editor/public/components/controls/rows_or_columns.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/rows_or_columns.tsx rename to src/plugins/vis_default_editor/public/components/controls/rows_or_columns.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/scale_metrics.tsx b/src/plugins/vis_default_editor/public/components/controls/scale_metrics.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/scale_metrics.tsx rename to src/plugins/vis_default_editor/public/components/controls/scale_metrics.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/size.test.tsx b/src/plugins/vis_default_editor/public/components/controls/size.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/size.test.tsx rename to src/plugins/vis_default_editor/public/components/controls/size.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/size.tsx b/src/plugins/vis_default_editor/public/components/controls/size.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/size.tsx rename to src/plugins/vis_default_editor/public/components/controls/size.tsx index 824ec4aeb253c..9f55eb9212dee 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/size.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/size.tsx @@ -48,7 +48,7 @@ function SizeParamEditor({ useEffect(() => { setValidity(isValid); - }, [isValid]); + }, [isValid, setValidity]); return ( { setValidity(isValid); - }, [isValid]); + }, [isValid, setValidity]); const onChange = useCallback(ev => setValue(ev.target.value), [setValue]); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_agg.tsx b/src/plugins/vis_default_editor/public/components/controls/sub_agg.tsx similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_agg.tsx rename to src/plugins/vis_default_editor/public/components/controls/sub_agg.tsx index c9f53a68b3e83..ee0fbd8532ce9 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_agg.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/sub_agg.tsx @@ -20,7 +20,7 @@ import React, { useEffect } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { AggParamType, IAggConfig, AggGroupNames } from '../../../../../../plugins/data/public'; +import { AggParamType, IAggConfig, AggGroupNames } from '../../../../data/public'; import { useSubAggParamsHandlers } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; import { DefaultEditorAggParams } from '../agg_params'; @@ -29,7 +29,6 @@ function SubAggParamEditor({ agg, aggParam, formIsTouched, - value, metricAggs, state, setValue, @@ -44,7 +43,7 @@ function SubAggParamEditor({ } else if (!agg.params.customMetric) { setValue(aggParam.makeAgg(agg)); } - }, [value, metricAggs]); + }, [metricAggs, agg, setValue, aggParam]); const { onAggTypeChange, setAggParamValue } = useSubAggParamsHandlers( agg, diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_metric.tsx b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_metric.tsx rename to src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx index ead3f8bb00623..361eeba9abdbf 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_metric.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx @@ -17,11 +17,12 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { EuiFormLabel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useMount } from 'react-use'; -import { AggParamType, IAggConfig, AggGroupNames } from '../../../../../../plugins/data/public'; +import { AggParamType, IAggConfig, AggGroupNames } from '../../../../data/public'; import { useSubAggParamsHandlers } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; import { DefaultEditorAggParams } from '../agg_params'; @@ -48,13 +49,13 @@ function SubMetricParamEditor({ const aggTitle = type === 'customMetric' ? metricTitle : bucketTitle; const aggGroup = type === 'customMetric' ? AggGroupNames.Metrics : AggGroupNames.Buckets; - useEffect(() => { + useMount(() => { if (agg.params[type]) { setValue(agg.params[type]); } else { setValue(aggParam.makeAgg(agg)); } - }, []); + }); const { onAggTypeChange, setAggParamValue } = useSubAggParamsHandlers( agg, diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/switch.tsx b/src/plugins/vis_default_editor/public/components/controls/switch.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/switch.tsx rename to src/plugins/vis_default_editor/public/components/controls/switch.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/test_utils.ts b/src/plugins/vis_default_editor/public/components/controls/test_utils.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/test_utils.ts rename to src/plugins/vis_default_editor/public/components/controls/test_utils.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx b/src/plugins/vis_default_editor/public/components/controls/time_interval.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx rename to src/plugins/vis_default_editor/public/components/controls/time_interval.tsx index 971a62faf7d7c..4af41f67bc524 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/time_interval.tsx @@ -23,7 +23,7 @@ import { EuiFormRow, EuiIconTip, EuiComboBox, EuiComboBoxOptionOption } from '@e import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { search, AggParamOption } from '../../../../../../plugins/data/public'; +import { search, AggParamOption } from '../../../../data/public'; import { AggParamEditorProps } from '../agg_param_props'; const { parseEsInterval, InvalidEsCalendarIntervalError } = search.aggs; @@ -161,7 +161,7 @@ function TimeIntervalParamEditor({ useEffect(() => { setValidity(isValid); - }, [isValid]); + }, [isValid, setValidity]); return ( { setValidity(isValid); - }, [isValid]); + }, [isValid, setValidity]); useEffect(() => { if (isFirstRun.current) { @@ -102,7 +102,7 @@ export function TopAggregateParamEditor({ if (filteredOptions.length === 1) { setValue(aggParam.options.find(opt => opt.value === filteredOptions[0].value)); } - }, [fieldType]); + }, [aggParam.options, fieldType, filteredOptions, setValue, value]); const handleChange = (event: React.ChangeEvent) => { if (event.target.value === emptyValue.value) { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_field.tsx b/src/plugins/vis_default_editor/public/components/controls/top_field.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/top_field.tsx rename to src/plugins/vis_default_editor/public/components/controls/top_field.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_size.tsx b/src/plugins/vis_default_editor/public/components/controls/top_size.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/top_size.tsx rename to src/plugins/vis_default_editor/public/components/controls/top_size.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_sort_field.tsx b/src/plugins/vis_default_editor/public/components/controls/top_sort_field.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/top_sort_field.tsx rename to src/plugins/vis_default_editor/public/components/controls/top_sort_field.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/use_geocentroid.tsx b/src/plugins/vis_default_editor/public/components/controls/use_geocentroid.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/use_geocentroid.tsx rename to src/plugins/vis_default_editor/public/components/controls/use_geocentroid.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts b/src/plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/index.ts b/src/plugins/vis_default_editor/public/components/controls/utils/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/index.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/index.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/inline_comp_wrapper.tsx b/src/plugins/vis_default_editor/public/components/controls/utils/inline_comp_wrapper.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/inline_comp_wrapper.tsx rename to src/plugins/vis_default_editor/public/components/controls/utils/inline_comp_wrapper.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.test.ts b/src/plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.test.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.test.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.ts b/src/plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/strings/comma_separated_list.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/index.ts b/src/plugins/vis_default_editor/public/components/controls/utils/strings/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/index.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/strings/index.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/prose.test.ts b/src/plugins/vis_default_editor/public/components/controls/utils/strings/prose.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/prose.test.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/strings/prose.test.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/prose.ts b/src/plugins/vis_default_editor/public/components/controls/utils/strings/prose.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/strings/prose.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/strings/prose.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts b/src/plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts rename to src/plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/controls.tsx b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx similarity index 98% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/controls.tsx rename to src/plugins/vis_default_editor/public/components/sidebar/controls.tsx index 18b445b4a26db..db9d7d9e3316a 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/controls.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx @@ -30,7 +30,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useDebounce } from 'react-use'; -import { Vis } from '../../../../../../plugins/visualizations/public'; +import { Vis } from 'src/plugins/visualizations/public'; import { discardChanges, EditorAction } from './state'; interface DefaultEditorControlsProps { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/data_tab.tsx b/src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/data_tab.tsx rename to src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx index 0c967723db8e7..0466c64541e23 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/data_tab.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx @@ -26,7 +26,8 @@ import { IAggConfig, IMetricAggType, search, -} from '../../../../../../plugins/data/public'; + TimeRange, +} from '../../../../data/public'; import { DefaultEditorAggGroup } from '../agg_group'; import { EditorAction, @@ -39,7 +40,6 @@ import { } from './state'; import { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from '../agg_common_props'; import { ISchemas } from '../../schemas'; -import { TimeRange } from '../../../../../../plugins/data/public'; import { EditorVisState } from './state/reducers'; export interface DefaultEditorDataTabProps { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/index.ts b/src/plugins/vis_default_editor/public/components/sidebar/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/index.ts rename to src/plugins/vis_default_editor/public/components/sidebar/index.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/navbar.tsx b/src/plugins/vis_default_editor/public/components/sidebar/navbar.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/navbar.tsx rename to src/plugins/vis_default_editor/public/components/sidebar/navbar.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx rename to src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx index b24486a12fd24..9dfeae1815d1a 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx @@ -23,16 +23,15 @@ import { i18n } from '@kbn/i18n'; import { keyCodes, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EventEmitter } from 'events'; -import { Vis } from 'src/plugins/visualizations/public'; +import { Vis, PersistedState } from 'src/plugins/visualizations/public'; +import { SavedSearch } from 'src/plugins/discover/public'; +import { TimeRange } from 'src/plugins/data/public'; import { DefaultEditorNavBar, OptionTab } from './navbar'; import { DefaultEditorControls } from './controls'; import { setStateParamValue, useEditorReducer, useEditorFormState, discardChanges } from './state'; import { DefaultEditorAggCommonProps } from '../agg_common_props'; import { SidebarTitle } from './sidebar_title'; -import { PersistedState } from '../../../../../../plugins/visualizations/public'; -import { SavedSearch } from '../../../../../../plugins/discover/public'; import { Schema } from '../../schemas'; -import { TimeRange } from '../../../../../../plugins/data/public'; interface DefaultEditorSideBarProps { isCollapsed: boolean; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx rename to src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx index fb63a598a4fae..c9f83e5b474a6 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx @@ -35,8 +35,8 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { Vis } from '../../../../../../plugins/visualizations/public'; -import { SavedSearch } from '../../../../../../plugins/discover/public'; +import { Vis } from 'src/plugins/visualizations/public'; +import { SavedSearch } from 'src/plugins/discover/public'; interface LinkedSearchProps { savedSearch: SavedSearch; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/actions.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/actions.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/actions.ts rename to src/plugins/vis_default_editor/public/components/sidebar/state/actions.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/constants.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/constants.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/constants.ts rename to src/plugins/vis_default_editor/public/components/sidebar/state/constants.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/editor_form_state.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/editor_form_state.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/editor_form_state.ts rename to src/plugins/vis_default_editor/public/components/sidebar/state/editor_form_state.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/index.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/index.ts similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/index.ts rename to src/plugins/vis_default_editor/public/components/sidebar/state/index.ts index d39d6d07b32d2..1bfa100cbd0f7 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/index.ts +++ b/src/plugins/vis_default_editor/public/components/sidebar/state/index.ts @@ -24,7 +24,7 @@ import { Vis } from 'src/plugins/visualizations/public'; import { createEditorStateReducer, initEditorState, EditorVisState } from './reducers'; import { EditorStateActionTypes } from './constants'; import { EditorAction } from './actions'; -import { useKibana } from '../../../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../../../kibana_react/public'; import { VisDefaultEditorKibanaServices } from '../../../types'; export * from './editor_form_state'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts similarity index 99% rename from src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts rename to src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts index b9f89cebd8bf3..4e7a2904584da 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts +++ b/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts @@ -20,7 +20,7 @@ import { cloneDeep } from 'lodash'; import { Vis } from 'src/plugins/visualizations/public'; -import { AggGroupNames, DataPublicPluginStart } from '../../../../../../../plugins/data/public'; +import { AggGroupNames, DataPublicPluginStart } from '../../../../../data/public'; import { EditorStateActionTypes } from './constants'; import { getEnabledMetricAggsCount } from '../../agg_group_helper'; import { EditorAction } from './actions'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/utils/editor_config.ts b/src/plugins/vis_default_editor/public/components/utils/editor_config.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/utils/editor_config.ts rename to src/plugins/vis_default_editor/public/components/utils/editor_config.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/utils/index.ts b/src/plugins/vis_default_editor/public/components/utils/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/components/utils/index.ts rename to src/plugins/vis_default_editor/public/components/utils/index.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx similarity index 94% rename from src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx rename to src/plugins/vis_default_editor/public/default_editor.tsx index 899b9c1b5fd6e..f1963b94dcf95 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx +++ b/src/plugins/vis_default_editor/public/default_editor.tsx @@ -19,8 +19,8 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; -import { EditorRenderProps } from '../../kibana/public/visualize/np_ready/types'; -import { PanelsContainer, Panel } from '../../../../plugins/kibana_react/public'; +import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types'; +import { PanelsContainer, Panel } from '../../kibana_react/public'; import './vis_type_agg_filter'; import { DefaultEditorSideBar } from './components/sidebar'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx similarity index 92% rename from src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx rename to src/plugins/vis_default_editor/public/default_editor_controller.tsx index 58e67b5064da5..798da09f8e30b 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -23,9 +23,9 @@ import { i18n } from '@kbn/i18n'; import { EventEmitter } from 'events'; import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types'; -import { Vis, VisualizeEmbeddableContract } from '../../../../plugins/visualizations/public'; -import { Storage } from '../../../../plugins/kibana_utils/public'; -import { KibanaContextProvider } from '../../../../plugins/kibana_react/public'; +import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; +import { Storage } from '../../kibana_utils/public'; +import { KibanaContextProvider } from '../../kibana_react/public'; import { DefaultEditor } from './default_editor'; import { DefaultEditorDataTab, OptionTab } from './components/sidebar'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/editor_size.ts b/src/plugins/vis_default_editor/public/editor_size.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/editor_size.ts rename to src/plugins/vis_default_editor/public/editor_size.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/index.scss b/src/plugins/vis_default_editor/public/index.scss similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/index.scss rename to src/plugins/vis_default_editor/public/index.scss diff --git a/src/legacy/core_plugins/vis_default_editor/public/index.ts b/src/plugins/vis_default_editor/public/index.ts similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/index.ts rename to src/plugins/vis_default_editor/public/index.ts index 156d50f451b57..6c58c6df26b00 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/index.ts +++ b/src/plugins/vis_default_editor/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + export { DefaultEditorController } from './default_editor_controller'; export { useValidation } from './components/controls/utils'; export { RangesParamEditor, RangeValues } from './components/controls/ranges'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/schemas.ts b/src/plugins/vis_default_editor/public/schemas.ts similarity index 96% rename from src/legacy/core_plugins/vis_default_editor/public/schemas.ts rename to src/plugins/vis_default_editor/public/schemas.ts index 4e632da44afc0..05ba5fa9c9419 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/schemas.ts +++ b/src/plugins/vis_default_editor/public/schemas.ts @@ -21,7 +21,7 @@ import _, { defaults } from 'lodash'; import { Optional } from '@kbn/utility-types'; -import { AggGroupNames, AggParam, IAggGroupNames } from '../../../../plugins/data/public'; +import { AggGroupNames, AggParam, IAggGroupNames } from '../../data/public'; export interface ISchemas { [AggGroupNames.Buckets]: Schema[]; diff --git a/src/legacy/core_plugins/vis_default_editor/public/types.ts b/src/plugins/vis_default_editor/public/types.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/types.ts rename to src/plugins/vis_default_editor/public/types.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/utils.test.ts b/src/plugins/vis_default_editor/public/utils.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/utils.test.ts rename to src/plugins/vis_default_editor/public/utils.test.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/utils.ts b/src/plugins/vis_default_editor/public/utils.ts similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/utils.ts rename to src/plugins/vis_default_editor/public/utils.ts diff --git a/src/legacy/core_plugins/vis_default_editor/public/vis_options_props.tsx b/src/plugins/vis_default_editor/public/vis_options_props.tsx similarity index 100% rename from src/legacy/core_plugins/vis_default_editor/public/vis_options_props.tsx rename to src/plugins/vis_default_editor/public/vis_options_props.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/vis_type_agg_filter.ts b/src/plugins/vis_default_editor/public/vis_type_agg_filter.ts similarity index 97% rename from src/legacy/core_plugins/vis_default_editor/public/vis_type_agg_filter.ts rename to src/plugins/vis_default_editor/public/vis_type_agg_filter.ts index 3ff212c43e6e8..bf5661f42a9f5 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/vis_type_agg_filter.ts +++ b/src/plugins/vis_default_editor/public/vis_type_agg_filter.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { IAggType, IAggConfig, IndexPattern, search } from '../../../../plugins/data/public'; +import { IAggType, IAggConfig, IndexPattern, search } from '../../data/public'; const { aggTypeFilters, propFilter } = search.aggs; const filterByName = propFilter('name'); diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx index d3f66d708603c..6c430e34d5e29 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; -import { VisOptionsProps } from '../../../../../../src/legacy/core_plugins/vis_default_editor/public/vis_options_props'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public/vis_options_props'; interface CounterParams { counter: number; From 3d6fd68eb2117290451abe223afd5de19a55cb19 Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Wed, 8 Apr 2020 12:48:41 +0300 Subject: [PATCH 07/81] Get rid of ui/i18n in Discover (#62799) --- .../public/discover/get_inner_angular.ts | 1 - .../kibana/public/discover/kibana_services.ts | 2 - .../components/action_bar/action_bar.tsx | 146 ++++---- .../action_bar/action_bar_directive.ts | 4 +- .../np_ready/angular/directives/index.js | 10 +- .../np_ready/angular/directives/no_results.js | 46 +-- .../angular/directives/uninitialized.tsx | 70 ++-- .../public/discover/np_ready/angular/doc.ts | 4 +- .../doc_table/components/pager/index.ts | 5 +- .../components/pager/tool_bar_pager_text.tsx | 18 +- .../doc_table/components/table_header.ts | 4 +- .../discover/np_ready/components/doc/doc.tsx | 146 ++++---- .../components/fetch_error/fetch_error.tsx | 32 +- .../components/sidebar/discover_sidebar.tsx | 316 +++++++++--------- .../sidebar/discover_sidebar_directive.ts | 3 +- 15 files changed, 412 insertions(+), 395 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index 607d79b81618e..6ccbc13aeeb57 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -201,7 +201,6 @@ function createDocTableModule() { .directive('docTable', createDocTableDirective) .directive('kbnTableHeader', createTableHeaderDirective) .directive('toolBarPagerText', createToolBarPagerTextDirective) - .directive('toolBarPagerText', createToolBarPagerTextDirective) .directive('kbnTableRow', createTableRowDirective) .directive('toolBarPagerButtons', createToolBarPagerButtonsDirective) .directive('kbnInfiniteScroll', createInfiniteScrollDirective) diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 55f369eaecd2c..e6421142f6666 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -50,8 +50,6 @@ export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ setTrackedUrl: (url: string) => void; }>('urlTracker'); -// EXPORT legacy static dependencies, should be migrated when available in a new version; -export { wrapInI18nContext } from 'ui/i18n'; import { search } from '../../../../../plugins/data/public'; import { createGetterSetter } from '../../../../../plugins/kibana_utils/common'; export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar.tsx index 57ad8e0b1040f..8fcfcba08955c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar.tsx @@ -18,7 +18,7 @@ */ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiFieldNumber, @@ -88,77 +88,83 @@ export function ActionBar({ }; return ( -
- {isSuccessor && } - {isSuccessor && showWarning && } - {isSuccessor && showWarning && } - - - { - const value = newDocCount + defaultStepSize; - if (isValid(value)) { - setNewDocCount(value); - onChangeCount(value); - } - }} - flush="right" - > - - - - - - { - setNewDocCount(ev.target.valueAsNumber); - }} - onBlur={() => { - if (newDocCount !== docCount && isValid(newDocCount)) { - onChangeCount(newDocCount); + + + {isSuccessor && } + {isSuccessor && showWarning && ( + + )} + {isSuccessor && showWarning && } + + + { + const value = newDocCount + defaultStepSize; + if (isValid(value)) { + setNewDocCount(value); + onChangeCount(value); } }} - type="number" - value={newDocCount >= 0 ? newDocCount : ''} - /> - - - - - {isSuccessor ? ( - - ) : ( - + + + + + + { + setNewDocCount(ev.target.valueAsNumber); + }} + onBlur={() => { + if (newDocCount !== docCount && isValid(newDocCount)) { + onChangeCount(newDocCount); + } + }} + type="number" + value={newDocCount >= 0 ? newDocCount : ''} /> - )} - - - - {!isSuccessor && showWarning && } - {!isSuccessor && } - +
+
+ + + {isSuccessor ? ( + + ) : ( + + )} + + + + {!isSuccessor && showWarning && ( + + )} + {!isSuccessor && } + + ); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar_directive.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar_directive.ts index 697b039adde81..b705b4e4faeb5 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar_directive.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/components/action_bar/action_bar_directive.ts @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import { getAngularModule, wrapInI18nContext } from '../../../../../kibana_services'; +import { getAngularModule } from '../../../../../kibana_services'; import { ActionBar } from './action_bar'; getAngularModule().directive('contextActionBar', function(reactDirective: any) { - return reactDirective(wrapInI18nContext(ActionBar)); + return reactDirective(ActionBar); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js index 5482258e06564..5a999235bf9a5 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js @@ -20,16 +20,12 @@ import { DiscoverNoResults } from './no_results'; import { DiscoverUninitialized } from './uninitialized'; import { DiscoverHistogram } from './histogram'; -import { getAngularModule, wrapInI18nContext } from '../../../kibana_services'; +import { getAngularModule } from '../../../kibana_services'; const app = getAngularModule(); -app.directive('discoverNoResults', reactDirective => - reactDirective(wrapInI18nContext(DiscoverNoResults)) -); +app.directive('discoverNoResults', reactDirective => reactDirective(DiscoverNoResults)); -app.directive('discoverUninitialized', reactDirective => - reactDirective(wrapInI18nContext(DiscoverUninitialized)) -); +app.directive('discoverUninitialized', reactDirective => reactDirective(DiscoverUninitialized)); app.directive('discoverHistogram', reactDirective => reactDirective(DiscoverHistogram)); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/no_results.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/no_results.js index ba02068590c14..ad81d5252a25c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/no_results.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/no_results.js @@ -18,7 +18,7 @@ */ import React, { Component, Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import PropTypes from 'prop-types'; import { @@ -247,29 +247,31 @@ export class DiscoverNoResults extends Component { } return ( - - + + + - - - - } - color="warning" - iconType="help" - data-test-subj="discoverNoResults" - /> + + + + } + color="warning" + iconType="help" + data-test-subj="discoverNoResults" + /> - {shardFailuresMessage} - {timeFieldMessage} - {luceneQueryMessage} - - - + {shardFailuresMessage} + {timeFieldMessage} + {luceneQueryMessage} + + + + ); } } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/uninitialized.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/uninitialized.tsx index f40865800098e..b308607bbfbb0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/uninitialized.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/uninitialized.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiButton, EuiEmptyPrompt, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; @@ -28,38 +28,40 @@ interface Props { export const DiscoverUninitialized = ({ onRefresh }: Props) => { return ( - - - - - - - } - body={ -

- -

- } - actions={ - - - - } - /> -
-
-
+ + + + + + + + } + body={ +

+ +

+ } + actions={ + + + + } + /> +
+
+
+
); }; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc.ts index 459dcfb30d17b..092e3c79b1007 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { getAngularModule, wrapInI18nContext, getServices } from '../../kibana_services'; +import { getAngularModule, getServices } from '../../kibana_services'; // @ts-ignore import { getRootBreadcrumbs } from '../helpers/breadcrumbs'; import html from './doc.html'; @@ -30,7 +30,7 @@ const { timefilter } = getServices(); const app = getAngularModule(); app.directive('discoverDoc', function(reactDirective: any) { return reactDirective( - wrapInI18nContext(Doc), + Doc, [ ['id', { watchDepth: 'value' }], ['index', { watchDepth: 'value' }], diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/index.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/index.ts index f21f3b17c6955..7e155a6b82ca0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/index.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/index.ts @@ -16,14 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { wrapInI18nContext } from '../../../../../kibana_services'; import { ToolBarPagerText } from './tool_bar_pager_text'; import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; export function createToolBarPagerTextDirective(reactDirective: any) { - return reactDirective(wrapInI18nContext(ToolBarPagerText)); + return reactDirective(ToolBarPagerText); } export function createToolBarPagerButtonsDirective(reactDirective: any) { - return reactDirective(wrapInI18nContext(ToolBarPagerButtons)); + return reactDirective(ToolBarPagerButtons); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/tool_bar_pager_text.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/tool_bar_pager_text.tsx index 608dadd36b4b9..84338d817c86b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/tool_bar_pager_text.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/pager/tool_bar_pager_text.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; interface Props { startItem: number; @@ -27,12 +27,14 @@ interface Props { export function ToolBarPagerText({ startItem, endItem, totalItems }: Props) { return ( -
- -
+ +
+ +
+
); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header.ts index 84d865fd22a9a..5e7ad6a1d1ea0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header.ts @@ -17,13 +17,13 @@ * under the License. */ import { TableHeader } from './table_header/table_header'; -import { wrapInI18nContext, getServices } from '../../../../kibana_services'; +import { getServices } from '../../../../kibana_services'; export function createTableHeaderDirective(reactDirective: any) { const { uiSettings: config } = getServices(); return reactDirective( - wrapInI18nContext(TableHeader), + TableHeader, [ ['columns', { watchDepth: 'collection' }], ['hideTimeColumn', { watchDepth: 'value' }], diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/doc.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/doc.tsx index 28a17dbdb67b7..f3ceaef57d700 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/doc.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/doc.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent } from '@elastic/eui'; import { IndexPatternsContract } from 'src/plugins/data/public'; import { ElasticRequestState, useEsDocSearch } from './use_es_doc_search'; @@ -65,83 +65,85 @@ export function Doc(props: DocProps) { const [reqState, hit, indexPattern] = useEsDocSearch(props); return ( - - {reqState === ElasticRequestState.NotFoundIndexPattern && ( - - } - /> - )} - {reqState === ElasticRequestState.NotFound && ( - - } - > - + + {reqState === ElasticRequestState.NotFoundIndexPattern && ( + + } /> - - )} - - {reqState === ElasticRequestState.Error && ( - + } + > - } - > - {' '} - + )} + + {reqState === ElasticRequestState.Error && ( + + } > - - - )} + id="kbn.doc.somethingWentWrongDescription" + defaultMessage="{indexName} is missing." + values={{ indexName: props.index }} + />{' '} + + + + + )} - {reqState === ElasticRequestState.Loading && ( - - {' '} - - - )} + {reqState === ElasticRequestState.Loading && ( + + {' '} + + + )} - {reqState === ElasticRequestState.Found && hit !== null && indexPattern && ( -
- -
- )} -
+ {reqState === ElasticRequestState.Found && hit !== null && indexPattern && ( +
+ +
+ )} + + ); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx index 1aad7e953b8de..f8fc966dec351 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx @@ -17,9 +17,9 @@ * under the License. */ import React, { Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiCallOut, EuiCodeBlock, EuiSpacer } from '@elastic/eui'; -import { getAngularModule, wrapInI18nContext, getServices } from '../../../kibana_services'; +import { getAngularModule, getServices } from '../../../kibana_services'; interface Props { fetchError: { @@ -72,26 +72,28 @@ const DiscoverFetchError = ({ fetchError }: Props) => { } return ( - - + + + - - - - {body} + + + + {body} - {fetchError.error} - - - + {fetchError.error} + + + - - + + + ); }; export function createFetchErrorDirective(reactDirective: any) { - return reactDirective(wrapInI18nContext(DiscoverFetchError)); + return reactDirective(DiscoverFetchError); } getAngularModule().directive('discoverFetchError', createFetchErrorDirective); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.tsx index 5984df9c76e61..0adda0e484843 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.tsx @@ -20,7 +20,7 @@ import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiTitle } from '@elastic/eui'; import { sortBy } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { DiscoverField } from './discover_field'; import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; @@ -162,165 +162,175 @@ export function DiscoverSidebar({ } return ( -
- o.attributes.title)} - /> -
-
- - -
-
- {fields.length > 0 && ( - <> - -

- -

-
-
    - {selectedFields.map((field: IndexPatternField, idx: number) => { - return ( -
  • - -
  • - ); - })} -
-
- + +
+ o.attributes.title)} + /> +
+
+ + +
+
+ {fields.length > 0 && ( + <> +

-
- setShowFields(!showFields)} - aria-label={ - showFields - ? i18n.translate( - 'kbn.discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel', - { - defaultMessage: 'Hide fields', - } - ) - : i18n.translate( - 'kbn.discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel', - { - defaultMessage: 'Show fields', - } - ) - } - /> +
    + {selectedFields.map((field: IndexPatternField, idx: number) => { + return ( +
  • + +
  • + ); + })} +
+
+ +

+ +

+
+
+ setShowFields(!showFields)} + aria-label={ + showFields + ? i18n.translate( + 'kbn.discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel', + { + defaultMessage: 'Hide fields', + } + ) + : i18n.translate( + 'kbn.discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel', + { + defaultMessage: 'Show fields', + } + ) + } + /> +
+ + )} + {popularFields.length > 0 && ( +
+ + + +
    + {popularFields.map((field: IndexPatternField, idx: number) => { + return ( +
  • + +
  • + ); + })} +
- - )} - {popularFields.length > 0 && ( -
- - - -
    - {popularFields.map((field: IndexPatternField, idx: number) => { - return ( -
  • - -
  • - ); - })} -
-
- )} + )} -
    - {unpopularFields.map((field: IndexPatternField, idx: number) => { - return ( -
  • - -
  • - ); - })} -
-
-
+
    + {unpopularFields.map((field: IndexPatternField, idx: number) => { + return ( +
  • + +
  • + ); + })} +
+
+
+ ); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar_directive.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar_directive.ts index 9dcb459f83613..624ec0f757894 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar_directive.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar_directive.ts @@ -16,11 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { wrapInI18nContext } from '../../../kibana_services'; import { DiscoverSidebar } from './discover_sidebar'; export function createDiscoverSidebarDirective(reactDirective: any) { - return reactDirective(wrapInI18nContext(DiscoverSidebar), [ + return reactDirective(DiscoverSidebar, [ ['columns', { watchDepth: 'reference' }], ['fieldCounts', { watchDepth: 'reference' }], ['hits', { watchDepth: 'reference' }], From cc9c4113b2b889044ea216b3c8734244ec058420 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 8 Apr 2020 07:15:31 -0400 Subject: [PATCH 08/81] Document sub-feature privileges (#62335) * documenting sub-feature privileges * Apply suggestions from code review Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * address PR feedback Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- ...pment-plugin-feature-registration.asciidoc | 82 +++++++++++++++++- .../authorization/kibana-privileges.asciidoc | 7 +- .../images/assign_feature_privilege.png | Bin 507672 -> 651165 bytes 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/docs/developer/plugin/development-plugin-feature-registration.asciidoc b/docs/developer/plugin/development-plugin-feature-registration.asciidoc index ca61e5309ce85..4702204196bf2 100644 --- a/docs/developer/plugin/development-plugin-feature-registration.asciidoc +++ b/docs/developer/plugin/development-plugin-feature-registration.asciidoc @@ -45,10 +45,15 @@ Registering a feature consists of the following fields. For more information, co |An array of applications this feature enables. Typically, all of your plugin's apps (from `uiExports`) will be included here. |`privileges` (required) -|{repo}blob/{branch}/x-pack/plugins/features/server/feature.ts[`FeatureWithAllOrReadPrivileges`]. +|{repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. |See <> and <> |The set of privileges this feature requires to function. +|`subFeatures` (optional) +|{repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. +|See <> +|The set of subfeatures that enables finer access control than the `all` and `read` feature privileges. These options are only available in the Gold subscription level and higher. + |`icon` |`string` |"discoverApp" @@ -192,3 +197,78 @@ server.route({ } }); ----------- + +[[example-3-discover]] +==== Example 3: Discover + +Discover takes advantage of subfeature privileges to allow fine-grained access control. In this example, +a single "Create Short URLs" subfeature privilege is defined, which allows users to grant access to this feature without having to grant the `all` privilege to Discover. In other words, you can grant `read` access to Discover, and also grant the ability to create short URLs. + +["source","javascript"] +----------- +init(server) { + const xpackMainPlugin = server.plugins.xpack_main; + xpackMainPlugin.registerFeature({ + { + id: 'discover', + name: i18n.translate('xpack.features.discoverFeatureName', { + defaultMessage: 'Discover', + }), + order: 100, + icon: 'discoverApp', + navLinkId: 'kibana:discover', + app: ['kibana'], + catalogue: ['discover'], + privileges: { + all: { + app: ['kibana'], + catalogue: ['discover'], + savedObject: { + all: ['search', 'query'], + read: ['index-pattern'], + }, + ui: ['show', 'save', 'saveQuery'], + }, + read: { + app: ['kibana'], + catalogue: ['discover'], + savedObject: { + all: [], + read: ['index-pattern', 'search', 'query'], + }, + ui: ['show'], + }, + }, + subFeatures: [ + { + name: i18n.translate('xpack.features.ossFeatures.discoverShortUrlSubFeatureName', { + defaultMessage: 'Short URLs', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'url_create', + name: i18n.translate( + 'xpack.features.ossFeatures.discoverCreateShortUrlPrivilegeName', + { + defaultMessage: 'Create Short URLs', + } + ), + includeIn: 'all', + savedObject: { + all: ['url'], + read: [], + }, + ui: ['createShortUrl'], + }, + ], + }, + ], + }, + ], + } + }); +} +----------- diff --git a/docs/user/security/authorization/kibana-privileges.asciidoc b/docs/user/security/authorization/kibana-privileges.asciidoc index 6a0b7cefab018..21fcb9bdccb87 100644 --- a/docs/user/security/authorization/kibana-privileges.asciidoc +++ b/docs/user/security/authorization/kibana-privileges.asciidoc @@ -43,6 +43,10 @@ Assigning a feature privilege grants access to a specific feature. `all`:: Grants full read-write access. `read`:: Grants read-only access. +===== Sub-feature privileges +Some features allow for finer access control than the `all` and `read` privileges. +This additional level of control is available in the Gold subscription level and higher. + ===== Assigning feature privileges From the role management screen: @@ -62,7 +66,8 @@ PUT /api/security/role/my_kibana_role { "base": [], "feature": { - "dashboard": ["all"] + "visualize": ["all"], + "dashboard": ["read", "url_create"] }, "spaces": ["marketing"] } diff --git a/docs/user/security/images/assign_feature_privilege.png b/docs/user/security/images/assign_feature_privilege.png index e7d000d4554ad3e80b4eecc46bd5cfc2c67c1d54..26fbbf20b39ad665a3ab5d2dc7ab3f65c8922ae4 100644 GIT binary patch literal 651165 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

*8;$-fZ*AFtK2 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

d0!vkfZsAcqo2DY6BJ?AR(5Q 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

Hk7@ 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 E0CRj)@&Et; literal 507672 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

%i{%dnVwf(}40 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

O7+eIK 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

EiIiA&MailnCjO( zem_j>U00fQ^y4|^54uiR7gV+5LyfgW+9^#MBo~$`yyu_rx)9${ezA;u8#h$SK3o!o z(O-1JMwH%0>3WXZ=iPYTAR2U~>htHv)*T7)E~7xfSj*8;5ntP34yCt3N0>zx%t||t z7}paO?pZMZS-J{*&eV;6Y3f_~?LwF(fcbEpLN30{jx{?_ky@CWv^;phKRsY{cv*O0 zbJMe89%h8MGQ1?)^YW8|3=B7Plhe~w|C*a}F^FEHhEc$0j)=G_t>#4TJScK7%L1Fs z2A6xtnapa-Z;>RU=0{V}u`Ea|3s?{S4gs=${t>@P&=|BME7AH(b)??@KJm(MeaTY~Bb`mDctKr^bv`kA?IDDhuQ?L3s0;tp*WEp(Yl;=xS+`?!Y9<)nGui9mAz5X8 z<8&W6tT&6*)4@KpF`K^OoCSt^mUE!MwF`7*=F4O$vHY^ULrqoJ>rL4PFHjCxNG_PvXMfx#`i8;jG0O(`z1i(fVT zzjFYb+JXmsiJ#-SOwG7PcN`!2R!O_VS&U-j#~OGbU4> z)^9VuG;liF94vi9zmU6&OtR5yeF^h9_$azI-O0xK&YY6(kJik<_b0wE)*ePy z(rjUj{1=FdpnecTZhtpiAk0aDAmAf6!&MWY+X6g9>NZ^Zzwd@9+6g1ycC@yZ99=eL zXg?hyzX|!AC%E+4COI~CF$6csZTrf&VGJ1d4r8mKD@B%;h9NakrjFxcbWn#elPydr z^|;5WO4Y6Txj8XpJzXd^8JdwwOM7yux^vSoOUBn1Q$W*?jOYCb1c4!g#cGG@7<<`o z-(G4O895(b%(m!iX^}u8)R40|$nD-qY3j-vh6t=|-O#yFN@ebtwlhi zmm|Fm(@kR;oRGKngN{%<;!b08w8bn?2IHc?t9(uSUySL$k%(dM0<3uuI~y^s6HL|D z^K{{5Hr2j6KnJzE^kTbnvtG5U^7;f#qMU8$B~849Dmh;Su(`i>=O;^Hn-LOqC zryN+(Zj=GpJ4Vf;O3BoK7c5t-i4cvM#O%NS#=bGJoW;3a09uA$3;UoLsdQ}Hg0X&7 zdltcjJls406T7>ltrzFC%zx&DoR6>C9V6{h=sDL(=`$~Q25mI#ws5EE;Ka|9v~{ub zjg2$GTw2=N=Jn$SG*}xOn|L9wM7kg>Rc_(W^{o7p zNP2{V;hK<5CyV!X`rZ`4QHjPamt*KE7tKo&_6CfcZ*ZlqQw>3u(VUjPuTR@wyX;Ep zF=~l)^49V5#u~YfZDN#+1Ml<*M}c;j+ZLxF{(eKJ7t6z9TfdnN8>VHAb4lfHk~1X>Nl=G&;N)Y4=* z)Y*oeGst2e5b0iUh^Z0wjGL-WU{@^94?f_XrDWP9#ubYj0$vFZt)>OQ&6Imz17r@`CH6z8Uq?gI&?69wWtONx|3 zzb9y!mTHMpfWrMRcC)!M?fE!qTq<~EX!GnTYBhFN1MU2(r%u_(g7d~*1y-~0VBH(ZMqxQaV3*JxTrkId!7w3i^XeUx)WZpg^yvyu9s zbG5I1Y+cwF^-VXc{oil1szl&LOJPly2+W9 zjci(CD(de(#u-kM+)F4ZA-a7&yfxe7;Pi4zROeM`>iBv(H34?A>cDGHQ;T@fm$O+` ze=)|?f@wsrh8;r@c#llI7`*{pW-W|i&Y#92yIn3B3T|yyrFX1s=oS6jJ{4-DATK+q zHk}pi44e>f*NvEy>S?M!tMn%BwQPF;FX1iPXvmJ-WHP5-jtk|w7K<~v!)Adi9cT6$ z>S&lfR}qQ$vKQ~)(c}Sj+s*IDr#T$;^Rq?`c-vg7dRd_ohYK>WAzijUm=_PECRE_^ zF^3+BJEmk+O}6xRWMfUaJvv|@tv${HOC*XO{?%3emT1OP9&F4|SIiE@e6Qcr={>vP z&j8eYHEx@yCw)2GZ*jjJ&iKcM;Cwumu9SLtWlo93^DcfvL1n3t5cBfH_0)inA&jpb zXPnG#VOD~xy4fD-Xk>g8~gaPOJ?cIw`m@s~dg8=9$uvys;Y5hjM zy9(6*!MKFL!VB9|o|yY=K6P8UxfdSMZ-1&1(khSM)K|f<{jt>7&t3GRka(iO_orq* zr*c5m-u)Ig<6rIPA9i|Bl}c0d{>;Z&N0?2M-H%cqa9{6=&~8V0yM+Hl7S3F(Ji@(| zZdCXa?EU9T{%>mp9IPauxixxivHtTh$VZ=7e4^@zh$Bw=rS_C{~`rEsHZ7T0Zop?8+79~j85O*sOC}w?# zjZw!Rx%H3!>tBDy&u8_IvYq}fzWV3eep|omL4c5#XSF z|A*IqJNnuAQxv~&@=?lT$ySTe_IFNE|GQV2yHgkmCdgKvZnSgeDUW0j3;in=|L@-Q z*K_;F*}A9?NEg-0=0Ers&;18G`BdVIwLbvD+n(l8=piO=KpebNMR*r|2@m_)7i+baDrqI2-$*?yN6KD`1$Nox} zy#Nl6?Twmv&9-v=VxU~VpeRg3)RPd5vZ$)*ljl_e{B5s{%b5#QlHsSO4ok1*&n~rP@-nQR#Yg{oR5} z%+eB{ot<5@X*7sFL78M1c(?ro#0T=J-w|`^l(L>)?6$8gO&n1~v={VP*7|ty-MmCe zF^M6aii#oFsF#EL%BD_;60rM2BO}xmq}$lQfP@jjbQaS~ z5tSG{nIE+qLJe#tqO@Nb>dvm$LK+ktH<@En8!n~RloSmT5PkT3D@?MdpI}uj0UuD= zdn7*l&Q3tJxrHXRKs4mknn!bI>l-Xp+kbaHe=MBDF~HO$)-k=;x|ndcc9hMQKH&(& zz1D;mrHAbsK2n8j>g-ITGjph|D*xOaQh;7m;B1EnJ_lV6c6M^|m`x@EWFwrdS5--g zzAugZ0zrnLX|FM87Q43#kR34^hF^V>?d(9M>>hNXd&m58@*G|b zDsEu|P}JSYtGAimJv*$E9gEtMD z^6x=6Zi>z(eLh(gfvT<+er9$hvQiRRGPu@H%TQt`?oB~DtKI$|`OCj<&%_uYX6smJ z119KR?*>r}t7Fnikapiz=UgDQ!Cy;#DnsCykh{k=GGdA~l=eLf=tjrDBI|s zr$fqYAl{ZGm|a;{TS79hld<2zQ@PWbVU4%G?6cYPhFcLBOtdmj_lWBMqF#PO5Uw;wu-Kc~}ja4~6^9RB{@RV{1HQHvjMGmAor@0;2* zj9ngZvbX=ZUv&p1u*=4kWNNH^6&OCyo%%XUB|9gZPMus3EbZ%v1Fuo1HK_5->9UsfkeTog$ z%?H;{N=XfsRicooCr^IA6Epfv`^~aTmq8HuvUS(PW%{nrN_l_}JR{97Cx?w_ zld?h2-R;uq_}vb_rUFVp#V^e^)UB{=yg^K@coA|FBh0w`B7az~_ zSHAF4v(y&7Jl7=K>donE0gRa7%O*GJjE<}3V)8u2qLTpjdxne$+Ggz0( zkO79$-zlKD9~4ka%ZJaOq@z@{+`^>c3`EW&1&kA=^V?Ox`(RB1H=B_$SP#>n^&aS) zYxcai?PIgZD^8uo#oq>HXA=T^oz%5<;6HH2ID-K z(I?XJ+PT@k<$Tv;@7jA-ZijIkCZ-y5Sc?4@KR2H0^O$UWjZe6Yd|FP0)U`N3d5XjE ztVvM6j0`z&-DP9rpcd?4R9=J%X%@$`7VM~sZ&l%0?8KgWflSf`Xef6TzIV!AR`2RO zt>*;(DAg4oAI~{AV$G8X9_Cp}u2W|yq1O|027%lpqOUOoZ&9NOjMnl&%ZrlQ!w*0p zkmftKnkZlQK|HPr7S|~*v1(|ot5_4C(>yZj^DXCd0hVs=!O>O2T)}=HALse&loab@q)OTIORmRAb>@f9MSC04x4fXAV6cQY z-*aFuAV~NGqJKWQ^?T>$?bMrBmmNh1x^Ba~I-GMyr8}Mv>^Mz2g*YYN_Tilgjomy< zjEQHb#P7+({U9xIDqsD!pHecu`L$+-)83ZlzmTJn;0QcepSO0`Ogc%?Lt%Ks*YH6F zT_^-=_ZV--t`&nWDr$_svLf~Ao|o2pnaUmVvBQU1-nrGlfI2oDjlYAC%YkmgL}mB^i=O^H`2hX{np)BGbZDM-qq!f`oZBiL`8N{P1t2T0k+A9=a` zMqyG%(Em+n0HrvV{rsuC`8gWZyqls&HxI^4G&Bkv2lefEUpkb!z@t*)FB#ZXHF0yu zwy*V7Pm0s^()Veq1`zDfDY7N!P!h}A$;6CQbpH^D?S1xnw6|*~1P`sg*B!NX>LMc$ z1qx+R?q_l0Q&{6PXWLtTd1)MHZI@3x(o#W{fI~ht+^M1(Q|x8i`+Y0DJ4Jyt0YgPj z1qomMVsLV5#9bYMr_DC6lA%Xe<^X=`}EeQ6Xo=J;lya&yLoM}PwU5d{`GJ$L1;qKfwo0)reytmd!LW9g&MmuQdu z0k98Ar9Plh^QxevlA)oYqnB~REI%9+6`f3Et!FMF0TFS4zk9!3P+b}hT=n|#XSrI^ zR&bREi;Rp(*|0B?#vRhcNQjiATwdEa{w))!GUM%BUQ=9*NdgyC+)6*JKejw5efu`L zSoJraaCl}w%~ci6HIb&(R~C?)=$rYOnGG1bHx=9bO`>v@kBB(Pcs#}km=S~@b*O}f zd22-_CtC?rsx0LeJTf#gN+vB8o0cg3o!$O-r#Ho@Uy9H*9Ip2t6=>~}7z%c85dt^U zZF9|zZba)Pyl21qy|ZZJp5o^%FiP_!*1 zeq!kEb_XKAKCI+$p`?VwBTrqJ(Yy&ZJB#Z2#ZyC}A$=5q+w5SvXAb3eI7AgR87SLI zQy<69bErSA&d$aK_YWH@z%MSBsA6nx_C2#Ho$!77G_aUwqM{Vu5whWt4nBPKR#7PP z=-R3yY@qr|O$fHs6&gmfI-MY7?v4paHBAGCd;dVs`~&+~*7kssT*jAGwz|2<&oyQP z`KFVepP1f(&>Om>eOu099L{Q}DDqilb8G84x^g9CzcmZpjFX6ZTU%Rnh1=|{;2bw$~Tv2_}WqfAU*Q`Ah6+_w+B_%muA!@Kd{6;9u$$2!~ z{?g%BP;VTbf_Q~JC*Q7!OBgPe>asU=;3~3v{X+M|+p>Vn>{pp6zSm71-Dl#KDE5=o zHVdv!rnXZUwm|zAN~3qqD~fM^Di}ksV*#4tyx6@AbDPIvQ;8QIiXI&o>&e9$&Ku^r zV9q?`dw1<6)lSAyH&6%X7-HNd`y`|+rzCgO=rZ5(!otT5W}XKP*!dZVs{cK3Tr1Jg zo8S#)Ik8RLGS8T_V+7M#&(6%m*{}G`i$Q^EWa3Gp2}$6=6UJe3jvqI-fP2&Ddl0xS zqJN^)?wvX>O4_2kT2$?olbBjw`<^d%*^A1ECy|#{65UiJ<%}|yAU7wKHB@3JGAMGy znJ8^B?@+pf*oDf45yF+~hqbe7-_UdBH=E3VqY(s-({A_fpS#({d=Q~!Ne=~8`hGjP z2FaD42;Vp@v^LXGTGMn*H!j|j!6gz~aYj%-mc0Z;atY;X{N!ti?lY`?{^bt^lKmr-rSn5_Vu@gnhS68ZIg%a1<;93)!ty z`{qlBbil0XN@wqob2ur@ZUUG<=d~XkdLWZazx$@*bF;3KB-7oVXH;T+2%w@b)QQ`l zp(v!HooOPI5xD8;#)6R4SV@VRF0{qd%fCRh8a3`J_!E=aA+#Nd2O7cL7Z2dT1 ze1#GYR+dN??srV^KVv)`Zu z`o=1$M~w_i3?YZK<|f&aG~LT}zFI?sS(W|=&l7bW0VY&TLPF)Mv@)ms#gpw9MS2-( za{6-yC^1J`mOhM*}RO)B`xCci9drqu~N?exNd|=*3u{Z#7Stiv%??I0>tWcuZ`4*U+XuL5aicLM_M981+ia&e)6>&_s3+(%T4tz)Wnfl| zbpMF^*V@8=pR5Kx5=cwkSR6f*G~tmz3$(7)!p?T5ii36wL_xD1FU}W7)Sz>y=>iLx zT>P*bOHlreZx4FSc^t~tVsqz9bi@f0jExP>6em&r>x25+O%QrXxxP%0p#)`j>m_OZ3FmX( zuf5NA0*?uS&L1nY5<UF%fKlg&yrxrZxLM`{wG8{&E)eK1tIV6ta&?g9Aexo<6 z@L#9Ge^Tdute~UjQD}#q$SI5Du)$9jtU;a8*TxWa?^|1W>_~alDJkz|Gqf-ZUHou( zkuSN+N0NVcG8I?#>cLk=PTDPFU z`CaAiZJLBHTX)qI_I!@sEv!IAvHE`|PENOR?2fp$eUtc9u!l^?3S@Y=G}QW>fc)E9Rqd{f4z zlTW_s(e_)U6diYm_6naQyZCT3rEj%%OIKGhJ@K9bM2o1L`M#x)=|^@fa+K`sn8v54 zx!+JwBvZtpjzE{ldsVqW?%fNd(}-Mm6uT~a0S>52nYIu0_KMhoecC-12jXcT@me*8 z&(DB~E2NN&nrH{rSAGp42j&On$NR(jb5*xZ*H$R;Rn=**}KGD|wKEPe4r zQ;)|crd5S3ZcGG@v+9jX=bNB(`@BBGtE)$X4;R%w%PIlQ0#2h{r##Hcev!fbZS7tO z3Z=IIQf@Gp8!B2d?l2lcd!n=waC$p8xlBckZ1`!w$kYVX^abNEp{53q@2RWZT~{xjN(fkg6PG*Z?d>h%Fwjm@ zb3~)-Wymy?<_hv)I=eu2Q)2%Tam-AS)YB<^a_X|&DEufq<{?i})kWc6-BVJsr z_^CqHD;i{1vZ#?*lI%ydJZLL7V~+js;cNbyK(_^4q{$ux+7R%}-iR|WFqnxu+9O@)?(y~lH(BuN94n;|_kG(B)b7`n2 z@7YXgJ=DWPt=-imtqDWxa%?6-#;<17bU&>LE2#Tjzwo_vOt0 z?1n(@tKCCG++DI;8FvcT&1Pi2-Ub+IC14e0p#AIV)9UkaTkJrJ5fgNJKX|Eh)(Kzg zane`A<;}l#2STM(oP1*=wiN6#oOhy~d7edhloNKME-*6Qj{e=#{76c)3~SBa-N^Bd z*BZXs)8a5^6x4E$p?6b)PP0oKeDV7Z=Q>Qu^C1wUotxu@(U^4?#tB2wT?3bA( zbsK}fU<*dC8gQ1R#>8(!?nUqUt~H=W|D6T3W^^6K+VjD{er{* z8lPF|=}!PWJo!P8tiv4M4oIIfmg@Z`)a%e zdb+yb@;h5v-n&jFCMPo%cN(NI9mzLBjs!35DUdJc>w&Kb{>cyeky=nQeU@ZGnu5t} z)9;mH6KHLuCrMwPTf}%Lo7`CT|I+Tb73`gy!mo72JI;5qMC-V32P;za!oLOBR8I!d z(3T^&bik_T-D#*5gaeZb&+&6}v*sb@{-o($EUW$A>4S>w!nZcYMqSI9k?PmZT1!-_ zmDRim=}=fe(R#sb>n$^55J$_;nywcexnDLD9L)MmO;W5r_1^Ky3;1_2>v z5zuhSt5wCWTjRd)XRId4zGxaNL46@PCmkQUFi69+dX1<}=evxIYc>)jw>LYc1GK88 z8T&<6Y!4hW`v)1&q{u-bdrknm1L*Nm<{piAubT5nbw=kd*bkLIwYX2eI8YG%&B*cW zma#_z-4onmk-Xb!QJeOM<&jRAY6{}COakxdWw(Vgiu^zz_|C5O^=~iFj=%pd_?OCb z3s)M`w-MjP(P>!XAy4P%$qNZv+^K{9Y(qYI{8lYd-&_aJrWPLapQ=Mmk+P;7?N?CVfms>c~z$1phY*_TM4_!7+Zhw{bJrXF) z)Lj#aUkR-#C$k<7X(ZNM;&Yunp5S%xGIpvZJEbgfsb+MfXjvBW!0`uP8;7D;B$(B# z!QH6$r4_$Mv|kmvN3mSBSG~VBa-8~(4~+XjM^~4pxP6ogJPOs@ldL%kj{C|R%fF}Z zRI|nkp_V3=qgf7e6gXFhgw`$34MMS3roRMj6)P+(oodm+;%~BRT?c(8PBBDA@2pu5 zb)-TM0F|5qn~+Lg96JEAs_NIGdCwozsY#Bfd-0z?sqCQ!s?y0cRjh^f1-t)@mo2V# zbD%dPBsO)PIQxHd$abKsvWZKBga}@Zm#v|x$-bQzvcJ@(%HVHDdWTH4XAa@@msCnB z^SdZDN(tML8cGOhKTeB;H09$wfvPt(P}6yOu)Dik9=Ori$Y~?GWsvut{(=)&Qi35K zcDqW~ZA(`Xt2}Q=@Vx~ytbl%TL2Z>=!76>-w2E!5JhJU{{NP^@7H^!0oR2f;o!caJ zW^6i==1s)LN_4ld!U6!M^h_Eakm{Vf#O3BdoQnz=<+7W(_ZP!&eupZd-6W;+aFc$j zLvpk*RA#CB*mOw&bf~&4Lh>~Ejz7@6UJ~h?Uoiq>`7pH%xkIH}RfsAtOY!|WNm zu2LleEMfGP+;u=dT)AreHT98J-qug};3J+RcA>$u<4aWo#_qmLHr2K^VEj|*iq$Rb ztvs*4P9_QzKJT?&nDs+s0VT6Adh=izGK*R}rp9Mh*h)ZNm6PKf^V^cyLZMPU8nMT# z67x~)3@2360xm$mAKW=*b(0G^?v}CK0EkG37VUf*hV|lmhF3qs;T&VKIn_QbGK2A= zJfGUn=erGG%1-91sX!b)76hOzErq+t7~o$romS33aMhBmsL|! zlQ1Xc16|Fl@{5;QT3TB_goG$EzrfPxPk?6Eqvt9pC|f=0tOn_qeRe7)@Ppt|y-U^=vO#q=`>!ehgum(b6TIi3^F*7JN+Rb4%TILiDH z`tR*c4M%+N3Hnoip=9Xu`@bxJG;4sCtSgg~kc z1h&6#Bloe31zfVfUaz87OQw?A`GA>1V)uKixGo+*VCBbO8-5<&%+ffJwzu0ipsPsT z+z<@Y4G6=UdaYW4R~MbD>_(;MAJqR10ljE+6#cj#$Lj5#E?B+(2Jb=^H~#wm#Ovu3r>=b&q#OX_td4un$4F8SMFFzAXEe;`XrjF!k_Gx*;jvai{mj zw|&9Nthm24(+%n*fVmd2_$gQ_d zUzh>5;|Er}si_wXu@u|_B&A@M&HAoO$KuVFc6S-rDknQ45<|uL123p4m;Z{)7KvDW zZr+8a!_O%db2CAraKD8?Lue;|M8@cP9Mb}7NnpZg?n`1d4|n`D@`7Y&0}n-qusN_iWO@QmKHV7Ju$LF#O)9sF9PN_k;)beK3P!d@YyY7uyfUHiAEDg>vkQ~cl)HUcG|}_ z^vor|-lS|`jXVtiuDE`>o5r<9c-g9W2>kkqH8IBe_Z&2@;e^+1t8`;`L#E6mr?_xm@XEa3EPTiDsd!dB~0_E2BjZM*C2 z_QHANm>yl-7)gK7;>eT_K|+EIe2;!C`B^A4QqVfxiuiX=)70$h;MW5J9sA1VMdzje z9sZrS1$mousWUn8$z1@hKeN`Qd~Ar5I^Vy>ZTIkDV*3~#;N<5ToQRdVhs z$im8*T@|SPtC6cAZ#aDJRr@vZTVQ&p8q6HzBk~cAT}r2T?0F6Nm9XJ!w;FRh@2Te( zmL^|ehy;}R{W{LrsY-NO%*aD9{;QsxEvPmx+ur$hIjtYpKTi)~q%6{CNfL^dVqsSl ztxDcsi&+LFJ!k zby=Gmy0r2b+*T5B|M5i7tg#k68lhfnOurx@nR~N9mo`s2EspXZntN9(v|Fds=qGRPH)_` zy}Nw;EUhwbDZqldMum|R)|WIt=~{m6cztxhm5zZXlVC&$^RtbgmpY1oU5(cqy{6g& z?Uz`>5Ix_{ClPt~rFwu}JC4hIM;`h)Fz|cedfKourC0%_hM|3%o`zNcp4eYc%xQ9q z(xiqAU0PZCG8&(;w&>;8&cNgTF2$_o9+r~mc$vw6y`j2auPU;y8{=&+;K>gd88Cta^gUh%>D^6wI~`(4={o@%Uo{WiN+diSHSQv1 zP6NK+j>|$3&2etL?YdRy8*53mYqc3_kU4%R{n;#h-I(?mI_4q7$+&}$jc$CrXMRpM zY>wXD?T?tAKP~p2z-QImBfRd3vOLoo6}Vk)+%6eAJL>%``e*e*fK`$GX&8x9R-~Fq z{o50~Nwc~82rRdl=!IZO1_!<0oX#duEUbhsLY8S%o~2JP^HBAhZOgf`eY^K-+w+GJaFFdjSI#>?_|w*R;uYg^*Nq=7 z27i!-1S7*1Jlodx8D2!H8?>6WzCJ%d$DTpLH#|b!dOYj+XmH%TSFDocSR+%PA7NO+ z=Vt!mP&kjivscr$I^~^$chbc5nBTT3EWFg*1ADm4b{m-TASP(=Kuqhd@%5)qK-=@D z?A{oJb+gEpr^2aq;ormD zt{YL^(it+ES0~OB+(ehXPVno!kZ0;<#Xp!0bRyyn*w5u34LVDCID_Tnh&D2-+oec8 z;ob;tKv@^a+K#m*jEwx92cOonkqxqz75F`E@48VrIz7DH+Lhu%GZo zb@l4*_lI5hvwh?~dBR#>={J_~<%T`Vi?~vvz!6 zkdzbiwDxk}vnk&^5l!Es_m#cenx0c(nm3!X^I;b(KJu@@c!CV;d=2NL>Ka$24 zWli!ly+@pfSxEoslC&p#=}Xh^WDOia4i2PQ)1`?WFgSxWJnUOgfg0_Mv`vSrNsJkX ziA`5K83SZZuzTXw^x>ePC*haW8E4*&&5GswiQW9ByKP|1pe{t-lW#Xm_&m;h-r`_4 zpg>KZWTe=9QcCeyRr!0kG#qYn8wid%GctsH-K~cE+zQJ|FVZoZJ+03b*kMpQTik zpNz07cvhpH*v>e`pU6$m^o+~i!Qy}-tb6t6qwdf1ek0Z?C5vsIP8y$ED&f0a zt%U>Qc1>+xTbI?CNL@9b(aQ&Kb;RT3jAgduVc>M@N~putT1-*Vprsih_K@0#EGkz# zX>^4!!zMfUt+rjRMbOwPUZKCdn}5MwE>cit><`b*aDU(e= z{^EdN!2JrqcDCwcb*WXm2l3i;bRbblvvW#Gvi0}^Pe}G9fIDaCTRi%Rn;uSeuE&Cc z#{|Kct7|=!$EMVH8e|QO0Y!j_P(}|beMs+g**}6+ z@7bz&+$j#Nx=`Qj{$*?guhO`Ky7$VQ(b4YJHd*D&5kLG3({*}%`Yb~Nvy_QyBw@qp zXGceE*78#Q;zta^W`o*fJm^z-UDYZ|fqAe7mZW0pBZEwKk@iM9OYblS?cRiqRoP6k zn`jy} zIx0}CQ#vHad{)tKnFSL66jELMb5Qb?)`zW&$#Kpo?|n5yXZCnt5K+@<(=wPWs$N8U zK4o;)T^7)#5{rOf9RvS)lWbNeMx&0kG^*p3X9J5|;Sv8>H@*}uzF&UgJINSShj2Zv(H?mnw zD`60LwCmwsusKYwJcU=4l5?+hNR6f--JWvH zW?-T$r=WF!O=g0RS)aQU^+9=iL43Td#hq?13EwyShqZfeH<6Fjr;6R)SrIF#RK_^= zf`r6jW)}E*UUMMLQoFsi=zF(L?6)$c4Cwl>nZj;A4ZJ?uQvhM{kJlb+7XDfv$AU@Db@GIxd`+FU>)3i&wE?Q8+VMDj6X#qf8DVKa$Hdj zWsz9aNklkRlooC<29`<3n7uAzi;Igh$yVGS*u3Fbd-pxBlN-Ub z2B#WQSoz_Plxt&Z%e<+s(ymU3SB(Di(aPK|9)~tb%eyv4tK;9Se|abMs0t%AC& zYmXZ<-_v&P;-H{WQZ%y!9(_H_U?D(A4@$2j+4R7daA3exx^xL2eVk+u~pK8Ql!p zqr8l;3TaT-YAq5S=KKJ_o+EoZPaFVw0@wbLtNrasxSu70TtjN;l`zCVl{Hve!p8I%h_J%q znyN~?9_Q(xrkYmaxLfh^oSNVIrj=883vhNto-S6V18xLPRh61Hzd`ldPVN|hf3&`E zAx32&OSWgu9Jm})9tNgr?@IVH3IB6GD={k_%DfW;sZ5jcx=R3ka`UpOq%L9#g=v zu1*OE%XrI3>L{XGed% z2jvA;!K{I|sC2(q^J`xY8OU4&`CTvUq-^11NCAr)@k;Qq-DBz0kY&c)HH5Mz;bFK` zHyv+#p5q>6AKLR~tv4K04_E)BTVx;DT0J`liyBBg9?@uU0G#vt1z6eh_q9fcwj8U- z4~a?bw=8VF6Yp~KH>5uD4oXv7zRNW@sQ#O2cgu4J+*K8B8}((~Aw(8_iFCFWon3q_ zjBTkF{j>sGeXai1JiI_dbQhp%CG6UH*JV#dWhF^Ckemo01(zf+XiV%6cs+4fT-)bs zF{pIU2r{<%Qf4&9%j-1w$eM4?z_Evf#f_PnnM+;w8l4^%9P+K~8Lnvkd~H_QRAP1= zUVH2c6VahY0Z3R-#AdelNZI|y`(IO73R|<#0N1m%;Go>lXIN*fH=swZc-WD2sjGYL z*ROrGeCyJEDX-?nIz?1n3r7Vj+^=VNYb!dRzq**`m2~X>r4Q1ZVPn@YCY(i!EVQ+O+}3!`1Exn@fd;lDE1N)sTE( z3_aveD4_k{vEUW4Xd>YFO^s_srtN%Q?EAjM(7GhY9y38 zEfP-GbhZLEwaO*%%6M}`ql`v>=RxyUcDAEd8Z8vbXAa=Mzdo;`N#I>{Aokl%v`_QR5v7${sw&GztfL1y{T`&L>3-;HkY*Dr-G^*9_c{GW=^hZZ*?Zwp>t zz}%GBQTj?kMB&BE6y$?fskPyb1N(2^_K;fQ8>LGli$MW@Ds;}8-e{w zmMm8AAo}!pqYsKAK^_OLu$D;<4SDXkWeDbBriFUToc#pYyScrEgig4G$cUTB%6 z(~WcEWC_#!dLI-kn0QGOn_K&Rt&$VdU>v~uF;f5}TZ3SpMc>)a_e=gK3n0Cypr8V< zLEC}yZONmf9>A+#v&EQeTaPxgho1KXB68|LTMXl>mlMw(1fS8?UIhsSFS20de`F7z zJAa<(W`B3$^5ptUM?rJ^fe^>}8(Q#B5r@C^xnx&WeP2JOa_eZ>3tQji$+`iPqM~yu zzLnRMgAI_^Q!v=~xYJWC7O)kWz==2yM`?8_!8@je}oYk#zf@2K~c>E%H3m=>h3gqu#=RJ~y z_(=RbAY{^7a-kMEO;&C*__9FQ}Q0(|w-c7$MKA8WxhN&E!s5;noM}{*hZQaR!&t ztRk5NM`R1dVDW`f@s}@jRJvOx+#t>T}@+im77t>Ps?MjIM?r-}ZOx-?Emh2F-;=t0n z;PWzbdmc@LH5K{u<}$)BmV|Mj(!KK%y>Bm+5i-zGE>H2%lP^Sf=0Bx2Mf^_S8Y=X9 zmO8od>;Ukww!YxrhM4%aqwtD3Oz^QdczHjX8;6Y>nQY#7;zG0lY&;Z4Z5Y@nalc$- z)Q(0`egOQSS2!&yc>sU|&=^hS(v>_gpYo*3fvQkJ$IK~DUgA(I(GGJ8RdQY?NkH|I z&T+y77JN5GqB=!2H)dMBY&{81;plV5O#%iE`putzUg)*d9CH36n2>?I$kWaJ?pm8Z zDzN%8DZsmb2HW7h;4aQ8`z~nu?}xxJSNNXQt@2j|^77(HSFhNRFW&}BI%!?0sXj?p zXv*!W@}}7{pFeMA`Bu1Os)ol^SJ$KzgbSI1HvvU?Cj@lEQT@b6zoHVzWpa8V{HjuU z7}_$)y2H^)?2LwBgB!Z?e10?cq36-nZ0eFcXQ?C zS?MqB`MSr}f2S`f`biPgfUx`QKrsVAqh3cI6myj=EYpa~QH2+x6KIk+~ zmNmei`{v?aO|B4+U0>dfAhZXkFh|buXu;D^y3S@8PFo`8B`i*obe**#FY`bDNm)s2}b3lCi z@v3{8&bci2)uJnnbG$lIp;pDI zA1t@u0TvYGb;RF1f>*H9rcY9Nq}yz3D(`9PKc$MNS|v16brl!%USB=2|Ko)nFo-uH z>ffjTu)cBd{iRgPLli{~&x#-ETe(A!ZP{<@A4TdoH)&rj-(Eh+ioEsIL%YCCAFv9n zbrv?ALl%Qb5^qcWC+FUfZ0%zN({sK5HpOHgva1qCW9@CxH7X-np$w5VxM0Khcs9~| zesS^E_;_u7`s2kb)Nvs8&7er7Vqj~{Sxww=2Mf49J_{ud>7E)I5VAe{ad>15hBK89 zNn{J{5ud5M0RQK{uYD`aS-;+L#uV~g-1b^WBS!7jT2O@Ge(wJ4T(jN+$^TLqeykPP ziOIaBSCEHjQ#Oy?9}&nSb*zA82qYU5uE)OONz z)EP&?oNQ7=mk+c@UGq5vU;TM51KID?t4|k)4woVyacGrpwY*1!m-_C4M$0m!VY1|T zPvLy=;d{jw~OFHao_>N-!sNVnJrx2K-1NP|ayEq!VuINMz$fsXYo8jpJ3>Ti$k7 ziQI-Dq)hs8u~HigkQk~KAB_X^r&gdkJAo+|+FI?|Y_pE;!?5(7k+>PLaAvt&^Xh`w zxeFi?Ym1>F9egK*oO`lNeit+S2ZTWO&eY;r~&{EGAk;3Wwt}rLT*x68cV0# z#E#EvF(X6;Xh83}yxS1NUCT6dCp=R^Eovp_^0sqC!BQ|z)udW)S`OFP*zuwRxqbZP ze-4NX*}FPLUU~l*$BJ*i)Wwn0S97x~O9g_anDbOwm`jvM9bQ4cdV9G$|Cg-#4R!m8 zr-vBh$sl?|rIxQG2@oJ(qg6qp0;b#8*AqBdcIrN3FEj=Q4;Q|cDf&{Yg}nqbHs-hW zJPMkt2lC4}?R^;!;Y~GdEg@A`a6ni^?j!3-s1k7@tlh8M@~x)-yspYYVc1c|N%g4C zXSrG1&ZgPS*_vN6o;rt`*OyKWOJwQ;)x;RitSw@d1s_=M?Hp^@f!m`mBEF% za(4I|Z3~R+YPTu}%;;v}i8@ryMPtx{ib=8(YR;jIo5I(dQ|KbeWIW{}v0E~_tYxP$ zS)c%cUYnqLG_u<%DvjX$=~zM4oODc11kr7`yEo2zKDp(wqcygc@W$UmzP3F9J@I|7Cxj z@9r62jYm(%P5%?Fcn`${W6kyhiw-}fFmY$km6 zY(|sL5j@~syEaiS$MZo`D-x8(BCwP*5Pgap-eDOSQIGpe1qHDYDEj;@I%`3LB?IZZ zH}6+vhu}3*hD2*HlaCJUwo@69i#Md?Akt(ljEr&?uKcSnyX26irAc|D@-g*&b}OX2 zB=(-8iMGqbE9ZZay@Tu_n`t-jW9{dU>VCUP9dMv?ZLLz2$H<5|8M~@hNiO0;e6Q+Q z3(!WKE4)n-9e`VM(HGTRDrT#c{z6oHW zk^MrV%DA;-xORP(!T#OjZuhKW3i~~XZr$@}8M7M~FSvGo@b4@v#U2ooleRvpEy^vj z;p(3Mwm5Q8`8#3;Lip6Yzn)IVagQ;gE<9H(8`teaxcCQ*b6ZN{$11o zt%U-R^$q0^*ERD#=}m0P@z<-0t?N2HAN3ybH#mFh?Op+pF3$+snn;LFFT`_6QPBRV zIi@aCp0;d%nds3bv&{5E>ep(|hd?cBdeU{}(#(mm(L{)iV|oq>Nq}k~d5?GuA6|4X z4bUiT4B|5PCX4Q7+NjNY7YD?Wjt=eSm^oAHLz<@S0VsZ#a_wg6Ml1+PeEMWF2K@Mg zK4-gYC#@?i`RJapLABoIywJka*A9I8Wc#$$m}*+yU^T7V1Slo}=FlVWFWmkl$ux5)1rk&+NVV%ODo@})#&YM< zQQU@5ZEffmkp^T0o$C`8O7Zw;QPOVuc&T`#f_*Z=LXA5jdk+|{!E2M^xfp5T=lnK^ zmz6U-*@1irm4+zdH_w4xE;n+W%-6Z!ey<@!;DDH#!!6^jK3-Yx82dfbA-9$wh@Wl- z+yKy9tNqTI7*xmZtPEok-X$0)^a)?xf~ZbdgivR?Ss6uh|gShQ4lv^iLq@Y4xTko zr5%{tukT{(IVHz)gOmxPk-q1EsOkxXW)MK+qxTXPc{4|Z^%1Z90YhfxK@dQ4Y= z#kOxQF@bxU{HSa=D=OJhW<~4ci^ki#e)u$EzI^#!GaA;FeO!d`TYi32zHfl&l1>{` zEw^CgCSL8jAt!b$^5yxg&c#am)|%t2oS=%~ndW*$yHuaQ@B9=)i3$F4la%8(pBROz z-cJ)Cr}}6R9O3Kg7Lc{9{Lk#gp~7OFyJu8BYFHA0ZA$|(EbXbT8&y~BT_xY(XnlmA zt*NQWW8M$*IchevNu$ZyRX#Ee4x0AW+I%8Ix(F%5*V1hJZ6j$Qpk*;@PjK-ZU`zr3l>T7{5e01c#>oq4m06 zse&bU@-sMUM{x1Vvo;wY;mo{&;iCM?Y2MNygk{vI-zHHaw=<-_W)Pw+K8A)pwz}YM zho;<>^eL~yY_O#_GTky!eDp@x?}<$$&&!$1-p%PSM;^6I;`cWUv@C$M*n2?!gcl&* z{66$zrwPZ$jx{+%u%{>Y^Qb7NKqqcV4!%EDN~K&xdE2G-4GR=vt#w>RYd(uhJ#Hm& zn`-5j;X?F5Rrh2{QM9v3>1G^h3Ol1zxO58(AIm3Ow)_F^cI@)SP(de~?-(EVG?q!( z#<*!ENLw!;G`Cu=CA05n^0^uUv^&S6Rc#I3X4sWWM`VKk(&mgaeDM0vHyn5%np(5> zjU!@4-L5XMx+g7#ed~cRPD2xA{a{t;4Z-H{)7-eT`7P9?=D)%(Aen3oPy}a&oM?)n z*1AwKE8Zq=%p}=yc)nK&Ef)l&EetQOAi<0Vj9c3XOmAqQFhZSqkF!#mms#0DM&UAx zj<*L zcd~Pj9kBh7zC@fdMtMZPVc1KlL~5ZvWXUISDG?JqDr{F15ru3h@^e=c96x(=X?Za? zkM6KwU_V$VKA9lrta)d9)O?plQe)BLQA_K)AA*Axcc7c2h>$4wm*TJJuHhQgn_%?> z-_WS+1c;Z@f|&MwxRvBs<+^Sx1Cu#oY`{KBbmO;7 zHB$>-@rFv%K5Qt@GZ$>H7k~ZQYR1hRp!yLg%U72CJ}H#)H_{l???u1+DjfQl zN9Q!%0M+}~%+O)_vXJsOURhxkYgX?mu37^NTD>}C#ilR)dFK_m)uWYB;QGqa8g?5d*4CK~&x&$)mZ zVN6t${N7swOpBK0)4;I2w8*`Cyst)@@WYDL(W=oxrj*q$o{Hlv$No_8c2iDK#b>>R z<)|pZjV*kHT3T*HBSQsykSyC183S4p8C9IeF7?8Zpo6GNXP!N05@uFiiCl+sR#{kI zIvHEqa%-w#{JNn)N7s`F&l-10x*gKZNKUhcD>cnkGKJmr_;ewbF=p_eof1 z?hOtO{$w`rIrseBU!_ZMk!#U|J06%iz-!~2-w%J~SSIh53RF>1tbTRaPvEI*ba*j* z-X9V$v}W&q337%UJnQA-BLd5*tn3mxk^uQC>h=JDc+aLacFPYG1>al~Kl7@pG;ngm zj0JP88XJp)Dnb(*M8-WPJm3OhDi?{6ekaNNq`drs8z}dF$(Dj`5^94Pv$(WGTDo2% za%%8P^;>~e+p}YbnIAQLY=b|5hL>WlPzuIntInAG1RI1X*jIZd$}{y3*ye{9JK0fC zr3K$h3ii9YE8A6mfEPn$v%ju4khS9UV(#W(Ce_l7nN#m5_XNUMqETt$KZ< zF&00Hp6fxyQ>$@QR1Z{Mv8Yw)<5aZ5x24LCNkFD)KzZGR2NgWjwAS7Ff*;KTuLpEN z=`k@sv4}0|m)E1RY?1jgYS9nPH5)in?!zzf zp`!Vmp6~q|QF!DSgkw`s!yXpKlqGEj!~-*?B~{&=|f^Zjfkx8!t~u%Ecv1)%4I2ymq> ztZ`VaSO&3<2>kg4-Cq+sm+{EDpRn4bHbE)-DcxgIQtJ{AIn2EvR34A}dM*rTE2GMR zIf-y;p&Yu%berxC+n)AyI^d`Zbe3D%`A#PHgN4^<7?v!QLGfcvD`S7*d6oAcM9;n| zEUBiT;YYqvx5+X-fO7P{1WOgMB~1(~yv7Tab)eNR`3d7sNy^|a2&CRL94%ugZ&#*6 z7o`T+X3J1387?kPdz@aP^Ku(cFz~}^EeRl#o=+SBOo3D+mc3O=|FOG(DSKXXd?^OR zO_qWGI%zo{xW_Y-JrbZ1;^~UJNfVw%uK&V6Z`1Z!`~Wz@Xd5=Zfg-PaG#e6#uXCj5 z1auN_RjJnzbB_AQm!wX;&|K3A7T%vq*;p#!heZT zW6t58?%?%9JxL2Tg9-|rY)q55q^TPgAY?%8R)%1$B*?XE-YNsA2ZF~(mvyQef`&Co2X_xd-{U=6JWomAUHC_=5%34(22Is3n89kRr$(# z*s)UVaC+-inXiv}KTZ}NJWI?qqm(#bsaTu>bT?vPwd=`4$u|ZH^N4FVUO2U}fXW1p zKhncv@vq35yql}im7f9UM~tfd3=Qwz=&;mR-kYx5Ca9v2aLhwCB=Fu5$K;NY&R9lE zUznXSs@TKb^_GM^RvJRh-X)x&` zulUigvd40xEf!xHtBmc=*qp;niDi$$$-486+~p4m=C{~3cQg0$Sxmo?w7N?8HGPOh zyoL{lA(!FY-?`eq(0@N&-8-HHy9nvA&|Q0Bl#`GkhU0&0DQ=HCbNF+gJnayX6R+28 zwmH~#>+;`!lu&;OL^hggu0ui$W_KFro>vA`tQdn%jJc>zA~~WsM6EtAtNXY>@1U600XHQvD*p|2}^0i0#xBRme3 z3Pvr!jNoVOF*-a}6&_LVCu8di(1&6>X^UUr{GH-Nz;of5>W6LQ59e2gE7{dtsT3Pw zhA*}{3vW$*Rb>R0I+{l9-m>qnU<-KK3lxLdui$zkbG&~@GwPxrEj{1 zj}6~fpTd!SJh(AV)z7`T;He|htb$zgv8(s~T1X>L^?tE-4kAtKj@@~PQDlaZL{Xyk zw8`GATTN$@9etb(7Ixu;;cZM_i+F{R*a*=Q9g(&M3mRuVx^`_fYAC6GZez(0t2*9O zVIK`7wd3FF>R5SE&32C{~;!_-fb8J!M5)CtB}72m$+;^W}w{(;`}A`*)_ zLdK8A4$tuR3|Ci=F+WdoPk<#kYL3JC3bf+hT&2L>Z!}nA<-rS=h~AauoOf_GbD`uQ zXRNHA9mYEITG?V;X!sNiEZ>+tJgKmj+uNd_Mls{t`VOU%5qLtB}akC`x6A2v}D<%rCaZ zjA6__F2-dy!t8GkT?q#=pV~Zg^xdpWP ztEo@vUdJ9Zt(JAAbCsxG5=kG`o*O@#Dv*v#Tw(tL>aaoTT(a&uzo#W*s zi1?~om`rx^N?+8{x3(Ax7nC$ue;HrzQ_8$%$iz(VW7!1$c)i#p8J~)vjh1^Z2b>nF z3s||#NmX7P!Q9L=C5`HZB=w}wjENH$6KY>I=^dJz)A$dJ0yJs^sa5NmEy4pj3XpW! zn-+6DPU)d>PFH30K(PDcK#Ob8xk6{hU1y7*2rTQIZcgBbje(}4kedlILCkjnnM%0z zp3*H#J)@iO@$4e1MWk1^D|n;a-NJZT%HEZw=#U6iFP^4_(^$##5rB>N znc|fpSG+#|L2&SRS+Dg*mnz~RMF&G?04`H~ci@dt!0=Ip8vJStoQzBlaADn>u$3~T z+~@fGWScD!-1tq92yh3Sawyzj0mZ2c@_bL@+a}9vO?NXA%%pqX0pgi@?o8(rH36ir zasKbTGVY@ww=<+}PZkNE;^y=r_}J+KFN9y4rgY3p9=Yy(T3Ah8Gse#1iMIPo2-XC36kH1_Sf$q*@i zL|*kJPH?i7Soe}^*V(A(veep+`S(GZw2{+)@hwThT zGLQJGobw0W(u0R9kJpdl)u6^RKjEE1u0{0^K^w{u2_X5waAQFr;gP|R!#r_mV8OPh5*oJ?!E?vqhQx=WvP+m#2z#f6UAUGf`#(ie){oehdv!w{n z-w;;7z-LW^^^R%kSfL6d^G?pRdJ*NUId?1>fI{K=u4OC{ImB2mhm>MzX(_3kkv!!s zQZRzpa#F6~ItV2_qyo~i3KJD=ih{#|^T!0HO()VQe0iE~<%zkZ+RS780ViHBtzqt@ zQ-eA{^cEVfw8R^DQ1~5w=OMm#uZczv+@%r@nH%{09=F1m($5669Uw)&>hZ)OBYxBk zFX$Ba?LTtsL>GKZH;nSPc#V2)jDQR+{)K?Fa4wr4EF(4X0)0Tat+7#{%l=zoULL`2 zxVAvKE4fXLX^ zsygceh5Po>ija1tBO#|^2!irqaY{qHh_(h$PjZtiQ922h81PK$66A@Jyz*LJZ*kC& zS2dz~l*T+$@lReg`o#R=Pzb`3+U5*N^CS#zxe(w6J&=@}!%S-rs02AgL4sse&XLD! zOVx5t@~RYU9fu~X>0$fk1?^1Zd$JZwngd)*bbRX>TN1U%ryNI6#rl!GwyELA1^w>k zn&tb)?^Jy=;PSbJT1g{}Xqb#++l17gh3q0c(^uIJ`cbS(D7JtkzE}6Li}%6wzl*FX zZ40vTcnLDA**CrahEgAEqgA{P8Abw_!g9@tYl0no%UnmwkMuQQna&+|zHgS%LwSHj za{tjtxH6|``-#*a2k(L*UFJS8;dd@<*c~S+&C_$PbX*>wq*H5%8nxP=mv+OBm7*NJ z>6I(l*Iw|5VgfF_P2u3C?Sd~E3f?8SeihvI%?pJ{emS+KydXLZ6Uw2&BG#Y!N2~3= z7m66G^6=Cg+L_8abgYIor<}) zL;EyG5^JLN*ILd5LA0$>Eg;@At@)IMA6ULTSqm#l$S&{aaGqC@i5L5o%&*B-- zu4laset03knvjo`j3eG1fAhU(pVq}RZJlJ$FnF@v|3n&CV?UUza&YSy(VV(q_c!$X zYL|O~Q!jT3+dS%TLh$sxxs2pvQk<%Ng?5%}Z^L{(_4DU)3lBm|IVEP(^(2-*+nhpL zuczbdg99jSAt80AL}RZEc#{aDeM2A!}2cyX=B68lgw~-m*0=Ty+ox?)d3pamiyZcBPYW5 zIl$@~n!Nl7VNu0Wy(>1NuHo*fmRThw0Ezp!THY_mjHp^r2ycZ-wB1B`j9NK{_VBN3 zBgv06V3pcNv=tfeLb8Og#i}xQqkuw_tT+3`hR16JM^3EM3MreDeNvBJUX;3>$n2Rt zCw`&Twf&g)OT`PT8@eW%5d`hoRP*{BGg4CBnO>D%kLp#m4%ex!7ngtSvK*Js)>N8>vvDazCx-^-U6Y+Y>jb8?)@v51@lGjgNxVYrACH8CQ+g5 zEqC)ck@65NJ*_K(&4gM_dmgID`3u_e1gamR1Q91cG)+{rb2y><*r@nzK*2<{!c(Cg z)U=9C{~E;GJ5O|N%;f|>r`mt4|MI0hs`AiI0tWGE!_;qV{bX2rc+)G5;>xYNPl`2yzhGI(+y!yt9yTo-?}zus-BiUxdo(!MF{o8(6kjNqhMxcHfl%KJ;oPZ zg8gqEMj%Sn=yMTgij_`|o`$Mdg0WvLh>H6I&j}nxUdg~QT z?Av~fdplKQsxe49_kFXyRFz8khNw*JZrxlDS3%2P0xMp_C>smAHS(tI9P`x`Mdce@ z>!aSfxBm3S9Ki_)XE<$gC?i%pjvaK)fmLso`(`5n1a}*W493J*&1~dlB8DH;fdKO# zi_qdo*o3&?|4a-2^V7#80y^O%xA7;E9zX0{+?Pc7$Nub)j|BF3(WYyG>(Thes|SJg zt#08`#-}xRK;B-Dd!5I9;B+?Qua3xdHFtR~i8=TZHS9 zGeYqFVNZ*5Hk_>7xAo2UBuZfSD{@~I_Vkoz;bsRep3{S>js7S`5SH? zeNeS{g3OlthMBbV5OJ^c~@kwvkXVsQ@dO`1eHcMP5wfzRK#8Y>uq~fWv684#6 zEG4esZb43JO*U>05Hf9iCkUkXIwl8?%_>)cZA~Y)Be(DR7~hw67<}22*^yf~fU{h$ z7yF>os|S;mc1ZqvppU;jTfN^MouQmccO8damHRaG5UOYBv9@0kcf(V?_e)Ye6XpfwUP zLbVjNOOV9gA+duPzfXVnp0?-SbMCp{`=12K=ly;^&+~rf>qR*b6RNnU64PH2T1hC9 z>B}2$avyGo--1^3CP-5Df*Pr9m$SN?V&?oU^Kbvmd(=-J?Q*_{u?%_%ZI8hVMvUKp z>|bcc0WB+c1*rpZ)GU~PDuI3Jn7(Sb&52f~;MjKYVsyV(&7AT69yE@>vJA-IH$^)4 z#akRu_*b$2fi}=ot8O#yk0*;M-&c1#5aW4d!Pro4{R+>*9YZZQdN6kfNZS3T<4Wtq z`$)1mCf=s_UA>7AE8Keny?RW1r?|@x4zxOO_?X;R;k9N@xh<{4xF)u6qR6Hfi+-rd z#8o(V%*eB%KIUE2%{eBEb0>=Px(gLCYc=FRT(ujPk)>aIaO%5PuJH5YKL4~ zU$gDo10ynux4xBU>Sf#+rSaFWz|=L6gUe|`r6sb6=XpMf2Wx2aSEtv2bZy3(2PRI} zswY>LjBdwT1tvIp)t({w9xdb;At06@U$0Q|$vUMkFGdM)p|6r}#VoY7jf?l@E88eF z^M8E}-&C4v`*sdq#D_Cc5r}@crIa1pbZsBSp3=Sha!zB0G3(0BK5BHc=UONyKDgmS zLj_Be52Tp61f*ZWB*>GljWcSd_tZ>$Dn>!Q{ocba2kHgd;5>!RdR@ufUgUrhZNBPr zrXyX0caygY8b9zsat5q*)W-s|^t|n5=?4N^?&Z0yX)1#3L|#my4XPzy_Z@LCkqfr^ z(dY`)SVVKlA0^RCmQ*4Du$3cOi!ZL523Jp``81RXgd`Q--~$?terzk4tB_qYLKm%C)8 zpvz*pJ`j#e2{9#hcO5~8LEQTEh1|IyX^TD`u8i>0hoXAnH-RLu5f2TC`LZ~Nr3y41 z=tEIrc_&(|33|^x3W~IXS<^$JBZH+&X!pW6%&*rZ-=|q6WV?fvtpC zmR_8_%25P43*dt|)~$HByJy(wP&w~ySBxjg8@!|n4IMEu$+d(*D}?R~e0@Em)g{~*tmM-s3bB5- z65drMY$}gpD+;t=d(e4kH@CoX-BePNw2x=$ZtjR?~jnTm!H8JVZ#7 zlm}UibbE12jt)~&p@xfxa6h|D{=ag+V4>MAo(D#d?iCJ)zp+}1QyW?g#(Po-`%f^Z z1xCUdPH-yR962Wd^wtKg@TfT(a@CO^W<2Hl{b2v?#JP-x0w4N+6gaG${eVWd*bdxv z^X7Qrqxm2MMa$JQtf=bW!n%&oB*cb9FEc$5ci^XcMQx(6x>~@mA@Y2D%v{6HSc8Jt z)1$t1r{tuPX(phs)!A~}OT26(AMU;+{zp+CTbkVYnS2-yP$YT8`sMr3mVL`YUy1}% z4>vA`i%=31noY<-wTGg_hSQI3x|zPD7~S;(?Xxw0%gO?BbjBKnE|)Lh1@!#y&OW8X z2MUUHA=o&HmRg&mFDk-gssp=s^Gm7E0Xs?9-*XvWGVQiM4gC3nb0v}ErQ5Q`=P)zT zAXCO^f+-emlv6Z!Ebu(MzW5=j`)I8V=(bwl>Xn9dqs~Xy)j0PrJ;>%38Zfq7QvKdU zmv&OR;?38*zE3%QC&3GWi=O?@mUiOxMvgVY$A>53s8r7o!oV?{!@urql8yR-b#mq& zE42xWu}i$XeUA`GfmS-S$WRCbutl{oz2b+W9;kkP+fHc4pauiU8A7gr^pF=tFh{lj ziipi20brW?T2&&t`1ByCAutkNmz`s(OA#{6RiywV!&Nu?o{=b zVa2P{eIu_|uhEj2%Cu=o{9b$nf=c+t$B$9Z`>?Nx#Gr;%JrmA496cY-YEy~#lChAY znW4H0=uvDBh+fsDyOr>ufohvQs+6X|k&eAwOTw`6MCtU)O1N}C9J#%p3x`PCteGE( z;I|1FoLviu;~vFo;Xt-lFv_RW>ZnclW--%*hT+skF1Q-LPE^&+FEHQBV?)hUxh)o? zXDcY}&DvjCZ6ZiXuI0ebKy>IGEE{y|Vk-hbgEU;a{Gs&i=Jpa_rbuWc`O&0{UWPTSFXt1c{MbGCo z=`e$WPj1!SL|++z$7g47LDDO(!jXVU^HwuW{YV*7i<#h-)O&iD-ANyuRnr>e(O>lD z)(ht0;5fsBOjqJAo?)R2=dE??OTSmp;(e{LS7XV>pY2E&Hh|E4iWTLq;>MI^Vmy0? z#QF`E`@TSN%|I^LQ|g_PKwyt5%bclIF2u0%JhtT&=+^p+LnN#z2Xu5QTJQ7&T$jIo zGAY_^16_hexG(W!dFH-*SAP?~`d;Wp#m=WdUp`oih(TtCcNx!D-B43RvH#cf%DRh| zZ!&#P>COgZg$?0}=3@Xg$m4P|`M+fs|7lE)i>TMDG?JEDLQqW647$P_Lvrg4?QgK@ zY8OTY=vuf@{6$qjLtn#c&oeuqy$5WbZh^qZK>DJD>OOwbDvpBK8HRxS=7&mKA>#ot z@t_J7Mt)H}!Is70>vEJLS(d}dWO#bmcvv^Cd*Z%iPL_d{8ah7KFy=ki4Bx!x7W_j9 zfMBdl?-wm6FHZ%@%FD+bpk$B2WIiQ}8ebC}AJ6dE81><{7QE5xFVqQ&^mcJ!QBHS( zf>~s%XYNZn7WZm9I#%&1fAD!Theo5t5L&M_)?J#T^nwK^t6S#_%*!8w z;i1>$Rog%TCDI^&lL}vun~CG@d7hfi2lw`wtwdu?g&<1^tyI1+((=VP5>HGQgMvx( zqk#zkWS@uoC^~^ZCfsUqak0OD_$~s081sW?mL#wl<{9Og(@$`|Frq1Q!kQ*fso6z= zRTxDe+l3Gv{G@wX?*Ik9+p!7q;>A&qNEGLS7dMz`75+JT&q) znewCuYF$n|rfl$9_o|0}phpREATnw0_LIS|-%OB66&=ML4zDMgwwM+rCyOJ?KT^y^ z28x}|pF8&;!$#pquJIFdUE$^Qv_2AgUzJ5bcBAmvidt0_G=k~df%G>Ut)9pz`WK(-`v)A6Z+X7q@o|xy_Ty*JHwmrhSxCUPyzvi zY&Yf3dDXy67<6%W2CQ^pV-91BLiplPqXVjDD63w8FEl`I9wDSQ0y>+BFs9;qlWN53 zK2C|7AU0A1iF_Y-&x*|xO}p^l4kzfJnDt9u<;zG9n^jXnNPh_XdMQ-SN0H6=+-3Ce zY$?rK-qi!bJzacJyfJ~YbGK+L1rNNJ>EU&jG zODZeyt+KwauTApw>~0onXZwr2uy4AO^avGsUObRrH6VpXj(`NmcX51ifF&a& z`p1;I;%E9ZuQ``2*X%Fw{U~4j-L5gldw(a!Vl^u zrWF;{sU>%JFuTvsTb-?r-~fTBfZ;H!E!3$C+Y%$rEW3#$*9~^hi=**Ap8bCmNN5Q5|&VYOF9F zu6Lg9@B0sRwY>Ix;oS43oSHQ8FFxvzDD>a;95#07Q|mOFw^_;++axC0s+)L?Tihqu z^BAz{`WhB;5^8<)-F-)-P3o23mDe@J1aaT*M|tp|flArS*_Y)#J$CLc_e;#P+|;65 zN12$2AJ9RMM=Iel;!6l00O3w^Fz)47<+mStyp;#C#VS&(;FJ$@K7C&;%q<#)%0A4V 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

;Oysp%Wq47RZM%+5e$tBa6WBz6nds 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 Date: Wed, 8 Apr 2020 15:14:07 +0200 Subject: [PATCH 09/81] skips 'Sorts by activated rules' (#62924) --- .../siem/cypress/integration/signal_detection_rules.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts index 1559285d508ed..2d2db9e70255b 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts @@ -32,7 +32,7 @@ describe('Signal detection rules', () => { esArchiverUnload('prebuilt_rules_loaded'); }); - it('Sorts by activated rules', () => { + it.skip('Sorts by activated rules', () => { loginAndWaitForPageWithoutDateRange(DETECTIONS); goToManageSignalDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); From 4d7cc6dfb787b10dd3c32b8f90b95bd948920076 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Wed, 8 Apr 2020 14:32:13 +0100 Subject: [PATCH 10/81] Removing unused import (#62917) Co-authored-by: Elastic Machine --- .../embeddable/public/lib/embeddables/error_embeddable.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx index 816001ba42ff1..715827a72c61b 100644 --- a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx @@ -20,8 +20,6 @@ import React from 'react'; import { ErrorEmbeddable } from './error_embeddable'; import { EmbeddableRoot } from './embeddable_root'; import { mount } from 'enzyme'; -// @ts-ignore -import { findTestSubject } from '@elastic/eui/lib/test'; test('ErrorEmbeddable renders an embeddable', async () => { const embeddable = new ErrorEmbeddable('some error occurred', { id: '123', title: 'Error' }); From 6b52ce7341e52bdb1cce4b1fb90181c03fc74f5e Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 8 Apr 2020 15:09:56 +0100 Subject: [PATCH 11/81] [ML] Adding configurable file size to file data visualizer (#62752) * [ML] Adding configurable file size to file data visualizer * updating translated strings --- .../ml/common/constants/file_datavisualizer.ts | 2 ++ x-pack/plugins/ml/common/types/ml_config.ts | 16 ++++++++++++++++ x-pack/plugins/ml/public/application/app.tsx | 7 ++++++- .../datavisualizer/datavisualizer_selector.tsx | 6 +++++- .../components/about_panel/welcome_content.tsx | 6 +++++- .../file_datavisualizer_view.js | 13 +++++++------ .../file_error_callouts.tsx | 7 +++---- .../file_based/components/utils/index.ts | 2 ++ .../file_based/components/utils/utils.ts | 15 +++++++++++++++ .../public/application/util/dependency_cache.ts | 11 +++++++++++ x-pack/plugins/ml/public/index.ts | 4 ++-- x-pack/plugins/ml/public/plugin.ts | 13 ++++++++++++- x-pack/plugins/ml/server/config.ts | 15 +++++++++++++++ x-pack/plugins/ml/server/index.ts | 2 ++ .../plugins/translations/translations/ja-JP.json | 2 -- .../plugins/translations/translations/zh-CN.json | 2 -- 16 files changed, 103 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/ml/common/types/ml_config.ts create mode 100644 x-pack/plugins/ml/server/config.ts diff --git a/x-pack/plugins/ml/common/constants/file_datavisualizer.ts b/x-pack/plugins/ml/common/constants/file_datavisualizer.ts index d72e4d63cc47e..81d51bfa25816 100644 --- a/x-pack/plugins/ml/common/constants/file_datavisualizer.ts +++ b/x-pack/plugins/ml/common/constants/file_datavisualizer.ts @@ -5,6 +5,8 @@ */ export const MAX_BYTES = 104857600; +export const ABSOLUTE_MAX_BYTES = MAX_BYTES * 5; +export const FILE_SIZE_DISPLAY_FORMAT = '0,0.[0] b'; // Value to use in the Elasticsearch index mapping meta data to identify the // index as having been created by the ML File Data Visualizer. diff --git a/x-pack/plugins/ml/common/types/ml_config.ts b/x-pack/plugins/ml/common/types/ml_config.ts new file mode 100644 index 0000000000000..8fd9fd22bad8a --- /dev/null +++ b/x-pack/plugins/ml/common/types/ml_config.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { MAX_BYTES } from '../constants/file_datavisualizer'; + +export const configSchema = schema.object({ + file_data_visualizer: schema.object({ + max_file_size_bytes: schema.number({ defaultValue: MAX_BYTES }), + }), +}); + +export type MlConfigType = TypeOf; diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index e9796fcbb0fe4..f1facd18b9da5 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -15,10 +15,14 @@ import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/p import { setDependencyCache, clearCache } from './util/dependency_cache'; import { setLicenseCache } from './license'; import { MlSetupDependencies, MlStartDependencies } from '../plugin'; +import { MlConfigType } from '../../common/types/ml_config'; import { MlRouter } from './routing'; -type MlDependencies = MlSetupDependencies & MlStartDependencies; +type MlDependencies = MlSetupDependencies & + MlStartDependencies & { + mlConfig: MlConfigType; + }; interface AppProps { coreStart: CoreStart; @@ -74,6 +78,7 @@ export const renderApp = ( http: coreStart.http, security: deps.security, urlGenerators: deps.share.urlGenerators, + mlConfig: deps.mlConfig, }); const mlLicense = setLicenseCache(deps.licensing); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index a8bb5a0a8fe10..2d6505f5ce1f7 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -26,6 +26,7 @@ import { isFullLicense } from '../license'; import { useTimefilter, useMlKibana } from '../contexts/kibana'; import { NavigationMenu } from '../components/navigation_menu'; +import { getMaxBytesFormatted } from './file_based/components/utils'; function startTrialDescription() { return ( @@ -59,6 +60,8 @@ export const DatavisualizerSelector: FC = () => { licenseManagement.enabled === true && isFullLicense() === false; + const maxFileSize = getMaxBytesFormatted(); + return ( @@ -102,7 +105,8 @@ export const DatavisualizerSelector: FC = () => { description={ } betaBadgeLabel={i18n.translate( diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/welcome_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/welcome_content.tsx index c56ab021e2f2f..49b2396aeca13 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/welcome_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/welcome_content.tsx @@ -19,6 +19,7 @@ import { } from '@elastic/eui'; import { ExperimentalBadge } from '../experimental_badge'; +import { getMaxBytesFormatted } from '../utils'; export const WelcomeContent: FC = () => { const toolTipContent = i18n.translate( @@ -28,6 +29,8 @@ export const WelcomeContent: FC = () => { } ); + const maxFileSize = getMaxBytesFormatted(); + return ( @@ -117,7 +120,8 @@ export const WelcomeContent: FC = () => {

diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js index 02f14a9e4553e..d1b615a878b2b 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js @@ -19,14 +19,15 @@ import { FileCouldNotBeRead, FileTooLarge } from './file_error_callouts'; import { EditFlyout } from '../edit_flyout'; import { ExplanationFlyout } from '../explanation_flyout'; import { ImportView } from '../import_view'; -import { MAX_BYTES } from '../../../../../../common/constants/file_datavisualizer'; import { + getMaxBytes, readFile, createUrlOverrides, processResults, reduceData, hasImportPermission, } from '../utils'; + import { MODE } from './constants'; const UPLOAD_SIZE_MB = 5; @@ -57,6 +58,7 @@ export class FileDataVisualizerView extends Component { this.overrides = {}; this.previousOverrides = {}; this.originalSettings = {}; + this.maxFileUploadBytes = getMaxBytes(); } async componentDidMount() { @@ -93,7 +95,7 @@ export class FileDataVisualizerView extends Component { }; async loadFile(file) { - if (file.size <= MAX_BYTES) { + if (file.size <= this.maxFileUploadBytes) { try { const fileContents = await readFile(file); const data = fileContents.data; @@ -105,7 +107,6 @@ export class FileDataVisualizerView extends Component { await this.loadSettings(data); } catch (error) { - console.error(error); this.setState({ loaded: false, loading: false, @@ -181,8 +182,6 @@ export class FileDataVisualizerView extends Component { fileCouldNotBeRead: isRetry, }); } catch (error) { - console.error(error); - this.setState({ results: undefined, explanation: undefined, @@ -287,7 +286,9 @@ export class FileDataVisualizerView extends Component { {loading && } - {fileTooLarge && } + {fileTooLarge && ( + + )} {fileCouldNotBeRead && loading === false && ( diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx index 3dbc20f16012b..7333c96a0d8ea 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx @@ -11,8 +11,7 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { ErrorResponse } from '../../../../../../common/types/errors'; - -const FILE_SIZE_DISPLAY_FORMAT = '0,0.[0] b'; +import { FILE_SIZE_DISPLAY_FORMAT } from '../../../../../../common/constants/file_datavisualizer'; interface FileTooLargeProps { fileSize: number; @@ -81,7 +80,7 @@ interface FileCouldNotBeReadProps { } export const FileCouldNotBeRead: FC = ({ error, loaded }) => { - const message = error.body.message; + const message = error?.body?.message || ''; return ( = ({ error, loaded }; export const Explanation: FC<{ error: ErrorResponse }> = ({ error }) => { - if (!error.body.attributes?.body?.error?.suppressed?.length) { + if (!error?.body?.attributes?.body?.error?.suppressed?.length) { return null; } const reason: string = error.body.attributes.body.error.suppressed[0].reason; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts index 6f670359586b7..0f0036a7c4616 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts @@ -10,4 +10,6 @@ export { processResults, readFile, reduceData, + getMaxBytes, + getMaxBytesFormatted, } from './utils'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts index 5048065ae60fa..0d2016b71ed83 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts @@ -5,8 +5,14 @@ */ import { isEqual } from 'lodash'; +import numeral from '@elastic/numeral'; import { ml } from '../../../../services/ml_api_service'; import { AnalysisResult, InputOverrides } from '../../../../../../common/types/file_datavisualizer'; +import { + ABSOLUTE_MAX_BYTES, + FILE_SIZE_DISPLAY_FORMAT, +} from '../../../../../../common/constants/file_datavisualizer'; +import { getMlConfig } from '../../../../util/dependency_cache'; const DEFAULT_LINES_TO_SAMPLE = 1000; @@ -54,6 +60,15 @@ export function reduceData(data: string, mb: number) { return data.length >= size ? data.slice(0, size) : data; } +export function getMaxBytes() { + const maxBytes = getMlConfig().file_data_visualizer.max_file_size_bytes; + return maxBytes < ABSOLUTE_MAX_BYTES ? maxBytes : ABSOLUTE_MAX_BYTES; +} + +export function getMaxBytesFormatted() { + return numeral(getMaxBytes()).format(FILE_SIZE_DISPLAY_FORMAT); +} + export function createUrlOverrides(overrides: InputOverrides, originalSettings: InputOverrides) { const formattedOverrides: InputOverrides = {}; for (const o in overrideDefaults) { diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index d5605d3bca65f..934a0a5e9ae3a 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -23,6 +23,7 @@ import { } from 'kibana/public'; import { SharePluginStart } from 'src/plugins/share/public'; import { SecurityPluginSetup } from '../../../../security/public'; +import { MlConfigType } from '../../../common/types/ml_config'; export interface DependencyCache { timefilter: DataPublicPluginSetup['query']['timefilter'] | null; @@ -42,6 +43,7 @@ export interface DependencyCache { security: SecurityPluginSetup | null; i18n: I18nStart | null; urlGenerators: SharePluginStart['urlGenerators'] | null; + mlConfig: MlConfigType | null; } const cache: DependencyCache = { @@ -62,6 +64,7 @@ const cache: DependencyCache = { security: null, i18n: null, urlGenerators: null, + mlConfig: null, }; export function setDependencyCache(deps: Partial) { @@ -82,6 +85,7 @@ export function setDependencyCache(deps: Partial) { cache.security = deps.security || null; cache.i18n = deps.i18n || null; cache.urlGenerators = deps.urlGenerators || null; + cache.mlConfig = deps.mlConfig || null; } export function getTimefilter() { @@ -202,6 +206,13 @@ export function getGetUrlGenerator() { return cache.urlGenerators.getUrlGenerator; } +export function getMlConfig() { + if (cache.mlConfig === null) { + throw new Error("mlConfig hasn't been initialized"); + } + return cache.mlConfig; +} + export function clearCache() { console.log('clearing dependency cache'); // eslint-disable-line no-console Object.keys(cache).forEach(k => { diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index 8070f94a1264d..4697496270edf 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializer } from 'kibana/public'; +import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; import './index.scss'; import { MlPlugin, @@ -19,6 +19,6 @@ export const plugin: PluginInitializer< MlPluginStart, MlSetupDependencies, MlStartDependencies -> = () => new MlPlugin(); +> = (context: PluginInitializerContext) => new MlPlugin(context); export { MlPluginSetup, MlPluginStart }; diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 2472c343d0510..b51be4d248683 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -5,7 +5,13 @@ */ import { i18n } from '@kbn/i18n'; -import { Plugin, CoreStart, CoreSetup, AppMountParameters } from 'kibana/public'; +import { + Plugin, + CoreStart, + CoreSetup, + AppMountParameters, + PluginInitializerContext, +} from 'kibana/public'; import { ManagementSetup } from 'src/plugins/management/public'; import { SharePluginStart } from 'src/plugins/share/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; @@ -19,6 +25,7 @@ import { LicenseManagementUIPluginSetup } from '../../license_management/public' import { setDependencyCache } from './application/util/dependency_cache'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; import { registerFeature } from './register_feature'; +import { MlConfigType } from '../common/types/ml_config'; export interface MlStartDependencies { data: DataPublicPluginStart; @@ -34,7 +41,10 @@ export interface MlSetupDependencies { } export class MlPlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + setup(core: CoreSetup, pluginsSetup: MlSetupDependencies) { + const mlConfig = this.initializerContext.config.get(); core.application.register({ id: PLUGIN_ID, title: i18n.translate('xpack.ml.plugin.title', { @@ -57,6 +67,7 @@ export class MlPlugin implements Plugin { usageCollection: pluginsSetup.usageCollection, licenseManagement: pluginsSetup.licenseManagement, home: pluginsSetup.home, + mlConfig, }, { element: params.element, diff --git a/x-pack/plugins/ml/server/config.ts b/x-pack/plugins/ml/server/config.ts new file mode 100644 index 0000000000000..7cef6f17bbefb --- /dev/null +++ b/x-pack/plugins/ml/server/config.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginConfigDescriptor } from 'kibana/server'; +import { MlConfigType, configSchema } from '../common/types/ml_config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + file_data_visualizer: true, + }, + schema: configSchema, +}; diff --git a/x-pack/plugins/ml/server/index.ts b/x-pack/plugins/ml/server/index.ts index 175c20bf49c94..6e638d647a387 100644 --- a/x-pack/plugins/ml/server/index.ts +++ b/x-pack/plugins/ml/server/index.ts @@ -9,3 +9,5 @@ import { MlServerPlugin } from './plugin'; export { MlPluginSetup, MlPluginStart } from './plugin'; export const plugin = (ctx: PluginInitializerContext) => new MlServerPlugin(ctx); + +export { config } from './config'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2ae29202ede43..d07026c0883b8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9732,7 +9732,6 @@ "xpack.ml.datavisualizer.selector.dataVisualizerTitle": "データビジュアライザー", "xpack.ml.datavisualizer.selector.experimentalBadgeLabel": "実験的", "xpack.ml.datavisualizer.selector.experimentalBadgeTooltipLabel": "実験的機能。フィードバックをお待ちしています。", - "xpack.ml.datavisualizer.selector.importDataDescription": "ログファイルからデータをインポートします。最大 100 MB のファイルをアップロードできます。", "xpack.ml.datavisualizer.selector.importDataTitle": "データのインポート", "xpack.ml.datavisualizer.selector.selectIndexButtonLabel": "インデックスを選択", "xpack.ml.datavisualizer.selector.selectIndexPatternDescription": "既存の Elasticsearch インデックスのデータを可視化します。", @@ -9983,7 +9982,6 @@ "xpack.ml.fileDatavisualizer.welcomeContent.logFilesWithCommonFormatDescription": "タイムスタンプの一般的フォーマットのログファイル", "xpack.ml.fileDatavisualizer.welcomeContent.newlineDelimitedJsonDescription": "改行区切りの JSON", "xpack.ml.fileDatavisualizer.welcomeContent.supportedFileFormatDescription": "ファイルデータビジュアライザーはこれらのファイル形式をサポートしています:", - "xpack.ml.fileDatavisualizer.welcomeContent.uploadedFilesAllowedSizeDescription": "最大 100 MB のファイルをアップロードできます。", "xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileDescription": "ファイルデータビジュアライザーは、ログファイルのフィールドとメトリックの理解に役立ちます。ファイルをアップロードして、データを分析し、 Elasticsearch インデックスにインポートするか選択できます。", "xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileTitle": "ログファイルのデータを可視化 {experimentalBadge}", "xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription": "実際値が通常値と同じ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7f5df15dec83a..7e64ff6301faf 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9735,7 +9735,6 @@ "xpack.ml.datavisualizer.selector.dataVisualizerTitle": "数据可视化工具", "xpack.ml.datavisualizer.selector.experimentalBadgeLabel": "实验性", "xpack.ml.datavisualizer.selector.experimentalBadgeTooltipLabel": "实验功能。我们很乐意听取您的反馈意见。", - "xpack.ml.datavisualizer.selector.importDataDescription": "从日志文件导入数据。您可以上传最大 100 MB 的文件。", "xpack.ml.datavisualizer.selector.importDataTitle": "导入数据", "xpack.ml.datavisualizer.selector.selectIndexButtonLabel": "选择索引", "xpack.ml.datavisualizer.selector.selectIndexPatternDescription": "可视化现有 Elasticsearch 索引中的数据。", @@ -9986,7 +9985,6 @@ "xpack.ml.fileDatavisualizer.welcomeContent.logFilesWithCommonFormatDescription": "具有时间戳通用格式的日志文件", "xpack.ml.fileDatavisualizer.welcomeContent.newlineDelimitedJsonDescription": "换行符分隔的 JSON", "xpack.ml.fileDatavisualizer.welcomeContent.supportedFileFormatDescription": "File Data Visualizer 支持以下文件格式:", - "xpack.ml.fileDatavisualizer.welcomeContent.uploadedFilesAllowedSizeDescription": "您可以上传最大 100 MB 的文件。", "xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileDescription": "File Data Visualizer 可帮助您理解日志文件中的字段和指标。上传文件、分析文件数据,然后选择是否将数据导入 Elasticsearch 索引。", "xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileTitle": "可视化来自日志文件的数据 {experimentalBadge}", "xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription": "实际上与典型模式相同", From 67536e4b3cda294dae60e4f5e013e3afcfc4d1fd Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Wed, 8 Apr 2020 08:12:37 -0600 Subject: [PATCH 12/81] =?UTF-8?q?Fix=20issue=20with=20license=20not=20gett?= =?UTF-8?q?ing=20obtained=20&=20passed=20to=20server=E2=80=A6=20(#62883)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- x-pack/legacy/plugins/tilemap/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/tilemap/index.js b/x-pack/legacy/plugins/tilemap/index.js index 767a0fe72985e..d4105519ee0a7 100644 --- a/x-pack/legacy/plugins/tilemap/index.js +++ b/x-pack/legacy/plugins/tilemap/index.js @@ -15,7 +15,7 @@ export const tilemap = kibana => { require: ['xpack_main', 'vis_type_vislib'], publicDir: resolve(__dirname, 'public'), uiExports: { - visTypeEnhancers: ['plugins/tilemap/vis_type_enhancers/update_tilemap_settings'], + hacks: ['plugins/tilemap/vis_type_enhancers/update_tilemap_settings'], }, init: function(server) { const thisPlugin = this; From d94d7cc7195870c9c7379062684c0adbf2a5e73b Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Wed, 8 Apr 2020 09:20:25 -0500 Subject: [PATCH 13/81] [Canvas] Fix Canvas-specific storybook after new platform changes (#61876) * Fix Canvas storybook webpack config * Temporarily disable workpad export example * Mock out lib/notify and download_workpad Co-authored-by: Elastic Machine --- .../canvas/.storybook/webpack.config.js | 27 ++++++++++++++++--- .../canvas/tasks/mocks/downloadWorkpad.js | 13 +++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 x-pack/legacy/plugins/canvas/tasks/mocks/downloadWorkpad.js diff --git a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js index 85cb6d45c595d..cc74faeac6a96 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js +++ b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js @@ -6,6 +6,7 @@ const path = require('path'); const webpack = require('webpack'); +const { stringifyRequest } = require('loader-utils'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { DLL_OUTPUT, KIBANA_ROOT } = require('./constants'); @@ -73,7 +74,20 @@ module.exports = async ({ config }) => { path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), }, }, - { loader: 'sass-loader' }, + { + loader: 'sass-loader', + options: { + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss') + )};\n`; + }, + sassOptions: { + includePaths: [path.resolve(KIBANA_ROOT, 'node_modules')], + }, + }, + }, ], }); @@ -86,8 +100,9 @@ module.exports = async ({ config }) => { loader: 'css-loader', options: { importLoaders: 2, - modules: true, - localIdentName: '[name]__[local]___[hash:base64:5]', + modules: { + localIdentName: '[name]__[local]___[hash:base64:5]', + }, }, }, { @@ -159,7 +174,11 @@ module.exports = async ({ config }) => { // what require() calls it will execute within the bundle JSON.stringify({ type, modules: extensions[type] || [] }), ].join(''); - }) + }), + + // Mock out libs used by a few componets to avoid loading in kibana_legacy and platform + new webpack.NormalModuleReplacementPlugin(/lib\/notify/, path.resolve(__dirname, '../tasks/mocks/uiNotify')), + new webpack.NormalModuleReplacementPlugin(/lib\/download_workpad/, path.resolve(__dirname, '../tasks/mocks/downloadWorkpad')), ); // Tell Webpack about relevant extensions diff --git a/x-pack/legacy/plugins/canvas/tasks/mocks/downloadWorkpad.js b/x-pack/legacy/plugins/canvas/tasks/mocks/downloadWorkpad.js new file mode 100644 index 0000000000000..3571448c11aa9 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/tasks/mocks/downloadWorkpad.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export const downloadWorkpad = async workpadId => console.log(`Download workpad ${workpadId}`); + +export const downloadRenderedWorkpad = async renderedWorkpad => + console.log(`Download workpad ${renderedWorkpad.id}`); + +export const downloadRuntime = async basePath => console.log(`Download run time at ${basePath}`); + +export const downloadZippedRuntime = async data => console.log(`Downloading data ${data}`); From 1af82c709fe403cb6a3dccda5e29b0d4589a433f Mon Sep 17 00:00:00 2001 From: eyalkoren <41850454+eyalkoren@users.noreply.github.com> Date: Wed, 8 Apr 2020 17:34:04 +0300 Subject: [PATCH 14/81] [APM] Agent remote configuration: general settings descriptions (#62276) * Updates general remote config descriptions I removed `log_level` from here because it seems it doesn't fit at least the Java, Go and Node agents - see #61821, and it is already deactivated for most others (this doesn't have to be included in this PR though). * Update general_settings.ts * Restore log_level definition * Remove extra spaces Co-authored-by: Nathan L Smith --- .../setting_definitions/general_settings.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index f1d11c5c70c2b..e73aed35e87f9 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -63,7 +63,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.captureBody.description', { defaultMessage: - 'For transactions that are HTTP requests, the agent can optionally capture the request body (e.g. POST variables).' + 'For transactions that are HTTP requests, the agent can optionally capture the request body (e.g. POST variables).\nFor transactions that are initiated by receiving a message from a message broker, the agent can capture the textual message body.' } ), options: [ @@ -87,7 +87,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.captureHeaders.description', { defaultMessage: - 'If set to `true`, the agent will capture request and response headers, including cookies.\n\nNOTE: Setting this to `false` reduces network bandwidth, disk space and object allocations.' + 'If set to `true`, the agent will capture HTTP request and response headers (including cookies), as well as message headers/properties when using messaging frameworks (like Kafka).\n\nNOTE: Setting this to `false` reduces network bandwidth, disk space and object allocations.' } ), excludeAgents: ['js-base', 'rum-js', 'nodejs'] @@ -117,7 +117,7 @@ export const generalSettings: RawSettingDefinition[] = [ }), description: i18n.translate('xpack.apm.agentConfig.recording.description', { defaultMessage: - 'When recording, the agent instruments incoming HTTP requests, tracks errors, and collects and sends metrics. When inactive, the agent works as a noop, not collecting data and not communicating with the APM Server except for polling for updated configuration. As this is a reversible switch, agent threads are not being killed when inactivated, but they will be mostly idle in this state, so the overhead should be negligible. You can use this setting to dynamically control whether Elastic APM is enabled or disabled.' + 'When recording, the agent instruments incoming HTTP requests, tracks errors, and collects and sends metrics. When set to non-recording, the agent works as a noop, not collecting data and not communicating with the APM Server except for polling for updated configuration. As this is a reversible switch, agent threads are not being killed when set to non-recording, but they will be mostly idle in this state, so the overhead should be negligible. You can use this setting to dynamically control whether Elastic APM is enabled or disabled.' }), excludeAgents: ['nodejs'] }, @@ -215,7 +215,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.transactionSampleRate.description', { defaultMessage: - 'By default, the agent will sample every transaction (e.g. request to your service). To reduce overhead and storage requirements, you can set the sample rate to a value between 0.0 and 1.0. We still record overall time and the result for unsampled transactions, but no context information, labels, or spans.' + 'By default, the agent will sample every transaction (e.g. request to your service). To reduce overhead and storage requirements, you can set the sample rate to a value between 0.0 and 1.0. We still record overall time and the result for unsampled transactions, but not context information, labels, or spans.' } ) } From 5d34697ea57f444ffa7ba5b1ff2e00f1e85ee0e1 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 8 Apr 2020 10:46:06 -0400 Subject: [PATCH 15/81] [SIEM][Detection Engine] - Update list values in REST interfaces (#62320) Summary - #60022 - Follow up on #60171 - Modifies boolean filters to enum of "included" and "excluded" - Adds operator types of enum "match", "match_all", "list", and "exists" - Adds values properties to include those for "list" - DOES NOT FILTER ON THE VALUES JUST YET (That will be a follow on PR) --- .../routes/__mocks__/request_responses.ts | 28 ++-- .../routes/__mocks__/utils.ts | 28 ++-- .../routes/rules/validate.test.ts | 28 ++-- .../add_prepackaged_rules_schema.test.ts | 28 ++-- .../schemas/create_rules_schema.test.ts | 28 ++-- .../schemas/import_rules_schema.test.ts | 28 ++-- .../routes/schemas/patch_rules_schema.test.ts | 53 ++++--- .../schemas/response/__mocks__/utils.ts | 28 ++-- .../routes/schemas/response/schemas.ts | 37 +++-- .../routes/schemas/schemas.ts | 30 +++- .../schemas/types/lists_default_array.test.ts | 131 ++++++++++++++++-- .../schemas/types/lists_default_array.ts | 8 +- .../schemas/update_rules_schema.test.ts | 25 ++-- .../rules/get_export_all.test.ts | 28 ++-- .../rules/get_export_by_object_ids.test.ts | 56 +++++--- .../scripts/rules/patches/update_list.json | 27 ++-- .../rules/queries/query_with_list.json | 32 +++-- .../scripts/rules/updates/update_list.json | 27 ++-- .../signals/__mocks__/es_results.ts | 28 ++-- .../signals/build_bulk_body.test.ts | 112 +++++++++------ .../signals/build_rule.test.ts | 84 ++++++----- 21 files changed, 578 insertions(+), 296 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 3b24b722f7356..77c1641d073c6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -450,25 +450,31 @@ export const getResult = (): RuleAlertType => ({ lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts index 63fe51f2c81cd..c929b0718207d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -141,25 +141,31 @@ export const getOutputRuleAlertForRest = (): Omit< lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts index 77e05796fbcbe..7537401e5a366 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts @@ -74,25 +74,31 @@ export const ruleOutput: RulesSchema = { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index b10627d151fa2..8c741c937bf15 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -1561,25 +1561,31 @@ describe('add prepackaged rules schema', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index 08bd01ee9a1a0..e56e8e5fe34d3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -1526,25 +1526,31 @@ describe('create rules schema', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts index c8e5bb981f921..40f7b19ea12b3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -1747,25 +1747,31 @@ describe('import rules schema', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts index 45b5028f392b9..e01a8f40fcea4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts @@ -1229,25 +1229,31 @@ describe('patch rules schema', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -1263,25 +1269,28 @@ describe('patch rules schema', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts index 46cd1b653b5b4..d5ea950d163f5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts @@ -66,25 +66,31 @@ export const getBaseResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesS lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts index 538c8f754fd6e..faae1dde83545 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts @@ -154,13 +154,34 @@ export const note = t.string; // NOTE: Experimental list support not being shipped currently and behind a feature flag // TODO: Remove this comment once we lists have passed testing and is ready for the release -export const boolean_operator = t.keyof({ and: null, 'and not': null }); -export const list_type = t.keyof({ value: null }); // TODO: (LIST-FEATURE) Eventually this can include "list" when we support lists CRUD -export const list_value = t.exact(t.type({ name: t.string, type: list_type })); +export const list_field = t.string; +export const list_values_operator = t.keyof({ included: null, excluded: null }); +export const list_values_type = t.keyof({ match: null, match_all: null, list: null, exists: null }); +export const list_values = t.exact( + t.intersection([ + t.type({ + name: t.string, + }), + t.partial({ + id: t.string, + description: t.string, + created_at, + }), + ]) +); export const list = t.exact( - t.type({ - field: t.string, - boolean_operator, - values: t.array(list_value), - }) + t.intersection([ + t.type({ + field: t.string, + values_operator: list_values_operator, + values_type: list_values_type, + }), + t.partial({ values: t.array(list_values) }), + ]) ); +export const list_and = t.intersection([ + list, + t.partial({ + and: t.array(list), + }), +]); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts index 16e419f389f09..c17ae8466a86c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts @@ -126,12 +126,28 @@ export const note = Joi.string(); // NOTE: Experimental list support not being shipped currently and behind a feature flag // TODO: (LIST-FEATURE) Remove this comment once we lists have passed testing and is ready for the release -export const boolean_operator = Joi.string().valid('and', 'and not'); -export const list_type = Joi.string().valid('value'); // TODO: (LIST-FEATURE) Eventually this can be "list" when we support list types -export const list_value = Joi.object({ name: Joi.string().required(), type: list_type.required() }); +export const list_field = Joi.string(); +export const list_values_operator = Joi.string().valid(['included', 'excluded']); +export const list_values_types = Joi.string().valid(['match', 'match_all', 'list', 'exists']); +export const list_values = Joi.object({ + name: Joi.string().required(), + id: Joi.string(), + description: Joi.string(), + created_at, +}); export const list = Joi.object({ - field: Joi.string().required(), - boolean_operator: boolean_operator.required(), - values: Joi.array().items(list_value), + field: list_field.required(), + values_operator: list_values_operator.required(), + values_type: list_values_types.required(), + values: Joi.when('values_type', { + is: 'exists', + then: Joi.forbidden(), + otherwise: Joi.array() + .items(list_values) + .required(), + }), +}); +export const list_and = Joi.object({ + and: Joi.array().items(list), }); -export const lists = Joi.array().items(list); +export const lists = Joi.array().items(list.concat(list_and)); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts index 14df1c3d8cd55..e8a9c7b0886a1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts @@ -23,25 +23,79 @@ describe('lists_default_array', () => { const payload = [ { field: 'source.ip', - boolean_operator: 'and', + values_operator: 'included', + values_type: 'exists', + }, + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'match', values: [ { - name: '127.0.0.1', - type: 'value', + name: 'rock01', + }, + ], + and: [ + { + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array of lists that includes a values_operator other than included or excluded', () => { + const payload = [ + { + field: 'source.ip', + values_operator: 'included', + values_type: 'exists', + }, + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'exists', + }, + { + field: 'host.hostname', + values_operator: 'jibber jabber', + values_type: 'exists', + }, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "jibber jabber" supplied to "values_operator"', + ]); + expect(message.schema).toEqual({}); + }); + + // TODO - this scenario should never come up, as the values key is forbidden when values_type is "exists" in the incoming schema - need to find a good way to do this in io-ts + test('it will validate an array of lists that includes "values" when "values_type" is "exists"', () => { + const payload = [ { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'exists', values: [ { - name: 'rock01', - type: 'value', - }, - { - name: 'mothra', - type: 'value', + name: '127.0.0.1', }, ], }, @@ -53,15 +107,63 @@ describe('lists_default_array', () => { expect(message.schema).toEqual(payload); }); + // TODO - this scenario should never come up, as the values key is required when values_type is "match" in the incoming schema - need to find a good way to do this in io-ts + test('it will validate an array of lists that does not include "values" when "values_type" is "match"', () => { + const payload = [ + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'match', + }, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + // TODO - this scenario should never come up, as the values key is required when values_type is "match_all" in the incoming schema - need to find a good way to do this in io-ts + test('it will validate an array of lists that does not include "values" when "values_type" is "match_all"', () => { + const payload = [ + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'match_all', + }, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + // TODO - this scenario should never come up, as the values key is required when values_type is "list" in the incoming schema - need to find a good way to do this in io-ts + test('it should not validate an array of lists that does not include "values" when "values_type" is "list"', () => { + const payload = [ + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'list', + }, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + test('it should not validate an array with a number', () => { const payload = [ { field: 'source.ip', - boolean_operator: 'and', + values_operator: 'included', + values_type: 'exists', values: [ { name: '127.0.0.1', - type: 'value', }, ], }, @@ -70,7 +172,10 @@ describe('lists_default_array', () => { const decoded = ListsDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to ""', + 'Invalid value "5" supplied to ""', + ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts index 0e0944a11b416..85a38e296494a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts @@ -7,10 +7,10 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { list } from '../response/schemas'; +import { list_and as listAnd } from '../response/schemas'; export type ListsDefaultArrayC = t.Type; -type List = t.TypeOf; +type List = t.TypeOf; /** * Types the ListsDefaultArray as: @@ -18,9 +18,9 @@ type List = t.TypeOf; */ export const ListsDefaultArray: ListsDefaultArrayC = new t.Type( 'listsWithDefaultArray', - t.array(list).is, + t.array(listAnd).is, (input): Either => - input == null ? t.success([]) : t.array(list).decode(input), + input == null ? t.success([]) : t.array(listAnd).decode(input), t.identity ); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index 6f6beea7fa5fb..e8f9aad620ca0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -1552,25 +1552,28 @@ describe('create rules schema', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index ca6fb15e1fad9..dd004e3685b1d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -82,25 +82,31 @@ describe('getExportAll', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 175c906f7996c..715cb23e8444a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -90,25 +90,31 @@ describe('get_export_by_object_ids', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -212,25 +218,31 @@ describe('get_export_by_object_ids', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json index 8c86f4c85af1d..4db8724db4e13 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json @@ -3,21 +3,28 @@ "lists": [ { "field": "source.ip", - "boolean_operator": "and", - "values": [ - { - "name": "127.0.0.1", - "type": "value" - } - ] + "values_operator": "excluded", + "values_type": "exists" }, { "field": "host.name", - "boolean_operator": "and not", + "values_operator": "included", + "values_type": "match", "values": [ { - "name": "rock01", - "type": "value" + "name": "rock01" + } + ], + "and": [ + { + "field": "host.id", + "values_operator": "included", + "values_type": "match_all", + "values": [ + { + "name": "123456" + } + ] } ] } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json index f6856eec59966..997d03369a699 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json @@ -9,25 +9,31 @@ "lists": [ { "field": "source.ip", - "boolean_operator": "and", - "values": [ - { - "name": "127.0.0.1", - "type": "value" - } - ] + "values_operator": "included", + "values_type": "exists" }, { "field": "host.name", - "boolean_operator": "and not", + "values_operator": "excluded", + "values_type": "match", "values": [ { - "name": "rock01", - "type": "value" - }, + "name": "rock01" + } + ], + "and": [ { - "name": "mothra", - "type": "value" + "field": "host.id", + "values_operator": "included", + "values_type": "match_all", + "values": [ + { + "name": "123" + }, + { + "name": "678" + } + ] } ] } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json index 6704c9676fa56..66b198974f574 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json @@ -9,21 +9,28 @@ "lists": [ { "field": "source.ip", - "boolean_operator": "and", - "values": [ - { - "name": "127.0.0.1", - "type": "value" - } - ] + "values_operator": "excluded", + "values_type": "exists" }, { "field": "host.name", - "boolean_operator": "and not", + "values_operator": "included", + "values_type": "match", "values": [ { - "name": "rock01", - "type": "value" + "name": "rock01" + } + ], + "and": [ + { + "field": "host.id", + "values_operator": "included", + "values_type": "match_all", + "values": [ + { + "name": "123456" + } + ] } ] } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 6d7d7e93d7e6e..7a211c5631da6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -47,25 +47,31 @@ export const sampleRuleAlertParams = ( lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts index f2c2b99bdac8c..f1729e35ce1f0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -93,25 +93,31 @@ describe('buildBulkBody', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -213,25 +219,31 @@ describe('buildBulkBody', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -331,25 +343,31 @@ describe('buildBulkBody', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -442,25 +460,31 @@ describe('buildBulkBody', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts index e360ceaf02f4d..e5183ed4df7bd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts @@ -82,25 +82,31 @@ describe('buildRule', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -159,25 +165,31 @@ describe('buildRule', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, @@ -235,25 +247,31 @@ describe('buildRule', () => { lists: [ { field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], + values_operator: 'included', + values_type: 'exists', }, { field: 'host.name', - boolean_operator: 'and not', + values_operator: 'excluded', + values_type: 'match', values: [ { name: 'rock01', - type: 'value', }, + ], + and: [ { - name: 'mothra', - type: 'value', + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], }, ], }, From 90e6f2ca6de48858e9faa8a802905abf3aa23e8f Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Wed, 8 Apr 2020 16:57:23 +0200 Subject: [PATCH 16/81] fixing multiple metrics (#62929) --- .../options/metrics_axes/__snapshots__/index.test.tsx.snap | 3 +++ .../public/components/options/metrics_axes/index.test.tsx | 1 + .../public/components/options/metrics_axes/index.tsx | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap index 442bc826d51fc..09e0753d592e5 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap @@ -54,6 +54,7 @@ exports[`MetricsAxisOptions component should init with the default set of props } vis={ Object { + "serialize": [MockFunction], "setState": [MockFunction], "type": Object { "schemas": Object { @@ -126,6 +127,7 @@ exports[`MetricsAxisOptions component should init with the default set of props } vis={ Object { + "serialize": [MockFunction], "setState": [MockFunction], "type": Object { "schemas": Object { @@ -169,6 +171,7 @@ exports[`MetricsAxisOptions component should init with the default set of props setCategoryAxis={[Function]} vis={ Object { + "serialize": [MockFunction], "setState": [MockFunction], "type": Object { "schemas": Object { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx index a3f150e718817..524792d1460fe 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx @@ -95,6 +95,7 @@ describe('MetricsAxisOptions component', () => { schemas: { metrics: [{ name: 'metric' }] }, }, setState: jest.fn(), + serialize: jest.fn(), }, stateParams: { valueAxes: [axis], diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx index c7b4562b1087e..114305d653dd1 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx @@ -299,7 +299,7 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) }, [stateParams.seriesParams]); useEffect(() => { - vis.setState({ type: visType } as any); + vis.setState({ ...vis.serialize(), type: visType }); }, [vis, visType]); return isTabSelected ? ( From 202366310483f51f614b3b1f2001917547803e2b Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 8 Apr 2020 10:03:37 -0600 Subject: [PATCH 17/81] [Maps] Add date-fields to metrics selection (#62629) Co-authored-by: Elastic Machine --- x-pack/plugins/maps/public/components/metric_editor.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/public/components/metric_editor.js b/x-pack/plugins/maps/public/components/metric_editor.js index 530f402592b2b..d1affe2f42190 100644 --- a/x-pack/plugins/maps/public/components/metric_editor.js +++ b/x-pack/plugins/maps/public/components/metric_editor.js @@ -24,8 +24,13 @@ function filterFieldsForAgg(fields, aggType) { return getTermsFields(fields); } + const metricAggFieldTypes = ['number']; + if (aggType !== AGG_TYPE.SUM) { + metricAggFieldTypes.push('date'); + } + return fields.filter(field => { - return field.aggregatable && field.type === 'number'; + return field.aggregatable && metricAggFieldTypes.includes(field.type); }); } From 3457dde6f55c606a3f693ef75fe237ab07b56904 Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Wed, 8 Apr 2020 12:10:00 -0400 Subject: [PATCH 18/81] [Endpoint] EMT-146: add host status info to the metadata API response (#62876) [Endpoint] EMT-146: add host status info to the metadata API response --- x-pack/plugins/endpoint/common/types.ts | 28 ++++++++++++++++++- .../endpoint/store/hosts/action.ts | 4 +-- .../endpoint/store/hosts/index.test.ts | 2 +- .../endpoint/store/hosts/middleware.test.ts | 2 +- .../store/hosts/mock_host_result_list.ts | 7 +++-- .../endpoint/store/hosts/reducer.ts | 4 +-- .../server/routes/alerts/details/handlers.ts | 2 +- .../endpoint/server/routes/metadata/index.ts | 15 +++++++--- .../server/routes/metadata/metadata.test.ts | 9 +++--- .../api_integration/apis/endpoint/metadata.ts | 17 +++++------ 10 files changed, 64 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index e8e1281a88925..a614526d92a3f 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -86,7 +86,7 @@ export interface AlertResultList { export interface HostResultList { /* the hosts restricted by the page size */ - hosts: HostMetadata[]; + hosts: HostInfo[]; /* the total number of unique hosts in the index */ total: number; /* the page size requested */ @@ -252,6 +252,32 @@ export type AlertData = AlertEvent & AlertMetadata; export type AlertDetails = AlertData & AlertState; +/** + * The status of the host + */ +export enum HostStatus { + /** + * Default state of the host when no host information is present or host information cannot + * be retrieved. e.g. API error + */ + ERROR = 'error', + + /** + * Host is online as indicated by its checkin status during the last checkin window + */ + ONLINE = 'online', + + /** + * Host is offline as indicated by its checkin status during the last checkin window + */ + OFFLINE = 'offline', +} + +export type HostInfo = Immutable<{ + metadata: HostMetadata; + host_status: HostStatus; +}>; + export type HostMetadata = Immutable<{ '@timestamp': number; event: { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts index dee35aa3b895a..4dafa68ddb647 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts @@ -5,7 +5,7 @@ */ import { HostListPagination, ServerApiError } from '../../types'; -import { HostResultList, HostMetadata } from '../../../../../common/types'; +import { HostResultList, HostInfo } from '../../../../../common/types'; interface ServerReturnedHostList { type: 'serverReturnedHostList'; @@ -14,7 +14,7 @@ interface ServerReturnedHostList { interface ServerReturnedHostDetails { type: 'serverReturnedHostDetails'; - payload: HostMetadata; + payload: HostInfo; } interface ServerFailedToReturnHostDetails { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts index 9aff66cdfb75e..6148934343635 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts @@ -52,7 +52,7 @@ describe('HostList store concerns', () => { }); const currentState = store.getState(); - expect(currentState.hosts).toEqual(payload.hosts); + expect(currentState.hosts).toEqual(payload.hosts.map(hostInfo => hostInfo.metadata)); expect(currentState.pageSize).toEqual(payload.request_page_size); expect(currentState.pageIndex).toEqual(payload.request_page_index); expect(currentState.total).toEqual(payload.total); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts index a1973a38b6534..8c8578426aa29 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts @@ -58,6 +58,6 @@ describe('host list middleware', () => { paging_properties: [{ page_index: 0 }, { page_size: 10 }], }), }); - expect(listData(getState())).toEqual(apiResponse.hosts); + expect(listData(getState())).toEqual(apiResponse.hosts.map(hostInfo => hostInfo.metadata)); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts index db39ecf448312..d4c2602e34387 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostResultList } from '../../../../../common/types'; +import { HostResultList, HostStatus } from '../../../../../common/types'; import { EndpointDocGenerator } from '../../../../../common/generate_data'; export const mockHostResultList: (options?: { @@ -27,7 +27,10 @@ export const mockHostResultList: (options?: { const hosts = []; for (let index = 0; index < actualCountToReturn; index++) { const generator = new EndpointDocGenerator('seed'); - hosts.push(generator.generateHostMetadata()); + hosts.push({ + metadata: generator.generateHostMetadata(), + host_status: HostStatus.ERROR, + }); } const mock: HostResultList = { hosts, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts index fd70317a9f37e..ad6741dab7be7 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts @@ -34,7 +34,7 @@ export const hostListReducer: Reducer = ( } = action.payload; return { ...state, - hosts, + hosts: hosts.map(hostInfo => hostInfo.metadata), total, pageSize, pageIndex, @@ -43,7 +43,7 @@ export const hostListReducer: Reducer = ( } else if (action.type === 'serverReturnedHostDetails') { return { ...state, - details: action.payload, + details: action.payload.metadata, }; } else if (action.type === 'serverFailedToReturnHostDetails') { return { diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts index 725e362f91ec7..0f32deb4fad9b 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts @@ -41,7 +41,7 @@ export const alertDetailsHandlerWrapper = function( id: response._id, ...response._source, state: { - host_metadata: currentHostInfo, + host_metadata: currentHostInfo?.metadata, }, next: await pagination.getNextUrl(), prev: await pagination.getPrevUrl(), diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts index ef01db9af98c4..450469914bc50 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts @@ -8,7 +8,7 @@ import { IRouter, RequestHandlerContext } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; -import { HostMetadata, HostResultList } from '../../../common/types'; +import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types'; import { EndpointAppContext } from '../../types'; import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; @@ -87,7 +87,7 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp export async function getHostData( context: RequestHandlerContext, id: string -): Promise { +): Promise { const query = getESQueryHostMetadataByID(id); const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( 'search', @@ -98,7 +98,7 @@ export async function getHostData( return undefined; } - return response.hits.hits[0]._source; + return enrichHostMetadata(response.hits.hits[0]._source); } function mapToHostResultList( @@ -113,7 +113,7 @@ function mapToHostResultList( hosts: searchResponse.hits.hits .map(response => response.inner_hits.most_recent.hits.hits) .flatMap(data => data as HitSource) - .map(entry => entry._source), + .map(entry => enrichHostMetadata(entry._source)), total: totalNumberOfHosts, }; } else { @@ -125,3 +125,10 @@ function mapToHostResultList( }; } } + +function enrichHostMetadata(hostMetadata: HostMetadata): HostInfo { + return { + metadata: hostMetadata, + host_status: HostStatus.ERROR, + }; +} diff --git a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts index e0fd11e737e7d..9bd251735cc04 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts @@ -18,7 +18,7 @@ import { httpServiceMock, loggingServiceMock, } from '../../../../../../src/core/server/mocks'; -import { HostMetadata, HostResultList } from '../../../common/types'; +import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types'; import { SearchResponse } from 'elasticsearch'; import { EndpointConfigSchema } from '../../config'; import * as data from '../../test_data/all_metadata_data.json'; @@ -230,7 +230,7 @@ describe('test endpoint route', () => { expect(message).toEqual('Endpoint Not Found'); }); - it('should return a single endpoint', async () => { + it('should return a single endpoint with status error', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: (data as any).hits.hits[0]._id }, }); @@ -257,8 +257,9 @@ describe('test endpoint route', () => { expect(mockScopedClient.callAsCurrentUser).toBeCalled(); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); - const result = mockResponse.ok.mock.calls[0][0]?.body as HostMetadata; - expect(result).toHaveProperty('endpoint'); + const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; + expect(result).toHaveProperty('metadata.endpoint'); + expect(result.host_status).toEqual(HostStatus.ERROR); }); }); }); diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index ad495d4505404..887be6b85b100 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -139,7 +139,7 @@ export default function({ getService }: FtrProviderContext) { .expect(200); expect(body.total).to.eql(2); const resultIps: string[] = [].concat( - ...body.hosts.map((metadata: Record) => metadata.host.ip) + ...body.hosts.map((hostInfo: Record) => hostInfo.metadata.host.ip) ); expect(resultIps).to.eql([ '10.192.213.130', @@ -164,7 +164,7 @@ export default function({ getService }: FtrProviderContext) { .expect(200); expect(body.total).to.eql(2); const resultOsVariantValue: Set = new Set( - body.hosts.map((metadata: Record) => metadata.host.os.variant) + body.hosts.map((hostInfo: Record) => hostInfo.metadata.host.os.variant) ); expect(Array.from(resultOsVariantValue)).to.eql([variantValue]); expect(body.hosts.length).to.eql(2); @@ -182,17 +182,17 @@ export default function({ getService }: FtrProviderContext) { }) .expect(200); expect(body.total).to.eql(1); - const resultIp: string = body.hosts[0].host.ip.filter( + const resultIp: string = body.hosts[0].metadata.host.ip.filter( (ip: string) => ip === targetEndpointIp ); expect(resultIp).to.eql([targetEndpointIp]); - expect(body.hosts[0].event.created).to.eql(1579881969541); + expect(body.hosts[0].metadata.event.created).to.eql(1579881969541); expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); - it('metadata api should return the endpoint based on the elastic agent id', async () => { + it('metadata api should return the endpoint based on the elastic agent id, and status should be error', async () => { const targetEndpointId = 'fc0ff548-feba-41b6-8367-65e8790d0eaf'; const targetElasticAgentId = '023fa40c-411d-4188-a941-4147bfadd095'; const { body } = await supertest @@ -203,11 +203,12 @@ export default function({ getService }: FtrProviderContext) { }) .expect(200); expect(body.total).to.eql(1); - const resultHostId: string = body.hosts[0].host.id; - const resultElasticAgentId: string = body.hosts[0].elastic.agent.id; + const resultHostId: string = body.hosts[0].metadata.host.id; + const resultElasticAgentId: string = body.hosts[0].metadata.elastic.agent.id; expect(resultHostId).to.eql(targetEndpointId); expect(resultElasticAgentId).to.eql(targetElasticAgentId); - expect(body.hosts[0].event.created).to.eql(1579881969541); + expect(body.hosts[0].metadata.event.created).to.eql(1579881969541); + expect(body.hosts[0].host_status).to.eql('error'); expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); From 8d8c41153afa90d53004ab872a4ea86ca30a7b37 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 8 Apr 2020 09:22:06 -0700 Subject: [PATCH 19/81] Reporting/fix listing pagination (#62881) * [Reporting] Fix report table pagination * update snapshot * nice little comment --- .../report_listing.test.tsx.snap | 524 ------------------ .../public/components/report_listing.tsx | 60 +- 2 files changed, 31 insertions(+), 553 deletions(-) diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap index 75bc1f9eea696..271d61c224210 100644 --- a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap @@ -2,525 +2,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` Array [ - -
- - -
- -
- - - -
-
- - - - -
- - -
-
- - - -
- -
- - - -
- - -
-
- -
- -
- -
- - -
- -
- -
- - -
- - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - -
- -
-
- - -
-
-
- - Report - -
-
-
- - Created at - -
-
-
- - Status - -
-
-
- - Actions - -
-
-
- - Loading reports - -
-
-
-
-
- -
- , { throw error; } } + + // Since the contents of the table have changed, we must reset the pagination + // and re-fetch. Otherwise, the Nth page we are on could be empty of jobs. + this.setState(() => ({ page: 0 }), this.fetchJobs); }; return ( @@ -476,34 +480,32 @@ class ReportListingUi extends Component { onSelectionChange: this.onSelectionChange, }; - const search = { - toolsRight: this.renderDeleteButton(), - }; - return ( - + + + {this.state.selectedJobs.length > 0 ? this.renderDeleteButton() : null} + ); } } From 18c3f75bfbcf95bc531b0c752c887a0e502b720a Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Wed, 8 Apr 2020 11:38:23 -0500 Subject: [PATCH 20/81] The theme doesn't exist on props when used from the alerting management screen (#62811) --- .../infra/public/components/alerting/metrics/expression.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index 0903787dd05c4..2e43ede2480ce 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -320,11 +320,11 @@ interface ExpressionRowProps { const StyledExpressionRow = euiStyled(EuiFlexGroup)` display: flex; flex-wrap: wrap; - margin: 0 -${props => props.theme.eui.euiSizeXS}; + margin: 0 -4px; `; const StyledExpression = euiStyled.div` - padding: 0 ${props => props.theme.eui.euiSizeXS}; + padding: 0 4px; `; export const ExpressionRow: React.FC = props => { From 730dcbf6382acb50b0c7664ca2b86fb22d5dba7a Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 8 Apr 2020 09:54:42 -0700 Subject: [PATCH 21/81] Implemented actions server API for supporting preconfigured connectors (#62382) * Implemented actions server API for supporting preconfigured connectors defined in kibana.yaml * Fixed type check * Fixed due to comments and extended functional tests * Fixed tests and renamed connectors * fixed jest tests * Fixed type checks * Fixed failing alert save * Fixed alert client tests * fixed type checks * Fixed language check error * Fixed jest tests * Added missing comments and docs * fixed due to comments * Fixed json config for preconfigured * fixed type check, reverted config * config experiment with json stringify * revert experiment * Removed the spaces from connector names in config --- .../__snapshots__/step1.test.tsx.snap | 1 + .../alerts/configuration/configuration.tsx | 2 +- .../alerts/configuration/step1.test.tsx | 2 + .../public/containers/case/configure/api.ts | 4 +- .../case/configure/use_connectors.tsx | 2 +- .../configure_cases/__mock__/index.tsx | 2 + .../routes/__mocks__/request_responses.ts | 2 + .../scripts/get_action_instances.sh | 2 +- x-pack/plugins/actions/README.md | 11 +- x-pack/plugins/actions/common/types.ts | 1 + .../actions/server/actions_client.mock.ts | 2 +- .../actions/server/actions_client.test.ts | 94 ++++++-- .../plugins/actions/server/actions_client.ts | 116 +++++---- x-pack/plugins/actions/server/config.test.ts | 38 +++ x-pack/plugins/actions/server/config.ts | 12 + x-pack/plugins/actions/server/index.ts | 8 +- ...configured_action_disabled_modification.ts | 24 ++ x-pack/plugins/actions/server/mocks.ts | 1 + x-pack/plugins/actions/server/plugin.test.ts | 33 ++- x-pack/plugins/actions/server/plugin.ts | 30 ++- .../plugins/actions/server/routes/delete.ts | 13 +- .../actions/server/routes/find.test.ts | 152 ------------ x-pack/plugins/actions/server/routes/find.ts | 95 -------- .../actions/server/routes/get_all.test.ts | 117 ++++++++++ .../plugins/actions/server/routes/get_all.ts | 42 ++++ x-pack/plugins/actions/server/routes/index.ts | 2 +- x-pack/plugins/actions/server/types.ts | 5 + .../alerting/server/alerts_client.test.ts | 1 + .../plugins/alerting/server/alerts_client.ts | 52 ++++- .../server/alerts_client_factory.test.ts | 2 + .../alerting/server/alerts_client_factory.ts | 5 + x-pack/plugins/alerting/server/plugin.ts | 1 + .../case/common/api/cases/configure.ts | 7 - .../api/cases/configure/get_connectors.ts | 10 +- .../builtin_action_types/email.test.tsx | 4 + .../builtin_action_types/webhook.test.tsx | 2 + .../lib/action_connector_api.test.ts | 25 +- .../application/lib/action_connector_api.ts | 24 +- .../action_form.test.tsx | 15 ++ .../action_connector_form/action_form.tsx | 2 +- .../connector_edit_flyout.test.tsx | 1 + .../connector_reducer.test.ts | 1 + .../actions_connectors_list.test.tsx | 125 ++++------ .../components/actions_connectors_list.tsx | 2 +- .../components/alerts_list.test.tsx | 28 +-- .../triggers_actions_ui/public/types.ts | 1 + .../alerting_api_integration/common/config.ts | 24 ++ .../actions/builtin_action_types/email.ts | 2 + .../actions/builtin_action_types/es_index.ts | 4 + .../actions/builtin_action_types/pagerduty.ts | 2 + .../builtin_action_types/server_log.ts | 2 + .../builtin_action_types/servicenow.ts | 2 + .../actions/builtin_action_types/slack.ts | 2 + .../actions/builtin_action_types/webhook.ts | 2 + .../tests/actions/create.ts | 1 + .../tests/actions/delete.ts | 30 +++ .../security_and_spaces/tests/actions/get.ts | 35 +++ .../tests/actions/{find.ts => get_all.ts} | 220 +++++++++--------- .../tests/actions/index.ts | 2 +- .../tests/actions/update.ts | 40 ++++ .../actions/builtin_action_types/es_index.ts | 4 + .../spaces_only/tests/actions/create.ts | 1 + .../spaces_only/tests/actions/delete.ts | 11 + .../spaces_only/tests/actions/find.ts | 92 -------- .../spaces_only/tests/actions/get.ts | 13 ++ .../spaces_only/tests/actions/get_all.ts | 116 +++++++++ .../spaces_only/tests/actions/index.ts | 2 +- .../tests/actions/type_not_enabled.ts | 2 + .../spaces_only/tests/actions/update.ts | 21 ++ 69 files changed, 1050 insertions(+), 701 deletions(-) create mode 100644 x-pack/plugins/actions/server/lib/errors/preconfigured_action_disabled_modification.ts delete mode 100644 x-pack/plugins/actions/server/routes/find.test.ts delete mode 100644 x-pack/plugins/actions/server/routes/find.ts create mode 100644 x-pack/plugins/actions/server/routes/get_all.test.ts create mode 100644 x-pack/plugins/actions/server/routes/get_all.ts rename x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/{find.ts => get_all.ts} (57%) delete mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/actions/find.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap index 94d951a94fe29..cb1081c0c14da 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap @@ -25,6 +25,7 @@ exports[`Step1 editing should allow for editing 1`] = ` "actionTypeId": "1abc", "config": Object {}, "id": "1", + "isPreconfigured": false, "name": "Testing", } } diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx index 0933cd22db7c9..eaa474ba177b1 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx @@ -61,7 +61,7 @@ export const AlertsConfiguration: React.FC = ( async function fetchEmailActions() { const kibanaActions = await kfetch({ method: 'GET', - pathname: `/api/action/_find`, + pathname: `/api/action/_getAll`, }); const actions = kibanaActions.data.filter( diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx index 650294c29e9a5..19a1a61d00a42 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx @@ -27,6 +27,7 @@ describe('Step1', () => { actionTypeId: '1abc', name: 'Testing', config: {}, + isPreconfigured: false, }, ]; const selectedEmailActionId = emailActions[0].id; @@ -83,6 +84,7 @@ describe('Step1', () => { actionTypeId: '.email', name: '', config: {}, + isPreconfigured: false, }, ], selectedEmailActionId: NEW_ACTION_ID, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts index ed47cdc62a1b6..c24081c777a96 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts @@ -6,7 +6,7 @@ import { isEmpty } from 'lodash/fp'; import { - CasesConnectorsFindResult, + Connector, CasesConfigurePatch, CasesConfigureResponse, CasesConfigureRequest, @@ -18,7 +18,7 @@ import { ApiProps } from '../types'; import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; import { CaseConfigure } from './types'; -export const fetchConnectors = async ({ signal }: ApiProps): Promise => { +export const fetchConnectors = async ({ signal }: ApiProps): Promise => { const response = await KibanaServices.get().http.fetch( `${CASES_CONFIGURE_URL}/connectors/_find`, { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx index d31dcdbee2a14..30108ecf33874 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx @@ -31,7 +31,7 @@ export const useConnectors = (): ReturnConnectors => { const res = await fetchConnectors({ signal: abortCtrl.signal }); if (!didCancel) { setLoading(false); - setConnectors(res.data); + setConnectors(res); } } catch (error) { if (!didCancel) { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx index a3df3664398ad..135f0f2a7e26d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx @@ -21,6 +21,7 @@ export const connectors: Connector[] = [ id: '123', actionTypeId: '.servicenow', name: 'My Connector', + isPreconfigured: false, config: { apiUrl: 'https://instance1.service-now.com', casesConfiguration: { @@ -48,6 +49,7 @@ export const connectors: Connector[] = [ id: '456', actionTypeId: '.servicenow', name: 'My Connector 2', + isPreconfigured: false, config: { apiUrl: 'https://instance2.service-now.com', casesConfiguration: { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 77c1641d073c6..e400360a5a5b2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -383,6 +383,7 @@ export const createActionResult = (): ActionResult => ({ actionTypeId: 'action-id-1', name: '', config: {}, + isPreconfigured: false, }); export const nonRuleAlert = () => ({ @@ -518,6 +519,7 @@ export const updateActionResult = (): ActionResult => ({ actionTypeId: 'action-id-1', name: '', config: {}, + isPreconfigured: false, }); export const getMockPrivilegesResult = () => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh index 7804439ce0734..750c5574f4a72 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh @@ -13,5 +13,5 @@ set -e # https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md#get-apiaction_find-find-actions curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/action/_find \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/action/_getAll \ | jq . diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index d217d26e84836..82cc09f5e9eca 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -28,7 +28,7 @@ Table of Contents - [RESTful API](#restful-api) - [`POST /api/action`: Create action](#post-apiaction-create-action) - [`DELETE /api/action/{id}`: Delete action](#delete-apiactionid-delete-action) - - [`GET /api/action/_find`: Find actions](#get-apiactionfind-find-actions) + - [`GET /api/action/_getAll`: Get all actions](#get-apiaction-get-all-actions) - [`GET /api/action/{id}`: Get action](#get-apiactionid-get-action) - [`GET /api/action/types`: List action types](#get-apiactiontypes-list-action-types) - [`PUT /api/action/{id}`: Update action](#put-apiactionid-update-action) @@ -92,6 +92,7 @@ Built-In-Actions are configured using the _xpack.actions_ namespoace under _kiba | _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | | _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | | _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | +| _xpack.actions._**preconfigured** | A list of preconfigured actions. Default: `[]` | Array | #### Whitelisting Built-in Action Types @@ -174,11 +175,13 @@ Params: | -------- | --------------------------------------------- | ------ | | id | The id of the action you're trying to delete. | string | -### `GET /api/action/_find`: Find actions +### `GET /api/action/_getAll`: Get all actions -Params: +No parameters. -See the [saved objects API documentation for find](https://www.elastic.co/guide/en/kibana/master/saved-objects-api-find.html). All the properties are the same except that you cannot pass in `type`. +Return all actions from saved objects merged with predefined list. +Use the [saved objects API for find](https://www.elastic.co/guide/en/kibana/master/saved-objects-api-find.html) with the proprties: `type: 'action'` and `perPage: 10000`. +List of predefined actions should be set up in Kibana.yaml. ### `GET /api/action/{id}`: Get action diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index f3042a701211f..61b338d47b9f5 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -20,4 +20,5 @@ export interface ActionResult { actionTypeId: string; name: string; config: Record; + isPreconfigured: boolean; } diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 8a39d68f40bb6..431bfb1e99c3b 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -12,9 +12,9 @@ const createActionsClientMock = () => { const mocked: jest.Mocked = { create: jest.fn(), get: jest.fn(), - find: jest.fn(), delete: jest.fn(), update: jest.fn(), + getAll: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 0df07ad58fb9e..955e1569380a5 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -51,6 +51,7 @@ beforeEach(() => { savedObjectsClient, scopedClusterClient, defaultKibanaIndex, + preconfiguredActions: [], }); }); @@ -83,6 +84,7 @@ describe('create()', () => { }); expect(result).toEqual({ id: '1', + isPreconfigured: false, name: 'my name', actionTypeId: 'my-action-type', config: {}, @@ -178,6 +180,7 @@ describe('create()', () => { }); expect(result).toEqual({ id: '1', + isPreconfigured: false, name: 'my name', actionTypeId: 'my-action-type', config: { @@ -226,6 +229,7 @@ describe('create()', () => { savedObjectsClient, scopedClusterClient, defaultKibanaIndex, + preconfiguredActions: [], }); const savedObjectCreateResult = { @@ -305,6 +309,7 @@ describe('get()', () => { const result = await actionsClient.get({ id: '1' }); expect(result).toEqual({ id: '1', + isPreconfigured: false, }); expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` @@ -314,9 +319,44 @@ describe('get()', () => { ] `); }); + + test('return predefined action with id', async () => { + actionsClient = new ActionsClient({ + actionTypeRegistry, + savedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + preconfiguredActions: [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: { + test: 'test1', + }, + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + }); + + const result = await actionsClient.get({ id: 'testPreconfigured' }); + expect(result).toEqual({ + id: 'testPreconfigured', + actionTypeId: '.slack', + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + }); + expect(savedObjectsClient.get).not.toHaveBeenCalled(); + }); }); -describe('find()', () => { +describe('getAll()', () => { test('calls savedObjectsClient with parameters', async () => { const expectedResult = { total: 1, @@ -327,6 +367,7 @@ describe('find()', () => { id: '1', type: 'type', attributes: { + name: 'test', config: { foo: 'bar', }, @@ -339,31 +380,50 @@ describe('find()', () => { scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({ aggregations: { '1': { doc_count: 6 }, + testPreconfigured: { doc_count: 2 }, }, }); - const result = await actionsClient.find({}); - expect(result).toEqual({ - total: 1, - perPage: 10, - page: 1, - data: [ + + actionsClient = new ActionsClient({ + actionTypeRegistry, + savedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + preconfiguredActions: [ { - id: '1', + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + name: 'test', config: { foo: 'bar', }, - referencedByCount: 6, }, ], }); - expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "type": "action", + const result = await actionsClient.getAll(); + expect(result).toEqual([ + { + id: '1', + isPreconfigured: false, + name: 'test', + config: { + foo: 'bar', }, - ] - `); + referencedByCount: 6, + }, + { + id: 'testPreconfigured', + actionTypeId: '.slack', + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + referencedByCount: 2, + }, + ]); }); }); @@ -420,6 +480,7 @@ describe('update()', () => { }); expect(result).toEqual({ id: 'my-action', + isPreconfigured: false, actionTypeId: 'my-action-type', name: 'my name', config: {}, @@ -524,6 +585,7 @@ describe('update()', () => { }); expect(result).toEqual({ id: 'my-action', + isPreconfigured: false, actionTypeId: 'my-action-type', name: 'my name', config: { diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 129829850f9c1..8f73bfb31ea4d 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -11,9 +11,16 @@ import { SavedObject, } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; import { ActionTypeRegistry } from './action_type_registry'; import { validateConfig, validateSecrets } from './lib'; -import { ActionResult, FindActionResult, RawAction } from './types'; +import { ActionResult, FindActionResult, RawAction, PreConfiguredAction } from './types'; +import { PreconfiguredActionDisabledModificationError } from './lib/errors/preconfigured_action_disabled_modification'; + +// We are assuming there won't be many actions. This is why we will load +// all the actions in advance and assume the total count to not go over 10000. +// We'll set this max setting assuming it's never reached. +export const MAX_ACTIONS_RETURNED = 10000; interface ActionUpdate extends SavedObjectAttributes { name: string; @@ -29,35 +36,12 @@ interface CreateOptions { action: Action; } -interface FindOptions { - options?: { - perPage?: number; - page?: number; - search?: string; - defaultSearchOperator?: 'AND' | 'OR'; - searchFields?: string[]; - sortField?: string; - hasReference?: { - type: string; - id: string; - }; - fields?: string[]; - filter?: string; - }; -} - -interface FindResult { - page: number; - perPage: number; - total: number; - data: FindActionResult[]; -} - interface ConstructorOptions { defaultKibanaIndex: string; scopedClusterClient: IScopedClusterClient; actionTypeRegistry: ActionTypeRegistry; savedObjectsClient: SavedObjectsClientContract; + preconfiguredActions: PreConfiguredAction[]; } interface UpdateOptions { @@ -70,17 +54,20 @@ export class ActionsClient { private readonly scopedClusterClient: IScopedClusterClient; private readonly savedObjectsClient: SavedObjectsClientContract; private readonly actionTypeRegistry: ActionTypeRegistry; + private readonly preconfiguredActions: PreConfiguredAction[]; constructor({ actionTypeRegistry, defaultKibanaIndex, scopedClusterClient, savedObjectsClient, + preconfiguredActions, }: ConstructorOptions) { this.actionTypeRegistry = actionTypeRegistry; this.savedObjectsClient = savedObjectsClient; this.scopedClusterClient = scopedClusterClient; this.defaultKibanaIndex = defaultKibanaIndex; + this.preconfiguredActions = preconfiguredActions; } /** @@ -106,6 +93,7 @@ export class ActionsClient { actionTypeId: result.attributes.actionTypeId, name: result.attributes.name, config: result.attributes.config, + isPreconfigured: false, }; } @@ -113,6 +101,20 @@ export class ActionsClient { * Update action */ public async update({ id, action }: UpdateOptions): Promise { + if ( + this.preconfiguredActions.find(preconfiguredAction => preconfiguredAction.id === id) !== + undefined + ) { + throw new PreconfiguredActionDisabledModificationError( + i18n.translate('xpack.actions.serverSideErrors.predefinedActionUpdateDisabled', { + defaultMessage: 'Preconfigured action {id} is not allowed to update.', + values: { + id, + }, + }), + 'update' + ); + } const existingObject = await this.savedObjectsClient.get('action', id); const { actionTypeId } = existingObject.attributes; const { name, config, secrets } = action; @@ -134,6 +136,7 @@ export class ActionsClient { actionTypeId: result.attributes.actionTypeId as string, name: result.attributes.name as string, config: result.attributes.config as Record, + isPreconfigured: false, }; } @@ -141,6 +144,18 @@ export class ActionsClient { * Get an action */ public async get({ id }: { id: string }): Promise { + const preconfiguredActionsList = this.preconfiguredActions.find( + preconfiguredAction => preconfiguredAction.id === id + ); + if (preconfiguredActionsList !== undefined) { + return { + id, + actionTypeId: preconfiguredActionsList.actionTypeId, + name: preconfiguredActionsList.name, + config: preconfiguredActionsList.config, + isPreconfigured: true, + }; + } const result = await this.savedObjectsClient.get('action', id); return { @@ -148,36 +163,56 @@ export class ActionsClient { actionTypeId: result.attributes.actionTypeId, name: result.attributes.name, config: result.attributes.config, + isPreconfigured: false, }; } /** - * Find actions + * Get all actions with preconfigured list */ - public async find({ options = {} }: FindOptions): Promise { - const findResult = await this.savedObjectsClient.find({ - ...options, - type: 'action', - }); + public async getAll(): Promise { + const savedObjectsActions = ( + await this.savedObjectsClient.find({ + perPage: MAX_ACTIONS_RETURNED, + type: 'action', + }) + ).saved_objects.map(actionFromSavedObject); - const data = await injectExtraFindData( + const mergedResult = [ + ...savedObjectsActions, + ...this.preconfiguredActions.map(preconfiguredAction => ({ + id: preconfiguredAction.id, + actionTypeId: preconfiguredAction.actionTypeId, + name: preconfiguredAction.name, + config: preconfiguredAction.config, + isPreconfigured: true, + })), + ].sort((a, b) => a.name.localeCompare(b.name)); + return await injectExtraFindData( this.defaultKibanaIndex, this.scopedClusterClient, - findResult.saved_objects.map(actionFromSavedObject) + mergedResult ); - - return { - page: findResult.page, - perPage: findResult.per_page, - total: findResult.total, - data, - }; } /** * Delete action */ public async delete({ id }: { id: string }) { + if ( + this.preconfiguredActions.find(preconfiguredAction => preconfiguredAction.id === id) !== + undefined + ) { + throw new PreconfiguredActionDisabledModificationError( + i18n.translate('xpack.actions.serverSideErrors.predefinedActionDeleteDisabled', { + defaultMessage: 'Preconfigured action {id} is not allowed to delete.', + values: { + id, + }, + }), + 'delete' + ); + } return await this.savedObjectsClient.delete('action', id); } } @@ -186,6 +221,7 @@ function actionFromSavedObject(savedObject: SavedObject): ActionResul return { id: savedObject.id, ...savedObject.attributes, + isPreconfigured: false, }; } diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 67b7553c4a736..51e87dbd75b48 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -14,6 +14,44 @@ describe('config validation', () => { "enabledActionTypes": Array [ "*", ], + "preconfigured": Array [], + "whitelistedHosts": Array [ + "*", + ], + } + `); + }); + + test('action with preconfigured actions', () => { + const config: Record = { + preconfigured: [ + { + id: 'my-slack1', + actionTypeId: '.slack', + name: 'Slack #xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + }, + ], + }; + expect(configSchema.validate(config)).toMatchInlineSnapshot(` + Object { + "enabled": true, + "enabledActionTypes": Array [ + "*", + ], + "preconfigured": Array [ + Object { + "actionTypeId": ".slack", + "config": Object { + "webhookUrl": "https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz", + }, + "id": "my-slack1", + "name": "Slack #xyz", + "secrets": Object {}, + }, + ], "whitelistedHosts": Array [ "*", ], diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 9e4795be6c208..1f04efd1941b4 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -21,6 +21,18 @@ export const configSchema = schema.object({ defaultValue: [WhitelistedHosts.Any], } ), + preconfigured: schema.arrayOf( + schema.object({ + id: schema.string({ minLength: 1 }), + name: schema.string(), + actionTypeId: schema.string({ minLength: 1 }), + config: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + }), + { + defaultValue: [], + } + ), }); export type ActionsConfig = TypeOf; diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index eee2ae352fe3d..88553c314112f 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -11,7 +11,13 @@ import { ActionsClient as ActionsClientClass } from './actions_client'; export type ActionsClient = PublicMethodsOf; -export { ActionsPlugin, ActionResult, ActionTypeExecutorOptions, ActionType } from './types'; +export { + ActionsPlugin, + ActionResult, + ActionTypeExecutorOptions, + ActionType, + PreConfiguredAction, +} from './types'; export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initContext: PluginInitializerContext) => new ActionsPlugin(initContext); diff --git a/x-pack/plugins/actions/server/lib/errors/preconfigured_action_disabled_modification.ts b/x-pack/plugins/actions/server/lib/errors/preconfigured_action_disabled_modification.ts new file mode 100644 index 0000000000000..884353e132b9c --- /dev/null +++ b/x-pack/plugins/actions/server/lib/errors/preconfigured_action_disabled_modification.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaResponseFactory } from '../../../../../../src/core/server'; +import { ErrorThatHandlesItsOwnResponse } from './types'; + +export type PreconfiguredActionDisabledFrom = 'update' | 'delete'; + +export class PreconfiguredActionDisabledModificationError extends Error + implements ErrorThatHandlesItsOwnResponse { + public readonly disabledFrom: PreconfiguredActionDisabledFrom; + + constructor(message: string, disabledFrom: PreconfiguredActionDisabledFrom) { + super(message); + this.disabledFrom = disabledFrom; + } + + public sendResponse(res: KibanaResponseFactory) { + return res.badRequest({ body: { message: this.message } }); + } +} diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index 75396f2aad897..bc4268bb69872 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -21,6 +21,7 @@ const createStartMock = () => { execute: jest.fn(), isActionTypeEnabled: jest.fn(), getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()), + preconfiguredActions: [], }; return mock; }; diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 383f84590fbc6..6215b08df81d4 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -31,7 +31,34 @@ describe('Actions Plugin', () => { let pluginsSetup: jest.Mocked; beforeEach(() => { - context = coreMock.createPluginInitializerContext(); + context = coreMock.createPluginInitializerContext({ + preconfigured: [ + { + id: 'my-slack1', + actionTypeId: '.slack', + name: 'Slack #xyz', + description: 'Send a message to the #xyz channel', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + }, + { + id: 'custom-system-abc-connector', + actionTypeId: 'system-abc-action-type', + description: 'Send a notification to system ABC', + name: 'System ABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + secrets: { + xyzSecret1: 'credential1', + xyzSecret2: 'credential2', + }, + }, + ], + }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -160,7 +187,9 @@ describe('Actions Plugin', () => { let pluginsStart: jest.Mocked; beforeEach(() => { - const context = coreMock.createPluginInitializerContext(); + const context = coreMock.createPluginInitializerContext({ + preconfigured: [], + }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); coreStart = coreMock.createStart(); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index ce31e62bc9b8e..34c9e7aa9e8b8 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -30,7 +30,7 @@ import { LICENSE_TYPE } from '../../licensing/common/types'; import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server'; import { ActionsConfig } from './config'; -import { Services, ActionType } from './types'; +import { Services, ActionType, PreConfiguredAction } from './types'; import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib'; import { ActionsClient } from './actions_client'; import { ActionTypeRegistry } from './action_type_registry'; @@ -44,7 +44,7 @@ import { getActionsConfigurationUtilities } from './actions_config'; import { createActionRoute, deleteActionRoute, - findActionRoute, + getAllActionRoute, getActionRoute, updateActionRoute, listActionTypesRoute, @@ -67,6 +67,7 @@ export interface PluginStartContract { isActionTypeEnabled(id: string): boolean; execute(options: ExecuteOptions): Promise; getActionsClientWithRequest(request: KibanaRequest): Promise>; + preconfiguredActions: PreConfiguredAction[]; } export interface ActionsPluginsSetup { @@ -97,6 +98,7 @@ export class ActionsPlugin implements Plugin, Plugi private eventLogger?: IEventLogger; private isESOUsingEphemeralEncryptionKey?: boolean; private readonly telemetryLogger: Logger; + private readonly preconfiguredActions: PreConfiguredAction[]; constructor(initContext: PluginInitializerContext) { this.config = initContext.config @@ -113,6 +115,7 @@ export class ActionsPlugin implements Plugin, Plugi this.logger = initContext.logger.get('actions'); this.telemetryLogger = initContext.logger.get('telemetry'); + this.preconfiguredActions = []; } public async setup(core: CoreSetup, plugins: ActionsPluginsSetup): Promise { @@ -151,8 +154,14 @@ export class ActionsPlugin implements Plugin, Plugi // get executions count const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); - const actionsConfigUtils = getActionsConfigurationUtilities( - (await this.config) as ActionsConfig + const actionsConfig = (await this.config) as ActionsConfig; + const actionsConfigUtils = getActionsConfigurationUtilities(actionsConfig); + + this.preconfiguredActions.push( + ...actionsConfig.preconfigured.map( + preconfiguredAction => + ({ ...preconfiguredAction, isPreconfigured: true } as PreConfiguredAction) + ) ); const actionTypeRegistry = new ActionTypeRegistry({ taskRunnerFactory, @@ -197,7 +206,7 @@ export class ActionsPlugin implements Plugin, Plugi createActionRoute(router, this.licenseState); deleteActionRoute(router, this.licenseState); getActionRoute(router, this.licenseState); - findActionRoute(router, this.licenseState); + getAllActionRoute(router, this.licenseState); updateActionRoute(router, this.licenseState); listActionTypesRoute(router, this.licenseState); executeActionRoute(router, this.licenseState, actionExecutor); @@ -226,6 +235,7 @@ export class ActionsPlugin implements Plugin, Plugi kibanaIndex, adminClient, isESOUsingEphemeralEncryptionKey, + preconfiguredActions, } = this; actionExecutor!.initialize({ @@ -271,8 +281,10 @@ export class ActionsPlugin implements Plugin, Plugi actionTypeRegistry: actionTypeRegistry!, defaultKibanaIndex: await kibanaIndex, scopedClusterClient: adminClient!.asScoped(request), + preconfiguredActions, }); }, + preconfiguredActions, }; } @@ -289,7 +301,12 @@ export class ActionsPlugin implements Plugin, Plugi private createRouteHandlerContext = ( defaultKibanaIndex: string ): IContextProvider, 'actions'> => { - const { actionTypeRegistry, adminClient, isESOUsingEphemeralEncryptionKey } = this; + const { + actionTypeRegistry, + adminClient, + isESOUsingEphemeralEncryptionKey, + preconfiguredActions, + } = this; return async function actionsRouteHandlerContext(context, request) { return { getActionsClient: () => { @@ -303,6 +320,7 @@ export class ActionsPlugin implements Plugin, Plugi actionTypeRegistry: actionTypeRegistry!, defaultKibanaIndex, scopedClusterClient: adminClient!.asScoped(request), + preconfiguredActions, }); }, listTypes: actionTypeRegistry!.list.bind(actionTypeRegistry!), diff --git a/x-pack/plugins/actions/server/routes/delete.ts b/x-pack/plugins/actions/server/routes/delete.ts index cddebb3a8e31e..ffd1f0faabbab 100644 --- a/x-pack/plugins/actions/server/routes/delete.ts +++ b/x-pack/plugins/actions/server/routes/delete.ts @@ -17,7 +17,7 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { ILicenseState, verifyApiAccess } from '../lib'; +import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../lib'; import { BASE_ACTION_API_PATH } from '../../common'; const paramSchema = schema.object({ @@ -46,8 +46,15 @@ export const deleteActionRoute = (router: IRouter, licenseState: ILicenseState) } const actionsClient = context.actions.getActionsClient(); const { id } = req.params; - await actionsClient.delete({ id }); - return res.noContent(); + try { + await actionsClient.delete({ id }); + return res.noContent(); + } catch (e) { + if (isErrorThatHandlesItsOwnResponse(e)) { + return e.sendResponse(res); + } + throw e; + } }) ); }; diff --git a/x-pack/plugins/actions/server/routes/find.test.ts b/x-pack/plugins/actions/server/routes/find.test.ts deleted file mode 100644 index 1b130421fa71f..0000000000000 --- a/x-pack/plugins/actions/server/routes/find.test.ts +++ /dev/null @@ -1,152 +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 { findActionRoute } from './find'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib'; -import { mockHandlerArguments } from './_mock_handler_arguments'; - -jest.mock('../lib/verify_api_access.ts', () => ({ - verifyApiAccess: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('findActionRoute', () => { - it('finds actions with proper parameters', async () => { - const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); - - findActionRoute(router, licenseState); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/action/_find"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-read", - ], - } - `); - - const findResult = { - page: 1, - perPage: 1, - total: 0, - data: [], - }; - const actionsClient = { - find: jest.fn().mockResolvedValueOnce(findResult), - }; - - const [context, req, res] = mockHandlerArguments( - { actionsClient }, - { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - }, - ['ok'] - ); - - expect(await handler(context, req, res)).toMatchInlineSnapshot(` - Object { - "body": Object { - "data": Array [], - "page": 1, - "perPage": 1, - "total": 0, - }, - } - `); - - expect(actionsClient.find).toHaveBeenCalledTimes(1); - expect(actionsClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "options": Object { - "defaultSearchOperator": "OR", - "fields": undefined, - "filter": undefined, - "page": 1, - "perPage": 1, - "search": undefined, - "sortField": undefined, - "sortOrder": undefined, - }, - }, - ] - `); - - expect(res.ok).toHaveBeenCalledWith({ - body: findResult, - }); - }); - - it('ensures the license allows finding actions', async () => { - const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); - - findActionRoute(router, licenseState); - - const [, handler] = router.get.mock.calls[0]; - - const actionsClient = { - find: jest.fn().mockResolvedValueOnce({ - page: 1, - perPage: 1, - total: 0, - data: [], - }), - }; - - const [context, req, res] = mockHandlerArguments(actionsClient, { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - }); - - await handler(context, req, res); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the license check prevents finding actions', async () => { - const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); - - (verifyApiAccess as jest.Mock).mockImplementation(() => { - throw new Error('OMG'); - }); - - findActionRoute(router, licenseState); - - const [, handler] = router.get.mock.calls[0]; - - const [context, req, res] = mockHandlerArguments( - {}, - { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - }, - ['ok'] - ); - expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); -}); diff --git a/x-pack/plugins/actions/server/routes/find.ts b/x-pack/plugins/actions/server/routes/find.ts deleted file mode 100644 index 45b967629a2a8..0000000000000 --- a/x-pack/plugins/actions/server/routes/find.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema, TypeOf } from '@kbn/config-schema'; -import { - IRouter, - RequestHandlerContext, - KibanaRequest, - IKibanaResponse, - KibanaResponseFactory, -} from 'kibana/server'; -import { FindOptions } from '../../../alerting/server'; -import { ILicenseState, verifyApiAccess } from '../lib'; -import { BASE_ACTION_API_PATH } from '../../common'; - -// config definition -const querySchema = schema.object({ - per_page: schema.number({ defaultValue: 20, min: 0 }), - page: schema.number({ defaultValue: 1, min: 1 }), - search: schema.maybe(schema.string()), - default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], { - defaultValue: 'OR', - }), - search_fields: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), - sort_field: schema.maybe(schema.string()), - sort_order: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])), - has_reference: schema.maybe( - // use nullable as maybe is currently broken - // in config-schema - schema.nullable( - schema.object({ - type: schema.string(), - id: schema.string(), - }) - ) - ), - fields: schema.maybe(schema.arrayOf(schema.string())), - filter: schema.maybe(schema.string()), -}); - -export const findActionRoute = (router: IRouter, licenseState: ILicenseState) => { - router.get( - { - path: `${BASE_ACTION_API_PATH}/_find`, - validate: { - query: querySchema, - }, - options: { - tags: ['access:actions-read'], - }, - }, - router.handleLegacyErrors(async function( - context: RequestHandlerContext, - req: KibanaRequest, any, any>, - res: KibanaResponseFactory - ): Promise> { - verifyApiAccess(licenseState); - if (!context.actions) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); - } - const actionsClient = context.actions.getActionsClient(); - const query = req.query; - const options: FindOptions['options'] = { - perPage: query.per_page, - page: query.page, - search: query.search, - defaultSearchOperator: query.default_search_operator, - sortField: query.sort_field, - fields: query.fields, - filter: query.filter, - sortOrder: query.sort_order, - }; - - if (query.search_fields) { - options.searchFields = Array.isArray(query.search_fields) - ? query.search_fields - : [query.search_fields]; - } - - if (query.has_reference) { - options.hasReference = query.has_reference; - } - - const findResult = await actionsClient.find({ - options, - }); - return res.ok({ - body: findResult, - }); - }) - ); -}; diff --git a/x-pack/plugins/actions/server/routes/get_all.test.ts b/x-pack/plugins/actions/server/routes/get_all.test.ts new file mode 100644 index 0000000000000..6499427b8c1a5 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/get_all.test.ts @@ -0,0 +1,117 @@ +/* + * 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 { getAllActionRoute } from './get_all'; +import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib'; +import { mockHandlerArguments } from './_mock_handler_arguments'; + +jest.mock('../lib/verify_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getAllActionRoute', () => { + it('get all actions with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router: RouterMock = mockRouter.create(); + + getAllActionRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/action/_getAll"`); + expect(config.options).toMatchInlineSnapshot(` + Object { + "tags": Array [ + "access:actions-read", + ], + } + `); + + const actionsClient = { + getAll: jest.fn().mockResolvedValueOnce([]), + }; + + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Array [], + } + `); + + expect(actionsClient.getAll).toHaveBeenCalledTimes(1); + + expect(res.ok).toHaveBeenCalledWith({ + body: [], + }); + }); + + it('ensures the license allows getting all actions', async () => { + const licenseState = licenseStateMock.create(); + const router: RouterMock = mockRouter.create(); + + getAllActionRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/action/_getAll"`); + expect(config.options).toMatchInlineSnapshot(` + Object { + "tags": Array [ + "access:actions-read", + ], + } + `); + + const actionsClient = { + getAll: jest.fn().mockResolvedValueOnce([]), + }; + + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents getting all actions', async () => { + const licenseState = licenseStateMock.create(); + const router: RouterMock = mockRouter.create(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + getAllActionRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/action/_getAll"`); + expect(config.options).toMatchInlineSnapshot(` + Object { + "tags": Array [ + "access:actions-read", + ], + } + `); + + const actionsClient = { + getAll: jest.fn().mockResolvedValueOnce([]), + }; + + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/get_all.ts b/x-pack/plugins/actions/server/routes/get_all.ts new file mode 100644 index 0000000000000..c70a13bc01c9f --- /dev/null +++ b/x-pack/plugins/actions/server/routes/get_all.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IRouter, + RequestHandlerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, +} from 'kibana/server'; +import { ILicenseState, verifyApiAccess } from '../lib'; +import { BASE_ACTION_API_PATH } from '../../common'; + +export const getAllActionRoute = (router: IRouter, licenseState: ILicenseState) => { + router.get( + { + path: `${BASE_ACTION_API_PATH}/_getAll`, + validate: {}, + options: { + tags: ['access:actions-read'], + }, + }, + router.handleLegacyErrors(async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + verifyApiAccess(licenseState); + if (!context.actions) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); + } + const actionsClient = context.actions.getActionsClient(); + const result = await actionsClient.getAll(); + return res.ok({ + body: result, + }); + }) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts index 33191132bece5..94f9ec1c94364 100644 --- a/x-pack/plugins/actions/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -6,7 +6,7 @@ export { createActionRoute } from './create'; export { deleteActionRoute } from './delete'; -export { findActionRoute } from './find'; +export { getAllActionRoute } from './get_all'; export { getActionRoute } from './get'; export { updateActionRoute } from './update'; export { listActionTypesRoute } from './list_action_types'; diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 999e739e77060..92e38d77314f8 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -55,6 +55,11 @@ export interface ActionResult { actionTypeId: string; name: string; config: Record; + isPreconfigured: boolean; +} + +export interface PreConfiguredAction extends ActionResult { + secrets: Record; } export interface FindActionResult extends ActionResult { diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client.test.ts index 0e929ff457fbd..a9ff5ee8ecdc6 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client.test.ts @@ -30,6 +30,7 @@ const alertsClientParams = { invalidateAPIKey: jest.fn(), logger: loggingServiceMock.create().get(), encryptedSavedObjectsPlugin: encryptedSavedObjects, + preconfiguredActions: [], }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index 5538b44b69fcb..6f8478df58a53 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -13,6 +13,7 @@ import { SavedObjectReference, SavedObject, } from 'src/core/server'; +import { PreConfiguredAction } from '../../actions/server'; import { Alert, PartialAlert, @@ -53,6 +54,7 @@ interface ConstructorOptions { getUserName: () => Promise; createAPIKey: () => Promise; invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise; + preconfiguredActions: PreConfiguredAction[]; } export interface FindOptions { @@ -123,6 +125,7 @@ export class AlertsClient { private readonly invalidateAPIKey: ( params: InvalidateAPIKeyParams ) => Promise; + private preconfiguredActions: PreConfiguredAction[]; encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; constructor({ @@ -136,6 +139,7 @@ export class AlertsClient { createAPIKey, invalidateAPIKey, encryptedSavedObjectsPlugin, + preconfiguredActions, }: ConstructorOptions) { this.logger = logger; this.getUserName = getUserName; @@ -147,6 +151,7 @@ export class AlertsClient { this.createAPIKey = createAPIKey; this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin; + this.preconfiguredActions = preconfiguredActions; } public async create({ data, options }: CreateOptions): Promise { @@ -659,18 +664,37 @@ export class AlertsClient { private async denormalizeActions( alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { - // Fetch action objects in bulk - const actionIds = [...new Set(alertActions.map(alertAction => alertAction.id))]; - const bulkGetOpts = actionIds.map(id => ({ id, type: 'action' })); - const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); const actionMap = new Map(); - for (const action of bulkGetResult.saved_objects) { - if (action.error) { - throw Boom.badRequest( - `Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}` - ); + // map preconfigured actions + for (const alertAction of alertActions) { + const action = this.preconfiguredActions.find( + preconfiguredAction => preconfiguredAction.id === alertAction.id + ); + if (action !== undefined) { + actionMap.set(action.id, action); + } + } + // Fetch action objects in bulk + // Excluding preconfigured actions to avoid an not found error, which is already mapped + const actionIds = [ + ...new Set( + alertActions + .filter(alertAction => !actionMap.has(alertAction.id)) + .map(alertAction => alertAction.id) + ), + ]; + if (actionIds.length > 0) { + const bulkGetOpts = actionIds.map(id => ({ id, type: 'action' })); + const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); + + for (const action of bulkGetResult.saved_objects) { + if (action.error) { + throw Boom.badRequest( + `Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}` + ); + } + actionMap.set(action.id, action); } - actionMap.set(action.id, action); } // Extract references and set actionTypeId const references: SavedObjectReference[] = []; @@ -681,10 +705,16 @@ export class AlertsClient { name: actionRef, type: 'action', }); + const actionMapValue = actionMap.get(id); + // if action is a save object, than actionTypeId should be under attributes property + // if action is a preconfigured, than actionTypeId is the action property + const actionTypeId = actionIds.find(actionId => actionId === id) + ? actionMapValue.attributes.actionTypeId + : actionMapValue.actionTypeId; return { ...alertAction, actionRef, - actionTypeId: actionMap.get(id).attributes.actionTypeId, + actionTypeId, }; }); return { diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts index 4c74ca54a0d2f..951d18a33b35f 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts @@ -28,6 +28,7 @@ const alertsClientFactoryParams: jest.Mocked = { getSpaceId: jest.fn(), spaceIdToNamespace: jest.fn(), encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.createStart(), + preconfiguredActions: [], }; const fakeRequest: Request = { headers: {}, @@ -67,6 +68,7 @@ test('creates an alerts client with proper constructor arguments', async () => { createAPIKey: expect.any(Function), invalidateAPIKey: expect.any(Function), encryptedSavedObjectsPlugin: alertsClientFactoryParams.encryptedSavedObjectsPlugin, + preconfiguredActions: [], }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerting/server/alerts_client_factory.ts index fd480658e236a..734417e72733e 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PreConfiguredAction } from '../../actions/server'; import { AlertsClient } from './alerts_client'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { KibanaRequest, Logger, SavedObjectsClientContract } from '../../../../src/core/server'; @@ -19,6 +20,7 @@ export interface AlertsClientFactoryOpts { getSpaceId: (request: KibanaRequest) => string | undefined; spaceIdToNamespace: SpaceIdToNamespaceFunction; encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; + preconfiguredActions: PreConfiguredAction[]; } export class AlertsClientFactory { @@ -30,6 +32,7 @@ export class AlertsClientFactory { private getSpaceId!: (request: KibanaRequest) => string | undefined; private spaceIdToNamespace!: SpaceIdToNamespaceFunction; private encryptedSavedObjectsPlugin!: EncryptedSavedObjectsPluginStart; + private preconfiguredActions!: PreConfiguredAction[]; public initialize(options: AlertsClientFactoryOpts) { if (this.isInitialized) { @@ -43,6 +46,7 @@ export class AlertsClientFactory { this.securityPluginSetup = options.securityPluginSetup; this.spaceIdToNamespace = options.spaceIdToNamespace; this.encryptedSavedObjectsPlugin = options.encryptedSavedObjectsPlugin; + this.preconfiguredActions = options.preconfiguredActions; } public create( @@ -100,6 +104,7 @@ export class AlertsClientFactory { result: invalidateAPIKeyResult, }; }, + preconfiguredActions: this.preconfiguredActions, }); } } diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 90e274df3a5ee..172a106226345 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -218,6 +218,7 @@ export class AlertingPlugin { getSpaceId(request: KibanaRequest) { return spaces?.getSpaceId(request); }, + preconfiguredActions: plugins.actions.preconfiguredActions, }); taskRunnerFactory.initialize({ diff --git a/x-pack/plugins/case/common/api/cases/configure.ts b/x-pack/plugins/case/common/api/cases/configure.ts index 9b210c2aa05ad..d92af587d0e92 100644 --- a/x-pack/plugins/case/common/api/cases/configure.ts +++ b/x-pack/plugins/case/common/api/cases/configure.ts @@ -61,13 +61,6 @@ export type CasesConnectorConfiguration = rt.TypeOf action.actionTypeId === CASE_SERVICE_NOW_ACTION + ); + return response.ok({ body: results }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx index a7d479f922ed1..af9e34071fd09 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx @@ -39,6 +39,7 @@ describe('connector validation', () => { id: 'test', actionTypeId: '.email', name: 'email', + isPreconfigured: false, config: { from: 'test@test.com', port: 2323, @@ -66,6 +67,7 @@ describe('connector validation', () => { }, id: 'test', actionTypeId: '.email', + isPreconfigured: false, name: 'email', config: { from: 'test@test.com', @@ -117,6 +119,7 @@ describe('connector validation', () => { }, id: 'test', actionTypeId: '.email', + isPreconfigured: false, name: 'email', config: { from: 'test@test.com', @@ -144,6 +147,7 @@ describe('connector validation', () => { }, id: 'test', actionTypeId: '.email', + isPreconfigured: false, name: 'email', config: { from: 'test@test.com', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx index fdb60bd2d9146..c4489a316d2c0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx @@ -39,6 +39,7 @@ describe('webhook connector validation', () => { id: 'test', actionTypeId: '.webhook', name: 'webhook', + isPreconfigured: false, config: { method: 'PUT', url: 'http:\\test', @@ -106,6 +107,7 @@ describe('WebhookActionConnectorFields renders', () => { }, id: 'test', actionTypeId: '.webhook', + isPreconfigured: false, name: 'webhook', config: { method: 'PUT', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts index ee68b7e269c34..e9cf2a270d180 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts @@ -43,27 +43,14 @@ describe('loadActionTypes', () => { }); describe('loadAllActions', () => { - test('should call find actions API', async () => { - const resolvedValue = { - page: 1, - perPage: 10000, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); + test('should call getAll actions API', async () => { + http.get.mockResolvedValueOnce([]); const result = await loadAllActions({ http }); - expect(result).toEqual(resolvedValue); + expect(result).toEqual([]); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/action/_find", - Object { - "query": Object { - "per_page": 10000, - "sort_field": "name.keyword", - "sort_order": "asc", - }, - }, + "/api/action/_getAll", ] `); }); @@ -73,6 +60,7 @@ describe('createActionConnector', () => { test('should call create action API', async () => { const connector: ActionConnectorWithoutId = { actionTypeId: 'test', + isPreconfigured: false, name: 'My test', config: {}, secrets: {}, @@ -86,7 +74,7 @@ describe('createActionConnector', () => { Array [ "/api/action", Object { - "body": "{\\"actionTypeId\\":\\"test\\",\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{}}", + "body": "{\\"actionTypeId\\":\\"test\\",\\"isPreconfigured\\":false,\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{}}", }, ] `); @@ -98,6 +86,7 @@ describe('updateActionConnector', () => { const id = '123'; const connector: ActionConnectorWithoutId = { actionTypeId: 'test', + isPreconfigured: false, name: 'My test', config: {}, secrets: {}, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts index 26ad97f05849d..e82d268accdd8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts @@ -8,32 +8,12 @@ import { HttpSetup } from 'kibana/public'; import { BASE_ACTION_API_PATH } from '../constants'; import { ActionConnector, ActionConnectorWithoutId, ActionType } from '../../types'; -// We are assuming there won't be many actions. This is why we will load -// all the actions in advance and assume the total count to not go over 100 or so. -// We'll set this max setting assuming it's never reached. -const MAX_ACTIONS_RETURNED = 10000; - export async function loadActionTypes({ http }: { http: HttpSetup }): Promise { return await http.get(`${BASE_ACTION_API_PATH}/types`); } -export async function loadAllActions({ - http, -}: { - http: HttpSetup; -}): Promise<{ - page: number; - perPage: number; - total: number; - data: ActionConnector[]; -}> { - return await http.get(`${BASE_ACTION_API_PATH}/_find`, { - query: { - per_page: MAX_ACTIONS_RETURNED, - sort_field: 'name.keyword', - sort_order: 'asc', - }, - }); +export async function loadAllActions({ http }: { http: HttpSetup }): Promise { + return await http.get(`${BASE_ACTION_API_PATH}/_getAll`); } export async function createActionConnector({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 89d37c4d00a11..41564146bb84d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -11,6 +11,10 @@ import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, Alert, AlertAction } from '../../../types'; import { ActionForm } from './action_form'; +jest.mock('../../lib/action_connector_api', () => ({ + loadAllActions: jest.fn(), + loadActionTypes: jest.fn(), +})); const actionTypeRegistry = actionTypeRegistryMock.create(); describe('action_form', () => { let deps: any; @@ -73,6 +77,17 @@ describe('action_form', () => { let wrapper: ReactWrapper; async function setup() { + const { loadAllActions } = jest.requireMock('../../lib/action_connector_api'); + loadAllActions.mockResolvedValueOnce([ + { + secrets: {}, + id: 'test', + actionTypeId: actionType.id, + name: 'Test connector', + config: {}, + isPreconfigured: false, + }, + ]); const mockes = coreMock.createSetup(); deps = { toastNotifications: mockes.notifications.toasts, 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 3ade4e6368f96..6b011ac84bc6f 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 @@ -129,7 +129,7 @@ export const ActionForm = ({ async function loadConnectors() { try { const actionsResponse = await loadAllActions({ http }); - setConnectors(actionsResponse.data); + setConnectors(actionsResponse); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index f9aa2cad8bfc6..2c063ea6b4fa6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -47,6 +47,7 @@ describe('connector_edit_flyout', () => { actionTypeId: 'test-action-type-id', actionType: 'test-action-type-name', name: 'action-connector', + isPreconfigured: false, referencedByCount: 0, config: {}, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts index df7e5d8fe9a78..e469a50108912 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts @@ -15,6 +15,7 @@ describe('connector reducer', () => { actionTypeId: 'test-action-type-id', name: 'action-connector', referencedByCount: 0, + isPreconfigured: false, config: {}, }; }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 9331fe1704694..4fa1e7e4c6e4d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -29,12 +29,7 @@ describe('actions_connectors_list component empty', () => { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); - loadAllActions.mockResolvedValueOnce({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); + loadAllActions.mockResolvedValueOnce([]); loadActionTypes.mockResolvedValueOnce([ { id: 'test', @@ -111,27 +106,22 @@ describe('actions_connectors_list component with items', () => { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); - loadAllActions.mockResolvedValueOnce({ - page: 1, - perPage: 10000, - total: 2, - data: [ - { - id: '1', - actionTypeId: 'test', - description: 'My test', - referencedByCount: 1, - config: {}, - }, - { - id: '2', - actionTypeId: 'test2', - description: 'My test 2', - referencedByCount: 1, - config: {}, - }, - ], - }); + loadAllActions.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: 'test', + description: 'My test', + referencedByCount: 1, + config: {}, + }, + { + id: '2', + actionTypeId: 'test2', + description: 'My test 2', + referencedByCount: 1, + config: {}, + }, + ]); loadActionTypes.mockResolvedValueOnce([ { id: 'test', @@ -214,12 +204,7 @@ describe('actions_connectors_list component empty with show only capability', () const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); - loadAllActions.mockResolvedValueOnce({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); + loadAllActions.mockResolvedValueOnce([]); loadActionTypes.mockResolvedValueOnce([ { id: 'test', @@ -289,27 +274,22 @@ describe('actions_connectors_list with show only capability', () => { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); - loadAllActions.mockResolvedValueOnce({ - page: 1, - perPage: 10000, - total: 2, - data: [ - { - id: '1', - actionTypeId: 'test', - description: 'My test', - referencedByCount: 1, - config: {}, - }, - { - id: '2', - actionTypeId: 'test2', - description: 'My test 2', - referencedByCount: 1, - config: {}, - }, - ], - }); + loadAllActions.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: 'test', + description: 'My test', + referencedByCount: 1, + config: {}, + }, + { + id: '2', + actionTypeId: 'test2', + description: 'My test 2', + referencedByCount: 1, + config: {}, + }, + ]); loadActionTypes.mockResolvedValueOnce([ { id: 'test', @@ -384,27 +364,22 @@ describe('actions_connectors_list component with disabled items', () => { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); - loadAllActions.mockResolvedValueOnce({ - page: 1, - perPage: 10000, - total: 2, - data: [ - { - id: '1', - actionTypeId: 'test', - description: 'My test', - referencedByCount: 1, - config: {}, - }, - { - id: '2', - actionTypeId: 'test2', - description: 'My test 2', - referencedByCount: 1, - config: {}, - }, - ], - }); + loadAllActions.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: 'test', + description: 'My test', + referencedByCount: 1, + config: {}, + }, + { + id: '2', + actionTypeId: 'test2', + description: 'My test 2', + referencedByCount: 1, + config: {}, + }, + ]); loadActionTypes.mockResolvedValueOnce([ { id: 'test', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index fc07171347e5e..81693e1d2d9d1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -110,7 +110,7 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { setIsLoadingActions(true); try { const actionsResponse = await loadAllActions({ http }); - setActions(actionsResponse.data); + setActions(actionsResponse); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 108cc724aa407..66aa02e1930a3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -72,12 +72,7 @@ describe('alerts_list component empty', () => { }, ]); loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); - loadAllActions.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); + loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); const [ @@ -196,12 +191,7 @@ describe('alerts_list component with items', () => { }, ]); loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); - loadAllActions.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); + loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); const [ { @@ -286,12 +276,7 @@ describe('alerts_list component empty with show only capability', () => { }, ]); loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); - loadAllActions.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); + loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); const [ { @@ -405,12 +390,7 @@ describe('alerts_list with show only capability', () => { }, ]); loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); - loadAllActions.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); + loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); const [ { diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 7dfaa7b918f70..31c77833cc0e8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -66,6 +66,7 @@ export interface ActionConnector { name: string; referencedByCount?: number; config: Record; + isPreconfigured: boolean; } export type ActionConnectorWithoutId = Omit; diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 5fb1afa7d584f..4d32a5ae9f53c 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -77,6 +77,30 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.alerting.enabled=true', '--xpack.eventLog.logEntries=true', + `--xpack.actions.preconfigured=${JSON.stringify([ + { + id: 'my-slack1', + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + }, + { + id: 'custom-system-abc-connector', + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + secrets: { + xyzSecret1: 'credential1', + xyzSecret2: 'credential2', + }, + }, + ])}`, ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts index e228f6c1f81c6..6001dd531cfae 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts @@ -39,6 +39,7 @@ export default function emailTest({ getService }: FtrProviderContext) { createdActionId = createdAction.id; expect(createdAction).to.eql({ id: createdActionId, + isPreconfigured: false, name: 'An email action', actionTypeId: '.email', config: { @@ -58,6 +59,7 @@ export default function emailTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'An email action', actionTypeId: '.email', config: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts index 01eaf92da33fe..612eba858ea0b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts @@ -40,6 +40,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'An index action', actionTypeId: '.index', config: { @@ -57,6 +58,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'An index action', actionTypeId: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: false, executionTimeField: null }, @@ -79,6 +81,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdActionWithIndex).to.eql({ id: createdActionWithIndex.id, + isPreconfigured: false, name: 'An index action with index config', actionTypeId: '.index', config: { @@ -96,6 +99,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(fetchedActionWithIndex).to.eql({ id: fetchedActionWithIndex.id, + isPreconfigured: false, name: 'An index action with index config', actionTypeId: '.index', config: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index cfc04663c6a4f..eeb0818b5fbab 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -50,6 +50,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'A pagerduty action', actionTypeId: '.pagerduty', config: { @@ -65,6 +66,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'A pagerduty action', actionTypeId: '.pagerduty', config: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts index f4ea568cf08c3..e9d3e6c542442 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts @@ -31,6 +31,7 @@ export default function serverLogTest({ getService }: FtrProviderContext) { serverLogActionId = createdAction.id; expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'A server.log action', actionTypeId: '.server-log', config: {}, @@ -44,6 +45,7 @@ export default function serverLogTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'A server.log action', actionTypeId: '.server-log', config: {}, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 48f348e1b834d..054f8f6141817 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -101,6 +101,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'A servicenow action', actionTypeId: '.servicenow', config: { @@ -117,6 +118,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'A servicenow action', actionTypeId: '.servicenow', config: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index 8afa43bfea21e..e00589b7e85b7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -47,6 +47,7 @@ export default function slackTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'A slack action', actionTypeId: '.slack', config: {}, @@ -60,6 +61,7 @@ export default function slackTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'A slack action', actionTypeId: '.slack', config: {}, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index da83dbf8c47e2..fd996ea4507ba 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -92,6 +92,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'A generic Webhook action', actionTypeId: '.webhook', config: { @@ -108,6 +109,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'A generic Webhook action', actionTypeId: '.webhook', config: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts index 43a3861491467..922315eba5a5c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts @@ -55,6 +55,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, response.body.id, 'action'); expect(response.body).to.eql({ id: response.body.id, + isPreconfigured: false, name: 'My action', actionTypeId: 'test.index-record', config: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts index 6fca330887c3e..011e47cf11b39 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts @@ -137,6 +137,36 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it(`shouldn't delete action from preconfigured list`, async () => { + const response = await supertestWithoutAuth + .delete(`${getUrlPrefix(space.id)}/api/action/my-slack1`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo'); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'Preconfigured action my-slack1 is not allowed to delete.', + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts index bed4c805aaf57..c84b089d48c85 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts @@ -59,6 +59,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ id: createdAction.id, + isPreconfigured: false, actionTypeId: 'test.index-record', name: 'My action', config: { @@ -115,6 +116,40 @@ export default function getActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should handle get preconfigured action request appropriately', async () => { + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/action/my-slack1`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + id: 'my-slack1', + actionTypeId: '.slack', + name: 'Slack#xyz', + isPreconfigured: true, + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts similarity index 57% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 89c5b4f451f82..80b512f3fb5e3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -10,11 +10,11 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/l import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export -export default function findActionTests({ getService }: FtrProviderContext) { +export default function getAllActionTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('find', () => { + describe('getAll', () => { const objectRemover = new ObjectRemover(supertest); afterEach(() => objectRemover.removeAll()); @@ -22,7 +22,7 @@ export default function findActionTests({ getService }: FtrProviderContext) { for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; describe(scenario.id, () => { - it('should handle find action request appropriately', async () => { + it('should handle get all action request appropriately', async () => { const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/action`) .set('kbn-xsrf', 'foo') @@ -40,11 +40,7 @@ export default function findActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action'); const response = await supertestWithoutAuth - .get( - `${getUrlPrefix( - space.id - )}/api/action/_find?search=test.index-record&search_fields=actionTypeId` - ) + .get(`${getUrlPrefix(space.id)}/api/action/_getAll`) .auth(user.username, user.password); switch (scenario.id) { @@ -61,90 +57,47 @@ export default function findActionTests({ getService }: FtrProviderContext) { case 'superuser at space1': case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - expect(response.body).to.eql({ - page: 1, - perPage: 20, - total: 1, - data: [ - { - id: createdAction.id, - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - referencedByCount: 0, + expect(response.body).to.eql([ + { + id: createdAction.id, + isPreconfigured: false, + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, }, - ], - }); - break; - default: - throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); - } - }); - - it('should handle find action request with filter appropriately', async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/action`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - secrets: { - encrypted: 'This value should be encrypted', - }, - }) - .expect(200); - objectRemover.add(space.id, createdAction.id, 'action'); - - const response = await supertestWithoutAuth - .get( - `${getUrlPrefix( - space.id - )}/api/action/_find?filter=action.attributes.actionTypeId:test.index-record` - ) - .auth(user.username, user.password); - - switch (scenario.id) { - case 'no_kibana_privileges at space1': - case 'space_1_all at space2': - expect(response.statusCode).to.eql(404); - expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }); - break; - case 'global_read at space1': - case 'superuser at space1': - case 'space_1_all at space1': - expect(response.statusCode).to.eql(200); - expect(response.body).to.eql({ - page: 1, - perPage: 20, - total: 1, - data: [ - { - id: createdAction.id, - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - referencedByCount: 0, + referencedByCount: 0, + }, + { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', }, - ], - }); + referencedByCount: 0, + }, + { + id: 'custom-system-abc-connector', + isPreconfigured: true, + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + referencedByCount: 0, + }, + ]); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); - it('should handle find request appropriately with proper referencedByCount', async () => { + it('should handle get all request appropriately with proper referencedByCount', async () => { const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/action`) .set('kbn-xsrf', 'foo') @@ -172,6 +125,13 @@ export default function findActionTests({ getService }: FtrProviderContext) { id: createdAction.id, params: {}, }, + { + group: 'default', + id: 'my-slack1', + params: { + message: 'test', + }, + }, ], }) ) @@ -179,11 +139,7 @@ export default function findActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAlert.id, 'alert'); const response = await supertestWithoutAuth - .get( - `${getUrlPrefix( - space.id - )}/api/action/_find?filter=action.attributes.actionTypeId:test.index-record` - ) + .get(`${getUrlPrefix(space.id)}/api/action/_getAll`) .auth(user.username, user.password); switch (scenario.id) { @@ -200,29 +156,47 @@ export default function findActionTests({ getService }: FtrProviderContext) { case 'superuser at space1': case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - expect(response.body).to.eql({ - page: 1, - perPage: 20, - total: 1, - data: [ - { - id: createdAction.id, - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - referencedByCount: 1, + expect(response.body).to.eql([ + { + id: createdAction.id, + isPreconfigured: false, + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, }, - ], - }); + referencedByCount: 1, + }, + { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + referencedByCount: 1, + }, + { + id: 'custom-system-abc-connector', + isPreconfigured: true, + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + referencedByCount: 0, + }, + ]); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); - it(`shouldn't find action from another space`, async () => { + it(`shouldn't get actions from another space`, async () => { const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/action`) .set('kbn-xsrf', 'foo') @@ -240,11 +214,7 @@ export default function findActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action'); const response = await supertestWithoutAuth - .get( - `${getUrlPrefix( - 'other' - )}/api/action/_find?search=test.index-record&search_fields=actionTypeId` - ) + .get(`${getUrlPrefix('other')}/api/action/_getAll`) .auth(user.username, user.password); switch (scenario.id) { @@ -261,12 +231,30 @@ export default function findActionTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': expect(response.statusCode).to.eql(200); - expect(response.body).to.eql({ - page: 1, - perPage: 20, - total: 0, - data: [], - }); + expect(response.body).to.eql([ + { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + referencedByCount: 0, + }, + { + id: 'custom-system-abc-connector', + isPreconfigured: true, + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + referencedByCount: 0, + }, + ]); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index c6960a4eedd25..d7ec2e78ccb30 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -19,7 +19,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./execute')); - loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./list_action_types')); loadTestFile(require.resolve('./update')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts index a792efede07ee..6cafbeb8c6ea8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts @@ -69,6 +69,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ id: createdAction.id, + isPreconfigured: false, actionTypeId: 'test.index-record', name: 'My action updated', config: { @@ -307,6 +308,45 @@ export default function updateActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it(`shouldn't update action from preconfigured list`, async () => { + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/action/custom-system-abc-connector`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action updated', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: `Preconfigured action custom-system-abc-connector is not allowed to update.`, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts index 3713e9c24419f..874d42ac04736 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts @@ -38,6 +38,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, + isPreconfigured: false, name: 'An index action', actionTypeId: '.index', config: { @@ -55,6 +56,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, + isPreconfigured: false, name: 'An index action', actionTypeId: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: false, executionTimeField: null }, @@ -77,6 +79,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdActionWithIndex).to.eql({ id: createdActionWithIndex.id, + isPreconfigured: false, name: 'An index action with index config', actionTypeId: '.index', config: { @@ -94,6 +97,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(fetchedActionWithIndex).to.eql({ id: fetchedActionWithIndex.id, + isPreconfigured: false, name: 'An index action with index config', actionTypeId: '.index', config: { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts index efd707b59cd34..c70c289194abb 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts @@ -37,6 +37,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, response.body.id, 'action'); expect(response.body).to.eql({ id: response.body.id, + isPreconfigured: false, name: 'My action', actionTypeId: 'test.index-record', config: { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts index 283e51352c272..26a811d2cc512 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts @@ -76,5 +76,16 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { message: 'Saved object [action/2] not found', }); }); + + it(`shouldn't delete action from preconfigured list`, async () => { + await supertest + .delete(`${getUrlPrefix(Spaces.space1.id)}/api/action/my-slack1`) + .set('kbn-xsrf', 'foo') + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: `Preconfigured action my-slack1 is not allowed to delete.`, + }); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/find.ts deleted file mode 100644 index acbc9edc1f2fb..0000000000000 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/find.ts +++ /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 { Spaces } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default function findActionTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - - describe('find', () => { - const objectRemover = new ObjectRemover(supertest); - - afterEach(() => objectRemover.removeAll()); - - it('should handle find action request appropriately', async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - secrets: { - encrypted: 'This value should be encrypted', - }, - }) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); - - await supertest - .get( - `${getUrlPrefix( - Spaces.space1.id - )}/api/action/_find?search=test.index-record&search_fields=actionTypeId` - ) - .expect(200, { - page: 1, - perPage: 20, - total: 1, - data: [ - { - id: createdAction.id, - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - referencedByCount: 0, - }, - ], - }); - }); - - it(`shouldn't find action from another space`, async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - secrets: { - encrypted: 'This value should be encrypted', - }, - }) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); - - await supertest - .get( - `${getUrlPrefix( - Spaces.other.id - )}/api/action/_find?search=test.index-record&search_fields=actionTypeId` - ) - .expect(200, { - page: 1, - perPage: 20, - total: 0, - data: [], - }); - }); - }); -} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts index 0f896bfaa0af9..a4a13441fb766 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts @@ -38,6 +38,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { .get(`${getUrlPrefix(Spaces.space1.id)}/api/action/${createdAction.id}`) .expect(200, { id: createdAction.id, + isPreconfigured: false, actionTypeId: 'test.index-record', name: 'My action', config: { @@ -71,5 +72,17 @@ export default function getActionTests({ getService }: FtrProviderContext) { message: `Saved object [action/${createdAction.id}] not found`, }); }); + + it('should handle get action request from preconfigured list', async () => { + await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/action/my-slack1`).expect(200, { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + }); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts new file mode 100644 index 0000000000000..517c64f178af5 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -0,0 +1,116 @@ +/* + * 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 { Spaces } from '../../scenarios'; +import { getUrlPrefix, ObjectRemover } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function getAllActionTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('getAll', () => { + const objectRemover = new ObjectRemover(supertest); + + afterEach(() => objectRemover.removeAll()); + + it('should handle get all action request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); + + await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/action/_getAll`).expect(200, [ + { + id: createdAction.id, + isPreconfigured: false, + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + referencedByCount: 0, + }, + { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + referencedByCount: 0, + }, + { + id: 'custom-system-abc-connector', + isPreconfigured: true, + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + referencedByCount: 0, + }, + ]); + }); + + it(`shouldn't get all action from another space`, async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); + + await supertest.get(`${getUrlPrefix(Spaces.other.id)}/api/action/_getAll`).expect(200, [ + { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + referencedByCount: 0, + }, + { + id: 'custom-system-abc-connector', + isPreconfigured: true, + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + config: { + xyzConfig1: 'value1', + xyzConfig2: 'value2', + listOfThings: ['a', 'b', 'c', 'd'], + }, + referencedByCount: 0, + }, + ]); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts index fb2be8c86f4e8..75544b7fd4169 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts @@ -11,7 +11,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { describe('Actions', () => { loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); - loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./list_action_types')); loadTestFile(require.resolve('./update')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts index 18a0ecc23c1e1..2593f342a8a86 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts @@ -63,6 +63,7 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) actionTypeId: 'test.not-enabled', config: {}, id: 'uuid-actionId', + isPreconfigured: false, name: 'an action created before test.not-enabled was disabled', }); }); @@ -89,6 +90,7 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) actionTypeId: 'test.not-enabled', config: {}, id: 'uuid-actionId', + isPreconfigured: false, name: 'an action created before test.not-enabled was disabled', }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts index fb0c5e13c0720..05d26aaaed2ec 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts @@ -48,6 +48,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { }) .expect(200, { id: createdAction.id, + isPreconfigured: false, actionTypeId: 'test.index-record', name: 'My action updated', config: { @@ -99,5 +100,25 @@ export default function updateActionTests({ getService }: FtrProviderContext) { message: `Saved object [action/${createdAction.id}] not found`, }); }); + + it(`shouldn't update action from preconfigured list`, async () => { + await supertest + .put(`${getUrlPrefix(Spaces.space1.id)}/api/action/custom-system-abc-connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action updated', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: `Preconfigured action custom-system-abc-connector is not allowed to update.`, + }); + }); }); } From 9d89a4fb4900a372c2445f0359374da8d9030515 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 8 Apr 2020 13:03:15 -0400 Subject: [PATCH 22/81] Support multiple reserved feature privileges (#61980) * support multiple reserved feature privileges * update reserved privilege ids * additional testing * Add ml_user and ml_admin reserved privileges * prrevent reserved privilege ids from sttarting with 'reserved_' * address pr feedback: dedicated reserved privilege type * re-enable ML test suites Co-authored-by: Elastic Machine --- x-pack/plugins/features/common/feature.ts | 3 +- .../common/reserved_kibana_privilege.ts | 12 + .../features/server/feature_registry.test.ts | 254 +++++++++++++----- .../features/server/feature_registry.ts | 4 +- .../plugins/features/server/feature_schema.ts | 16 +- x-pack/plugins/ml/server/plugin.ts | 35 ++- x-pack/plugins/monitoring/server/plugin.ts | 23 +- .../security/server/authorization/index.ts | 5 +- .../privileges/privileges.test.ts | 51 ++-- .../authorization/privileges/privileges.ts | 10 +- .../validate_feature_privileges.test.ts | 17 +- .../validate_reserved_privileges.test.ts | 181 +++++++++++++ .../validate_reserved_privileges.ts | 20 ++ x-pack/test/api_integration/apis/ml/index.ts | 5 +- .../apis/security/privileges.ts | 2 +- .../apis/security/privileges_basic.ts | 2 +- .../functional/apps/machine_learning/index.ts | 5 +- 17 files changed, 512 insertions(+), 133 deletions(-) create mode 100644 x-pack/plugins/features/common/reserved_kibana_privilege.ts create mode 100644 x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts create mode 100644 x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts diff --git a/x-pack/plugins/features/common/feature.ts b/x-pack/plugins/features/common/feature.ts index 82fcc33f5c8ce..ef32a8a80a0bd 100644 --- a/x-pack/plugins/features/common/feature.ts +++ b/x-pack/plugins/features/common/feature.ts @@ -7,6 +7,7 @@ import { RecursiveReadonly } from '@kbn/utility-types'; import { FeatureKibanaPrivileges } from './feature_kibana_privileges'; import { SubFeatureConfig, SubFeature } from './sub_feature'; +import { ReservedKibanaPrivilege } from './reserved_kibana_privilege'; /** * Interface for registering a feature. @@ -122,8 +123,8 @@ export interface FeatureConfig { * @private */ reserved?: { - privilege: FeatureKibanaPrivileges; description: string; + privileges: ReservedKibanaPrivilege[]; }; } diff --git a/x-pack/plugins/features/common/reserved_kibana_privilege.ts b/x-pack/plugins/features/common/reserved_kibana_privilege.ts new file mode 100644 index 0000000000000..0186011382e84 --- /dev/null +++ b/x-pack/plugins/features/common/reserved_kibana_privilege.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FeatureKibanaPrivileges } from '.'; + +export interface ReservedKibanaPrivilege { + id: string; + privilege: FeatureKibanaPrivileges; +} diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index 5b4f7728c9f31..2039f8f6acda2 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -110,19 +110,24 @@ describe('FeatureRegistry', () => { ], privilegesTooltip: 'some fancy tooltip', reserved: { - privilege: { - catalogue: ['foo'], - management: { - foo: ['bar'], - }, - app: ['app1'], - savedObject: { - all: ['space', 'etc', 'telemetry'], - read: ['canvas', 'config', 'url'], + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['foo'], + management: { + foo: ['bar'], + }, + app: ['app1'], + savedObject: { + all: ['space', 'etc', 'telemetry'], + read: ['canvas', 'config', 'url'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, }, - api: ['someApiEndpointTag', 'anotherEndpointTag'], - ui: ['allowsFoo', 'showBar', 'showBaz'], - }, + ], description: 'some completely adequate description', }, }; @@ -264,13 +269,18 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'foo', - privilege: { - ui: [], - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + ui: [], + savedObject: { + all: [], + read: [], + }, + }, }, - }, + ], }, }; @@ -278,7 +288,7 @@ describe('FeatureRegistry', () => { featureRegistry.register(feature); const result = featureRegistry.getAll(); - const reservedPrivilege = result[0]!.reserved!.privilege; + const reservedPrivilege = result[0]!.reserved!.privileges[0].privilege; expect(reservedPrivilege.savedObject.all).toEqual(['telemetry']); expect(reservedPrivilege.savedObject.read).toEqual(['config', 'url']); }); @@ -520,14 +530,19 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'something', - privilege: { - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar', 'baz'], + }, }, - ui: [], - app: ['foo', 'bar', 'baz'], - }, + ], }, }; @@ -546,14 +561,19 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'something', - privilege: { - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar'], + }, }, - ui: [], - app: ['foo', 'bar'], - }, + ], }, }; @@ -666,15 +686,20 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'something', - privilege: { - catalogue: ['foo', 'bar', 'baz'], - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, }, - ui: [], - app: [], - }, + ], }, }; @@ -694,15 +719,20 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'something', - privilege: { - catalogue: ['foo', 'bar'], - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['foo', 'bar'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, }, - ui: [], - app: [], - }, + ], }, }; @@ -840,18 +870,23 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'something', - privilege: { - catalogue: ['bar'], - management: { - kibana: ['hey-there'], - }, - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['bar'], + management: { + kibana: ['hey-there'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, }, - ui: [], - app: [], - }, + ], }, }; @@ -874,18 +909,23 @@ describe('FeatureRegistry', () => { privileges: null, reserved: { description: 'something', - privilege: { - catalogue: ['bar'], - management: { - kibana: ['hey-there'], - }, - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['bar'], + management: { + kibana: ['hey-there'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, }, - ui: [], - app: [], - }, + ], }, }; @@ -896,6 +936,78 @@ describe('FeatureRegistry', () => { ); }); + it('allows multiple reserved feature privileges to be registered', () => { + const feature: FeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + reserved: { + description: 'my reserved privileges', + privileges: [ + { + id: 'a_reserved_1', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + { + id: 'a_reserved_2', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.register(feature); + const result = featureRegistry.getAll(); + expect(result).toHaveLength(1); + expect(result[0].reserved?.privileges).toHaveLength(2); + }); + + it('does not allow reserved privilege ids to start with "reserved_"', () => { + const feature: FeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + reserved: { + description: 'my reserved privileges', + privileges: [ + { + id: 'reserved_1', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"child \\"reserved\\" fails because [child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"id\\" fails because [\\"id\\" with value \\"reserved_1\\" fails to match the required pattern: /^(?!reserved_)[a-zA-Z0-9_-]+$/]]]]"` + ); + }); + it('cannot register feature after getAll has been called', () => { const feature1: FeatureConfig = { id: 'test-feature', diff --git a/x-pack/plugins/features/server/feature_registry.ts b/x-pack/plugins/features/server/feature_registry.ts index 73a353cd27471..6140b7ac87ce0 100644 --- a/x-pack/plugins/features/server/feature_registry.ts +++ b/x-pack/plugins/features/server/feature_registry.ts @@ -39,9 +39,9 @@ export class FeatureRegistry { function applyAutomaticPrivilegeGrants(feature: FeatureConfig): FeatureConfig { const allPrivilege = feature.privileges?.all; const readPrivilege = feature.privileges?.read; - const reservedPrivilege = feature.reserved?.privilege; + const reservedPrivileges = (feature.reserved?.privileges ?? []).map(rp => rp.privilege); - applyAutomaticAllPrivilegeGrants(allPrivilege, reservedPrivilege); + applyAutomaticAllPrivilegeGrants(allPrivilege, ...reservedPrivileges); applyAutomaticReadPrivilegeGrants(readPrivilege); return feature; diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index fdeceb30b4e3d..403d9586bf160 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -18,6 +18,7 @@ const prohibitedFeatureIds: Array = ['catalogue', 'managem const featurePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/; const subFeaturePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/; const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/; +const reservedFeaturePrrivilegePartRegex = /^(?!reserved_)[a-zA-Z0-9_-]+$/; export const uiCapabilitiesRegex = /^[a-zA-Z0-9:_-]+$/; const managementSchema = Joi.object().pattern( @@ -118,8 +119,17 @@ const schema = Joi.object({ }), privilegesTooltip: Joi.string(), reserved: Joi.object({ - privilege: privilegeSchema.required(), description: Joi.string().required(), + privileges: Joi.array() + .items( + Joi.object({ + id: Joi.string() + .regex(reservedFeaturePrrivilegePartRegex) + .required(), + privilege: privilegeSchema.required(), + }) + ) + .required(), }), }); @@ -209,7 +219,9 @@ export function validateFeature(feature: FeatureConfig) { privilegeEntries.push(...Object.entries(feature.privileges)); } if (feature.reserved) { - privilegeEntries.push(['reserved', feature.reserved.privilege]); + feature.reserved.privileges.forEach(reservedPrivilege => { + privilegeEntries.push([reservedPrivilege.id, reservedPrivilege.privilege]); + }); } if (privilegeEntries.length === 0) { diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 7d3ef116e67ab..c7add12be142c 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -79,19 +79,36 @@ export class MlServerPlugin implements Plugin { - validateFeaturePrivileges(featuresService.getFeatures()); + const features = featuresService.getFeatures(); + validateFeaturePrivileges(features); + validateReservedPrivileges(features); await registerPrivilegesWithCluster(logger, privileges, applicationName, clusterClient); }, diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index 3d25fc03f568b..b023c12d35b79 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -409,13 +409,18 @@ describe('features', () => { }, privileges: null, reserved: { - privilege: { - savedObject: { - all: ['ignore-me-1', 'ignore-me-2'], - read: ['ignore-me-1', 'ignore-me-2'], + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['ignore-me-1', 'ignore-me-2'], + read: ['ignore-me-1', 'ignore-me-2'], + }, + ui: ['ignore-me-1'], + }, }, - ui: ['ignore-me-1'], - }, + ], description: '', }, }), @@ -591,13 +596,18 @@ describe('reserved', () => { }, privileges: null, reserved: { - privilege: { - savedObject: { - all: [], - read: [], + privileges: [ + { + id: 'foo', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, }, - ui: [], - }, + ], description: '', }, }), @@ -627,13 +637,18 @@ describe('reserved', () => { app: [], privileges: null, reserved: { - privilege: { - savedObject: { - all: ['savedObject-all-1', 'savedObject-all-2'], - read: ['savedObject-read-1', 'savedObject-read-2'], + privileges: [ + { + id: 'foo', + privilege: { + savedObject: { + all: ['savedObject-all-1', 'savedObject-all-2'], + read: ['savedObject-read-1', 'savedObject-read-2'], + }, + ui: ['ui-1', 'ui-2'], + }, }, - ui: ['ui-1', 'ui-2'], - }, + ], description: '', }, }), diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index b25aad30a3423..9a8935f80a174 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -110,10 +110,12 @@ export function privilegesFactory( }, reserved: features.reduce((acc: Record, feature: Feature) => { if (feature.reserved) { - acc[feature.id] = [ - actions.version, - ...featurePrivilegeBuilder.getActions(feature.reserved!.privilege, feature), - ]; + feature.reserved.privileges.forEach(reservedPrivilege => { + acc[reservedPrivilege.id] = [ + actions.version, + ...uniq(featurePrivilegeBuilder.getActions(reservedPrivilege.privilege, feature)), + ]; + }); } return acc; }, {}), diff --git a/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts index ac386d287cff1..cd2c7faa263c9 100644 --- a/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts @@ -26,13 +26,18 @@ it('allows features with reserved privileges to be defined', () => { privileges: null, reserved: { description: 'foo', - privilege: { - savedObject: { - all: ['foo'], - read: ['bar'], + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, }, - ui: [], - }, + ], }, }); diff --git a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts new file mode 100644 index 0000000000000..26af0dadfb288 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts @@ -0,0 +1,181 @@ +/* + * 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 { Feature } from '../../../features/server'; +import { validateReservedPrivileges } from './validate_reserved_privileges'; + +it('allows features to be defined without privileges', () => { + const feature: Feature = new Feature({ + id: 'foo', + name: 'foo', + app: [], + privileges: null, + }); + + validateReservedPrivileges([feature]); +}); + +it('allows features with a single reserved privilege to be defined', () => { + const feature: Feature = new Feature({ + id: 'foo', + name: 'foo', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + ], + }, + }); + + validateReservedPrivileges([feature]); +}); + +it('allows multiple features with reserved privileges to be defined', () => { + const feature1: Feature = new Feature({ + id: 'foo', + name: 'foo', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved-1', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + ], + }, + }); + + const feature2: Feature = new Feature({ + id: 'foo2', + name: 'foo', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved-2', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + ], + }, + }); + + validateReservedPrivileges([feature1, feature2]); +}); + +it('prevents a feature from specifying the same reserved privilege id', () => { + const feature1: Feature = new Feature({ + id: 'foo', + name: 'foo', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + { + id: 'reserved', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + ], + }, + }); + + expect(() => validateReservedPrivileges([feature1])).toThrowErrorMatchingInlineSnapshot( + `"Duplicate reserved privilege id detected: reserved. This is not allowed."` + ); +}); + +it('prevents features from sharing a reserved privilege id', () => { + const feature1: Feature = new Feature({ + id: 'foo', + name: 'foo', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + ], + }, + }); + + const feature2: Feature = new Feature({ + id: 'foo2', + name: 'foo', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['foo'], + read: ['bar'], + }, + ui: [], + }, + }, + ], + }, + }); + + expect(() => validateReservedPrivileges([feature1, feature2])).toThrowErrorMatchingInlineSnapshot( + `"Duplicate reserved privilege id detected: reserved. This is not allowed."` + ); +}); diff --git a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts new file mode 100644 index 0000000000000..0915308fc0f89 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Feature } from '../../../features/server'; + +export function validateReservedPrivileges(features: Feature[]) { + const seenPrivilegeIds = new Set(); + + for (const feature of features) { + (feature?.reserved?.privileges ?? []).forEach(({ id }) => { + if (seenPrivilegeIds.has(id)) { + throw new Error(`Duplicate reserved privilege id detected: ${id}. This is not allowed.`); + } + seenPrivilegeIds.add(id); + }); + } +} diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index bcba156a64f0e..4e21faa610bfe 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -9,10 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); - // ML tests need to be disabled in orde to get the ES snapshot with - // https://github.com/elastic/elasticsearch/pull/54713 promoted - // and should be re-enabled as part of https://github.com/elastic/kibana/pull/61980 - describe.skip('Machine Learning', function() { + describe('Machine Learning', function() { this.tags(['mlqa']); before(async () => { diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 77293ddff3f9f..9bec3fd076e86 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -41,7 +41,7 @@ export default function({ getService }: FtrProviderContext) { }, global: ['all', 'read'], space: ['all', 'read'], - reserved: ['ml', 'monitoring'], + reserved: ['ml_user', 'ml_admin', 'monitoring'], }; await supertest diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 0b29fc1cac7de..1f9eac148b302 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -39,7 +39,7 @@ export default function({ getService }: FtrProviderContext) { }, global: ['all', 'read'], space: ['all', 'read'], - reserved: ['ml', 'monitoring'], + reserved: ['ml_user', 'ml_admin', 'monitoring'], }; await supertest diff --git a/x-pack/test/functional/apps/machine_learning/index.ts b/x-pack/test/functional/apps/machine_learning/index.ts index 143899a61ffb9..47c699e309491 100644 --- a/x-pack/test/functional/apps/machine_learning/index.ts +++ b/x-pack/test/functional/apps/machine_learning/index.ts @@ -8,10 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); - // ML tests need to be disabled in orde to get the ES snapshot with - // https://github.com/elastic/elasticsearch/pull/54713 promoted - // and should be re-enabled as part of https://github.com/elastic/kibana/pull/61980 - describe.skip('machine learning', function() { + describe('machine learning', function() { this.tags('ciGroup3'); before(async () => { From 3e4469c99c28f8bb1b517bf2ed41539af73f4ffa Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 8 Apr 2020 13:13:40 -0400 Subject: [PATCH 23/81] [ML] Analytics: ensure both keyword/text types are excluded for selected excluded field (#62712) * exclude keyword and text types of field selected for exclusion * only show keyword type fields of accepted fields for depVar * make excludes field logic generic * fix regex to ensure escaped dot. reset regex and mainfield * ensure cloned jobs get correct excluded fields * add clarifying comments --- .../create_analytics_form.tsx | 2 +- .../form_options_validation.ts | 2 +- .../hooks/use_create_analytics_form/state.ts | 52 ++++++++++++++++++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index e5f30a50ed8f0..0c83dfb6a2346 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -250,7 +250,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta dependentVariableOptions: [] as State['form']['dependentVariableOptions'], }; - await newJobCapsService.initializeFromIndexPattern(indexPattern); + await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false); // Get fields and filter for supported types for job type const { fields } = newJobCapsService; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts index 9c0bc69f4b41f..4bd03fec7cc72 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts @@ -10,7 +10,7 @@ import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; import { AnalyticsJobType } from '../../hooks/use_create_analytics_form/state'; import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../common/fields'; -const CATEGORICAL_TYPES = new Set(['ip', 'keyword', 'text']); +const CATEGORICAL_TYPES = new Set(['ip', 'keyword']); // List of system fields we want to ignore for the numeric field check. export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score']; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 01a39d2ef9f3b..e121268e65e86 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -8,6 +8,7 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission } from '../../../../../privilege/check_privilege'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; +import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { isClassificationAnalysis, @@ -158,6 +159,55 @@ export const getInitialState = (): State => ({ estimatedModelMemoryLimit: '', }); +const getExcludesFields = (excluded: string[]) => { + const { fields } = newJobCapsService; + const updatedExcluded: string[] = []; + // Loop through excluded fields to check for multiple types of same field + for (let i = 0; i < excluded.length; i++) { + const fieldName = excluded[i]; + let mainField; + + // No dot in fieldName - it is the main field + if (fieldName.includes('.') === false) { + mainField = fieldName; + } else { + // Dot in fieldName - check if there's a field whose name equals the fieldName with the last dot suffix removed + const regex = /\.[^.]*$/; + const suffixRemovedField = fieldName.replace(regex, ''); + const fieldMatch = newJobCapsService.getFieldById(suffixRemovedField); + + // There's a match - set as the main field + if (fieldMatch !== null) { + mainField = suffixRemovedField; + } else { + // No main field to be found - add the fieldName to updatedExcluded array if it's not already there + if (updatedExcluded.includes(fieldName) === false) { + updatedExcluded.push(fieldName); + } + } + } + + if (mainField !== undefined) { + // Add the main field to the updatedExcluded array if it's not already there + if (updatedExcluded.includes(mainField) === false) { + updatedExcluded.push(mainField); + } + // Create regex to find all other fields whose names begin with main field followed by a dot + const regex = new RegExp(`${mainField}\\..+`); + + // Loop through fields and add fields matching the pattern to updatedExcluded array + for (let j = 0; j < fields.length; j++) { + const field = fields[j].name; + if (updatedExcluded.includes(field) === false && field.match(regex) !== null) { + updatedExcluded.push(field); + } + } + } + } + + return updatedExcluded; +}; + export const getJobConfigFromFormState = ( formState: State['form'] ): DeepPartial => { @@ -175,7 +225,7 @@ export const getJobConfigFromFormState = ( index: formState.destinationIndex, }, analyzed_fields: { - excludes: formState.excludes, + excludes: getExcludesFields(formState.excludes), }, analysis: { outlier_detection: {}, From 56fa9a5776631ad1699393ae323f9e0ebabb117f Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 8 Apr 2020 10:17:05 -0700 Subject: [PATCH 24/81] skip flaky suite (#59030) --- .../components/load_mappings/load_mappings_provider.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx index da4b8e6f6eef2..95630a6981843 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx @@ -51,7 +51,8 @@ const openModalWithJsonContent = ({ find, waitFor }: TestBed) => async (json: an }); }; -describe('', () => { +// FLAKY: https://github.com/elastic/kibana/issues/59030 +describe.skip('', () => { test('it should forward valid mapping definition', async () => { const mappingsToLoad = { properties: { From bfdccfdbc54f98e0c41cfc22665c5e424ec462a0 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 8 Apr 2020 11:22:52 -0600 Subject: [PATCH 25/81] [Maps] Show create filter button for top-term tooltip property (#62461) * [Maps] Show create filter button top-term tooltip property * add missing imports * update import for NP migration Co-authored-by: Elastic Machine --- .../maps/public/layers/fields/es_agg_field.ts | 2 +- .../layers/tooltips/es_agg_tooltip_property.ts | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/maps/public/layers/fields/es_agg_field.ts b/x-pack/plugins/maps/public/layers/fields/es_agg_field.ts index 65f952ca01038..34f7dd4b9578f 100644 --- a/x-pack/plugins/maps/public/layers/fields/es_agg_field.ts +++ b/x-pack/plugins/maps/public/layers/fields/es_agg_field.ts @@ -90,7 +90,7 @@ export class ESAggField implements IESAggField { async createTooltipProperty(value: string | undefined): Promise { const indexPattern = await this._source.getIndexPattern(); const tooltipProperty = new TooltipProperty(this.getName(), await this.getLabel(), value); - return new ESAggTooltipProperty(tooltipProperty, indexPattern, this); + return new ESAggTooltipProperty(tooltipProperty, indexPattern, this, this.getAggType()); } getValueAggDsl(indexPattern: IndexPattern): unknown | null { diff --git a/x-pack/plugins/maps/public/layers/tooltips/es_agg_tooltip_property.ts b/x-pack/plugins/maps/public/layers/tooltips/es_agg_tooltip_property.ts index 24011c51ddbaa..acd05475f9762 100644 --- a/x-pack/plugins/maps/public/layers/tooltips/es_agg_tooltip_property.ts +++ b/x-pack/plugins/maps/public/layers/tooltips/es_agg_tooltip_property.ts @@ -4,9 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ import { ESTooltipProperty } from './es_tooltip_property'; +import { AGG_TYPE } from '../../../common/constants'; +import { ITooltipProperty } from './tooltip_property'; +import { IField } from '../fields/field'; +import { IndexPattern } from '../../../../../../src/plugins/data/public'; export class ESAggTooltipProperty extends ESTooltipProperty { + private readonly _aggType: AGG_TYPE; + + constructor( + tooltipProperty: ITooltipProperty, + indexPattern: IndexPattern, + field: IField, + aggType: AGG_TYPE + ) { + super(tooltipProperty, indexPattern, field); + this._aggType = aggType; + } + isFilterable(): boolean { - return false; + return this._aggType === AGG_TYPE.TERMS; } } From 8cacbdfaa5242c3b251ac334c1a31a98da6933a2 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 8 Apr 2020 19:23:16 +0200 Subject: [PATCH 26/81] [Uptime]Alerting UI text in case filter is selected (#62570) Co-authored-by: Elastic Machine --- .../alerts/uptime_alerts_flyout_wrapper.tsx | 4 ++-- .../functional/alerts/alert_monitor_status.tsx | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/alerts/uptime_alerts_flyout_wrapper.tsx index b547f8b076f93..a49468ad3dd06 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/alerts/uptime_alerts_flyout_wrapper.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/alerts/uptime_alerts_flyout_wrapper.tsx @@ -17,7 +17,7 @@ interface Props { export const UptimeAlertsFlyoutWrapper = ({ alertTypeId, canChangeTrigger }: Props) => { const dispatch = useDispatch(); - const setAddFlyoutVisiblity = (value: React.SetStateAction) => + const setAddFlyoutVisibility = (value: React.SetStateAction) => // @ts-ignore the value here is a boolean, and it works with the action creator function dispatch(setAlertFlyoutVisible(value)); @@ -28,7 +28,7 @@ export const UptimeAlertsFlyoutWrapper = ({ alertTypeId, canChangeTrigger }: Pro alertFlyoutVisible={alertFlyoutVisible} alertTypeId={alertTypeId} canChangeTrigger={canChangeTrigger} - setAlertFlyoutVisibility={setAddFlyoutVisiblity} + setAlertFlyoutVisibility={setAddFlyoutVisibility} /> ); }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/alert_monitor_status.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/alerts/alert_monitor_status.tsx index 5143e1c963904..b86e85f35b17d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/alert_monitor_status.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/alerts/alert_monitor_status.tsx @@ -268,7 +268,21 @@ export const AlertMonitorStatusComponent: React.FC = pr /> } data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression" - description="any monitor is down >" + 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`} /> From 3598b8c44c84165e282aeff234f51417c3c54f96 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 8 Apr 2020 11:24:04 -0600 Subject: [PATCH 27/81] [Maps] fix attribution overflow with exit full screen button (#62699) * [Maps] fix attribution overflow with exit full screen button * use margin-left instead of padding-left Co-authored-by: Elastic Machine --- .../attribution_control/_attribution_control.scss | 4 ++++ .../widget_overlay/attribution_control/index.js | 2 ++ .../widget_overlay/attribution_control/view.js | 7 ++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss index 9ebaee57fba4d..e319535b4a45c 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss @@ -4,3 +4,7 @@ pointer-events: all; padding-left: $euiSizeM; } + +.mapAttributionControl__fullScreen { + margin-left: $euiSizeXXL * 4; +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js index e73a51ffa2ced..8bad536b39245 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js @@ -7,10 +7,12 @@ import { connect } from 'react-redux'; import { AttributionControl } from './view'; import { getLayerList } from '../../../selectors/map_selectors'; +import { getIsFullScreen } from '../../../selectors/ui_selectors'; function mapStateToProps(state = {}) { return { layerList: getLayerList(state), + isFullScreen: getIsFullScreen(state), }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js index 161b5b81c1255..8f11d1b23376c 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js @@ -7,6 +7,7 @@ import React, { Fragment } from 'react'; import _ from 'lodash'; import { EuiText, EuiLink } from '@elastic/eui'; +import classNames from 'classnames'; export class AttributionControl extends React.Component { state = { @@ -86,7 +87,11 @@ export class AttributionControl extends React.Component { return null; } return ( -
+
{this._renderAttributions()} From 1c718d676049d77168b5c326a4a92543ce158e8d Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 8 Apr 2020 11:30:12 -0600 Subject: [PATCH 28/81] Add --filter option to API docs script (#62888) --- src/dev/run_check_published_api_changes.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/dev/run_check_published_api_changes.ts b/src/dev/run_check_published_api_changes.ts index 8bb3fb20cea8b..ba3cd1280f34b 100644 --- a/src/dev/run_check_published_api_changes.ts +++ b/src/dev/run_check_published_api_changes.ts @@ -163,6 +163,7 @@ interface Options { accept: boolean; docs: boolean; help: boolean; + filter: string; } async function run( @@ -205,6 +206,7 @@ async function run( const extraFlags: string[] = []; const opts = (getopts(process.argv.slice(2), { boolean: ['accept', 'docs', 'help'], + string: ['filter'], default: { project: undefined, }, @@ -222,6 +224,8 @@ async function run( opts.help = true; } + const folders = ['core/public', 'core/server', 'plugins/data/server', 'plugins/data/public']; + if (opts.help) { process.stdout.write( dedent(chalk` @@ -240,9 +244,13 @@ async function run( {dim # Checks for and automatically accepts and updates documentation for any changes to the Kibana Core API} {dim $} node scripts/check_published_api_changes --accept + {dim # Only checks the core/public directory} + {dim $} node scripts/check_published_api_changes --filter=core/public + Options: --accept {dim Accepts all changes by updating the API Review files and documentation} --docs {dim Updates the Core API documentation} + --only {dim RegExp that folder names must match, folders: [${folders.join(', ')}]} --help {dim Show this message} `) ); @@ -258,9 +266,11 @@ async function run( return false; } - const folders = ['core/public', 'core/server', 'plugins/data/server', 'plugins/data/public']; - - const results = await Promise.all(folders.map(folder => run(folder, { log, opts }))); + const results = await Promise.all( + folders + .filter(folder => (opts.filter.length ? folder.match(opts.filter) : true)) + .map(folder => run(folder, { log, opts })) + ); if (results.find(r => r === false) !== undefined) { process.exitCode = 1; From cbe479b8bd1297a5f54b2b9a3f8ea9f480cbb713 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 8 Apr 2020 12:39:59 -0500 Subject: [PATCH 29/81] [Metrics UI] Invalidate non-count alerts which have no metrics (#62837) --- .../public/components/alerting/metrics/validation.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx index 55365c3d4c2ba..d84e46d08a287 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx @@ -23,6 +23,7 @@ export function validateMetricThreshold({ timeWindowSize: string[]; threshold0: string[]; threshold1: string[]; + metric: string[]; }; } = {}; validationResult.errors = errors; @@ -41,6 +42,7 @@ export function validateMetricThreshold({ timeWindowSize: [], threshold0: [], threshold1: [], + metric: [], }; if (!c.aggType) { errors[id].aggField.push( @@ -73,6 +75,14 @@ export function validateMetricThreshold({ }) ); } + + if (!c.metric && c.aggType !== 'count') { + errors[id].metric.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.metricRequired', { + defaultMessage: 'Metric is required.', + }) + ); + } }); return validationResult; From 941a4879ae2072344cc3be5846c47c8e2340d153 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 8 Apr 2020 12:40:27 -0500 Subject: [PATCH 30/81] [Alerting] Fix validation support for nested IErrorObjects (#62833) * [Alerting] Add validation support for nested IErrorObjects * Typecheck fix * Fix recursion crash when errors are strings * Typecheck fix --- .../public/application/sections/alert_form/alert_add.tsx | 9 ++++++++- .../public/common/expression_items/of.tsx | 3 ++- .../public/common/expression_items/threshold.tsx | 3 ++- x-pack/plugins/triggers_actions_ui/public/types.ts | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index e44e20751b315..e025248fad52e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useCallback, useReducer, useState } from 'react'; +import { isObject } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -83,7 +84,7 @@ export const AlertAdd = ({ ...(alertType ? alertType.validate(alert.params).errors : []), ...validateBaseProperties(alert).errors, } as IErrorObject; - const hasErrors = !!Object.keys(errors).find(errorKey => errors[errorKey].length >= 1); + const hasErrors = parseErrors(errors); const actionsErrors: Array<{ errors: IErrorObject; @@ -213,3 +214,9 @@ export const AlertAdd = ({ ); }; + +const parseErrors: (errors: IErrorObject) => boolean = errors => + !!Object.values(errors).find(errorList => { + if (isObject(errorList)) return parseErrors(errorList as IErrorObject); + return errorList.length >= 1; + }); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx index d399f6136690b..3de0a99ccbbd9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx @@ -17,13 +17,14 @@ import { } from '@elastic/eui'; import { builtInAggregationTypes } from '../constants'; import { AggregationType } from '../types'; +import { IErrorObject } from '../../types'; import { ClosablePopoverTitle } from './components'; import './of.scss'; interface OfExpressionProps { aggType: string; aggField?: string; - errors: { [key: string]: string[] }; + errors: IErrorObject; onChangeSelectedAggField: (selectedAggType?: string) => void; fields: Record; customAggTypesOptions?: { diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx index fb3ff9ceb0926..99dd7b63383fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx @@ -18,11 +18,12 @@ import { } from '@elastic/eui'; import { builtInComparators } from '../constants'; import { Comparator } from '../types'; +import { IErrorObject } from '../../types'; import { ClosablePopoverTitle } from './components'; interface ThresholdExpressionProps { thresholdComparator: string; - errors: { [key: string]: string[] }; + errors: IErrorObject; onChangeSelectedThresholdComparator: (selectedThresholdComparator?: string) => void; onChangeSelectedThreshold: (selectedThreshold?: number[]) => void; customComparators?: { diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 31c77833cc0e8..7f78d327d0122 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -112,5 +112,5 @@ export interface AlertTypeModel { } export interface IErrorObject { - [key: string]: string[]; + [key: string]: string | string[] | IErrorObject; } From d4f2bd744dc64eb530277927c67807c2a5fb9ba2 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 8 Apr 2020 10:45:15 -0700 Subject: [PATCH 31/81] Exclude disabled datasources and streams from agent config (#62869) --- .../datasource_to_agent_datasource.test.ts | 42 ++++++++++++++++++- .../datasource_to_agent_datasource.ts | 18 ++++---- .../server/services/agent_config.ts | 6 +-- 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts index ab039be8e7c22..b897c03e89f82 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts @@ -41,7 +41,7 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }, { id: 'test-logs-bar', - enabled: false, + enabled: true, dataset: 'bar', config: { barVar: { value: 'bar-value' }, @@ -119,7 +119,7 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }, { id: 'test-logs-bar', - enabled: false, + enabled: true, dataset: 'bar', barVar: 'bar-value', barVar2: [1, 2], @@ -140,6 +140,44 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }); }); + it('returns agent datasource config without disabled streams', () => { + expect( + storedDatasourceToAgentDatasource({ + ...mockDatasource, + inputs: [ + { + ...mockInput, + streams: [{ ...mockInput.streams[0] }, { ...mockInput.streams[1], enabled: false }], + }, + ], + }) + ).toEqual({ + id: 'mock-datasource', + namespace: 'default', + enabled: true, + use_output: 'default', + inputs: [ + { + type: 'test-logs', + enabled: true, + inputVar: 'input-value', + inputVar3: { + testField: 'test', + }, + streams: [ + { + id: 'test-logs-foo', + enabled: true, + dataset: 'foo', + fooVar: 'foo-value', + fooVar2: [1, 2], + }, + ], + }, + ], + }); + }); + it('returns agent datasource config without disabled inputs', () => { expect( storedDatasourceToAgentDatasource({ diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts index f58eaacb7be67..20bbbec8919d6 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts @@ -51,14 +51,16 @@ export const storedDatasourceToAgentDatasource = ( const fullInput = { ...input, ...Object.entries(input.config || {}).reduce(configReducer, {}), - streams: input.streams.map(stream => { - const fullStream = { - ...stream, - ...Object.entries(stream.config || {}).reduce(configReducer, {}), - }; - delete fullStream.config; - return fullStream; - }), + streams: input.streams + .filter(stream => stream.enabled) + .map(stream => { + const fullStream = { + ...stream, + ...Object.entries(stream.config || {}).reduce(configReducer, {}), + }; + delete fullStream.config; + return fullStream; + }), }; delete fullInput.config; return fullInput; diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index a941494072ae3..309ddca3784c2 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -319,9 +319,9 @@ class AgentConfigService { return outputs; }, {} as FullAgentConfig['outputs']), }, - datasources: (config.datasources as Datasource[]).map(ds => - storedDatasourceToAgentDatasource(ds) - ), + datasources: (config.datasources as Datasource[]) + .filter(datasource => datasource.enabled) + .map(ds => storedDatasourceToAgentDatasource(ds)), revision: config.revision, }; From 184f59447bfb1cb5ffc83e0e8d1df8bfe282f9d8 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 8 Apr 2020 11:13:39 -0700 Subject: [PATCH 32/81] [APM] Service map - fixes layout issues for maps with no rum services (#62887) * Closes #62878 in Service Maps by improving the selection algorithm for root nodes * Fixes some latent centering issues when navigating in the service map. * Removes unused imports * Added layoutstopDelayTimeout to cleanup step --- .../components/app/ServiceMap/Cytoscape.tsx | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index 21bedc204f48b..e4b656ae8160d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -14,8 +14,6 @@ import React, { useState } from 'react'; import { debounce } from 'lodash'; -import { isRumAgentName } from '../../../../../../../plugins/apm/common/agent_name'; -import { AGENT_NAME } from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; import { animationOptions, cytoscapeOptions, @@ -96,10 +94,15 @@ function getLayoutOptions( } function selectRoots(cy: cytoscape.Core): string[] { - const nodes = cy.nodes(); - const roots = nodes.roots(); - const rumNodes = nodes.filter(node => isRumAgentName(node.data(AGENT_NAME))); - return rumNodes.union(roots).map(node => node.id()); + const bfs = cy.elements().bfs({ + roots: cy.elements().leaves() + }); + const furthestNodeFromLeaves = bfs.path.last(); + return cy + .elements() + .roots() + .union(furthestNodeFromLeaves) + .map(el => el.id()); } export function Cytoscape({ @@ -168,15 +171,26 @@ export function Cytoscape({ layout.run(); } }; + let layoutstopDelayTimeout: NodeJS.Timeout; const layoutstopHandler: cytoscape.EventHandler = event => { - event.cy.animate({ - ...animationOptions, - center: { - eles: serviceName - ? event.cy.getElementById(serviceName) - : event.cy.collection() + // This 0ms timer is necessary to prevent a race condition + // between the layout finishing rendering and viewport centering + layoutstopDelayTimeout = setTimeout(() => { + if (serviceName) { + event.cy.animate({ + ...animationOptions, + fit: { + eles: event.cy.elements(), + padding: nodeHeight + }, + center: { + eles: event.cy.getElementById(serviceName) + } + }); + } else { + event.cy.fit(undefined, nodeHeight); } - }); + }, 0); }; // debounce hover tracking so it doesn't spam telemetry with redundant events const trackNodeEdgeHover = debounce( @@ -231,6 +245,7 @@ export function Cytoscape({ cy.removeListener('select', 'node', selectHandler); cy.removeListener('unselect', 'node', unselectHandler); } + clearTimeout(layoutstopDelayTimeout); }; }, [cy, height, serviceName, trackApmEvent, width]); From 369ddff95192373923fb5323ef06b70868303c4c Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 8 Apr 2020 12:15:48 -0700 Subject: [PATCH 33/81] [kbn/optimizer] link to kibanaReact/kibanaUtils plugins (#62720) * [kbn/optimizer] link to data/kibanaReact/kibanaUtils plugins * depend on normalize-path package * typos * avoid loading kibanaUtils and kibanaReact from urls * update types and tests, now that whole plugin is exported to window * update snapshot, removed export of `plugins` property * fix condition, ignore things NOT in data/react/utils * make es_ui_shared a "static bundle" too * move kibana_utils/common usage to /public * convert some more /common usage to /public * use async-download/ordered-execution for bootstrap script * fix typo * remove kibanaUtils bundle * remove kibanaReact bundle * Revert "remove kibanaReact bundle" This reverts commit f14e9ee604ae8f0bb664a04c6966e337254d2659. * Revert "remove kibanaUtils bundle" This reverts commit a64b2a7f647f44f6d1941dba4ba39076ce5d74d9. * stop linking to the data plugin * add comment pointing to async-download info Co-authored-by: spalger Co-authored-by: Elastic Machine --- packages/kbn-optimizer/package.json | 1 + .../basic_optimization.test.ts.snap | 4 +- .../src/worker/webpack.config.ts | 68 ++++++++++++++++-- packages/kbn-ui-shared-deps/polyfills.js | 7 ++ src/core/public/plugins/plugin_loader.test.ts | 2 +- src/core/public/plugins/plugin_loader.ts | 33 ++++++--- .../public/input_control_vis_type.ts | 2 +- .../kibana/public/discover/kibana_services.ts | 4 +- .../vis_type_metric/public/services.ts | 2 +- .../vis_type_table/public/services.ts | 2 +- .../vis_type_tagcloud/public/services.ts | 2 +- .../public/metrics_type.ts | 2 +- .../public/__mocks__/services.ts | 2 +- .../vis_type_vega/public/vega_type.ts | 2 +- .../vis_type_vislib/public/services.ts | 2 +- .../ui/ui_render/bootstrap/template.js.hbs | 69 ++++++++----------- src/plugins/discover/public/services.ts | 2 +- .../embeddable_action_storage.test.ts | 2 +- src/plugins/es_ui_shared/kibana.json | 5 ++ src/plugins/es_ui_shared/public/index.ts | 8 +++ src/plugins/kibana_react/kibana.json | 5 ++ .../public/adapters/react_to_ui_component.ts | 2 +- .../adapters/ui_to_react_component.test.tsx | 2 +- .../public/adapters/ui_to_react_component.ts | 2 +- src/plugins/kibana_react/public/index.ts | 8 +++ src/plugins/kibana_utils/kibana.json | 5 ++ src/plugins/kibana_utils/public/index.ts | 11 ++- .../ui_actions/public/actions/action.ts | 2 +- .../public/actions/action_definition.ts | 2 +- 29 files changed, 182 insertions(+), 78 deletions(-) create mode 100644 src/plugins/es_ui_shared/kibana.json create mode 100644 src/plugins/kibana_react/kibana.json create mode 100644 src/plugins/kibana_utils/kibana.json diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index b648004760d7c..b3e5a8c518682 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -32,6 +32,7 @@ "json-stable-stringify": "^1.0.1", "loader-utils": "^1.2.3", "node-sass": "^4.13.0", + "normalize-path": "^3.0.0", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "resolve-url-loader": "^3.1.1", diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index d52d89eebe2f1..4b4bb1282d939 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -57,6 +57,6 @@ OptimizerConfig { } `; -exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i bundle.id === p.id)) { + return; + } + + // ignore requests that don't include a /data/public, /kibana_react/public, or + // /kibana_utils/public segment as a cheap way to avoid doing path resolution + // for paths that couldn't possibly resolve to what we're looking for + const reqToStaticBundle = STATIC_BUNDLE_PLUGINS.some(p => + request.includes(`/${p.dirname}/public`) + ); + if (!reqToStaticBundle) { + return; + } + + // determine the most acurate resolution string we can without running full resolution + const rootRelative = normalizePath( + Path.relative(bundle.sourceRoot, Path.resolve(context, request)) + ); + for (const { id, dirname } of STATIC_BUNDLE_PLUGINS) { + if (rootRelative === `src/plugins/${dirname}/public`) { + return `__kbnBundles__['plugin/${id}']`; + } + } + + // import doesn't match a root public import + return undefined; +} + export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { const commonConfig: webpack.Configuration = { node: { fs: 'empty' }, @@ -63,7 +117,6 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { // When the entry point is loaded, assign it's exported `plugin` // value to a key on the global `__kbnBundles__` object. library: ['__kbnBundles__', `plugin/${bundle.id}`], - libraryExport: 'plugin', } : {}), }, @@ -72,9 +125,16 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { noEmitOnErrors: true, }, - externals: { - ...UiSharedDeps.externals, - }, + externals: [ + UiSharedDeps.externals, + function(context, request, cb) { + try { + cb(undefined, dynamicExternals(bundle, context, request)); + } catch (error) { + cb(error, undefined); + } + }, + ], plugins: [new CleanWebpackPlugin(), new DisallowedSyntaxPlugin()], diff --git a/packages/kbn-ui-shared-deps/polyfills.js b/packages/kbn-ui-shared-deps/polyfills.js index d2305d643e4d2..1f1392b02baff 100644 --- a/packages/kbn-ui-shared-deps/polyfills.js +++ b/packages/kbn-ui-shared-deps/polyfills.js @@ -20,6 +20,13 @@ require('core-js/stable'); require('regenerator-runtime/runtime'); require('custom-event-polyfill'); + +if (typeof window.Event === 'object') { + // IE11 doesn't support unknown event types, required by react-use + // https://github.com/streamich/react-use/issues/73 + window.Event = CustomEvent; +} + require('whatwg-fetch'); require('abortcontroller-polyfill/dist/polyfill-patch-fetch'); require('./vendor/childnode_remove_polyfill'); diff --git a/src/core/public/plugins/plugin_loader.test.ts b/src/core/public/plugins/plugin_loader.test.ts index e5cbffc3e2d94..b4e2c3095f14a 100644 --- a/src/core/public/plugins/plugin_loader.test.ts +++ b/src/core/public/plugins/plugin_loader.test.ts @@ -71,7 +71,7 @@ test('`loadPluginBundles` creates a script tag and loads initializer', async () // Setup a fake initializer as if a plugin bundle had actually been loaded. const fakeInitializer = jest.fn(); - coreWindow.__kbnBundles__['plugin/plugin-a'] = fakeInitializer; + coreWindow.__kbnBundles__['plugin/plugin-a'] = { plugin: fakeInitializer }; // Call the onload callback fakeScriptTag.onload(); await expect(loadPromise).resolves.toEqual(fakeInitializer); diff --git a/src/core/public/plugins/plugin_loader.ts b/src/core/public/plugins/plugin_loader.ts index 63aba0dde2af8..bf7711055e97b 100644 --- a/src/core/public/plugins/plugin_loader.ts +++ b/src/core/public/plugins/plugin_loader.ts @@ -32,7 +32,7 @@ export type UnknownPluginInitializer = PluginInitializer new Promise>( (resolve, reject) => { - const script = document.createElement('script'); const coreWindow = (window as unknown) as CoreWindow; + const exportId = `plugin/${pluginName}`; + + const readPluginExport = () => { + const PluginExport: any = coreWindow.__kbnBundles__[exportId]; + if (typeof PluginExport?.plugin !== 'function') { + reject( + new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`) + ); + } else { + resolve( + PluginExport.plugin as PluginInitializer + ); + } + }; + if (coreWindow.__kbnBundles__[exportId]) { + readPluginExport(); + return; + } + + const script = document.createElement('script'); // Assumes that all plugin bundles get put into the bundles/plugins subdirectory const bundlePath = addBasePath(`/bundles/plugin/${pluginName}/${pluginName}.plugin.js`); script.setAttribute('src', bundlePath); @@ -89,15 +108,7 @@ export const loadPluginBundle: LoadPluginBundle = < // Wire up resolve and reject script.onload = () => { cleanupTag(); - - const initializer = coreWindow.__kbnBundles__[`plugin/${pluginName}`]; - if (!initializer || typeof initializer !== 'function') { - reject( - new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`) - ); - } else { - resolve(initializer as PluginInitializer); - } + readPluginExport(); }; script.onerror = () => { diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts index 023e6ebb7125c..badea68eec19f 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts @@ -23,7 +23,7 @@ import { createInputControlVisController } from './vis_controller'; import { getControlsTab } from './components/editor/controls_tab'; import { OptionsTab } from './components/editor/options_tab'; import { InputControlVisDependencies } from './plugin'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common'; +import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; export function createInputControlVisTypeDefinition(deps: InputControlVisDependencies) { const InputControlVisController = createInputControlVisController(deps); diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index e6421142f6666..98679a8f24d16 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -17,6 +17,8 @@ * under the License. */ import { DiscoverServices } from './build_services'; +import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; +import { search } from '../../../../../plugins/data/public'; let angularModule: any = null; let services: DiscoverServices | null = null; @@ -50,8 +52,6 @@ export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ setTrackedUrl: (url: string) => void; }>('urlTracker'); -import { search } from '../../../../../plugins/data/public'; -import { createGetterSetter } from '../../../../../plugins/kibana_utils/common'; export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; export { unhashUrl, diff --git a/src/legacy/core_plugins/vis_type_metric/public/services.ts b/src/legacy/core_plugins/vis_type_metric/public/services.ts index 5af11bc7f0b03..b303ccd5aeed2 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/services.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../plugins/kibana_utils/common'; +import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; export const [getFormatService, setFormatService] = createGetterSetter< diff --git a/src/legacy/core_plugins/vis_type_table/public/services.ts b/src/legacy/core_plugins/vis_type_table/public/services.ts index 08efed733cafe..b4b491ac7a555 100644 --- a/src/legacy/core_plugins/vis_type_table/public/services.ts +++ b/src/legacy/core_plugins/vis_type_table/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../plugins/kibana_utils/common'; +import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; export const [getFormatService, setFormatService] = createGetterSetter< diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts index fef46282eb8dd..272bed3e91a08 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../plugins/kibana_utils/common'; +import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; export const [getFormatService, setFormatService] = createGetterSetter< diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts index 30c62d778933b..1db35c406eb13 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts @@ -25,7 +25,7 @@ import { metricsRequestHandler } from './request_handler'; import { EditorController } from './editor_controller'; // @ts-ignore import { PANEL_TYPES } from '../../../../plugins/vis_type_timeseries/common/panel_types'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common'; +import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; export const metricsVisDefinition = { name: 'metrics', diff --git a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts b/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts index 64a9aaaf3b7a6..b2f3e5b2241e6 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../../plugins/kibana_utils/common'; +import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; import { IUiSettingsClient, NotificationsStart, SavedObjectsStart } from 'kibana/public'; import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts index 5d9ed5c8df91c..f56d7682efc6f 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common'; +import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; import { createVegaRequestHandler } from './vega_request_handler'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_vislib/public/services.ts b/src/legacy/core_plugins/vis_type_vislib/public/services.ts index da50e227d84d2..0d6b1b5e8de58 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/services.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../plugins/kibana_utils/common'; +import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; export const [getDataActions, setDataActions] = createGetterSetter< diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index ad4aa97d8ea7a..1093153edbbf7 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -30,33 +30,27 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { function loadStyleSheet(url, cb) { var dom = document.createElement('link'); + dom.rel = 'stylesheet'; + dom.type = 'text/css'; + dom.href = url; dom.addEventListener('error', failure); - dom.setAttribute('rel', 'stylesheet'); - dom.setAttribute('type', 'text/css'); - dom.setAttribute('href', url); dom.addEventListener('load', cb); document.head.appendChild(dom); } function loadScript(url, cb) { var dom = document.createElement('script'); - dom.setAttribute('async', ''); + {{!-- NOTE: async = false is used to trigger async-download/ordered-execution as outlined here: https://www.html5rocks.com/en/tutorials/speed/script-loading/ --}} + dom.async = false; + dom.src = url; dom.addEventListener('error', failure); - dom.setAttribute('src', url); dom.addEventListener('load', cb); document.head.appendChild(dom); } - function load(urlSet, cb) { - if (urlSet.deps) { - load({ urls: urlSet.deps }, function () { - load({ urls: urlSet.urls }, cb); - }); - return; - } - - var pending = urlSet.urls.length; - urlSet.urls.forEach(function (url) { + function load(urls, cb) { + var pending = urls.length; + urls.forEach(function (url) { var innerCb = function () { pending = pending - 1; if (pending === 0 && typeof cb === 'function') { @@ -74,36 +68,27 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { }); } - load({ - deps: [ + load([ {{#each sharedJsDepFilenames}} '{{../regularBundlePath}}/kbn-ui-shared-deps/{{this}}', {{/each}} - ], - urls: [ - { - deps: [ - '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedJsFilename}}', - { - deps: [ - '{{dllBundlePath}}/vendors_runtime.bundle.dll.js' - ], - urls: [ - {{#each dllJsChunks}} - '{{this}}', - {{/each}} - ] - }, - '{{regularBundlePath}}/commons.bundle.js', - ], - urls: [ - '{{regularBundlePath}}/{{appId}}.bundle.js', - {{#each styleSheetPaths}} - '{{this}}', - {{/each}} - ] - } - ] + '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedJsFilename}}', + '{{dllBundlePath}}/vendors_runtime.bundle.dll.js', + {{#each dllJsChunks}} + '{{this}}', + {{/each}} + '{{regularBundlePath}}/commons.bundle.js', + {{!-- '{{regularBundlePath}}/plugin/data/data.plugin.js', --}} + '{{regularBundlePath}}/plugin/kibanaUtils/kibanaUtils.plugin.js', + '{{regularBundlePath}}/plugin/esUiShared/esUiShared.plugin.js', + '{{regularBundlePath}}/plugin/kibanaReact/kibanaReact.plugin.js' + ], function () { + load([ + '{{regularBundlePath}}/{{appId}}.bundle.js', + {{#each styleSheetPaths}} + '{{this}}', + {{/each}} + ]) }); }; } diff --git a/src/plugins/discover/public/services.ts b/src/plugins/discover/public/services.ts index 3a28759d82b71..37e2144800ea1 100644 --- a/src/plugins/discover/public/services.ts +++ b/src/plugins/discover/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../kibana_utils/common'; +import { createGetterSetter } from '../../kibana_utils/public'; import { DocViewsRegistry } from './doc_views/doc_views_registry'; export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter( diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts index 56facc37fc666..ddd84b0544345 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts @@ -21,7 +21,7 @@ import { Embeddable } from './embeddable'; import { EmbeddableInput } from './i_embeddable'; import { ViewMode } from '../types'; import { EmbeddableActionStorage, SerializedEvent } from './embeddable_action_storage'; -import { of } from '../../../../kibana_utils/common'; +import { of } from '../../../../kibana_utils/public'; class TestEmbeddable extends Embeddable { public readonly type = 'test'; diff --git a/src/plugins/es_ui_shared/kibana.json b/src/plugins/es_ui_shared/kibana.json new file mode 100644 index 0000000000000..5a3db3b344090 --- /dev/null +++ b/src/plugins/es_ui_shared/kibana.json @@ -0,0 +1,5 @@ +{ + "id": "esUiShared", + "version": "kibana", + "ui": true +} diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 944b800c66a28..6db6248f4c68f 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -35,3 +35,11 @@ export { export { indices } from './indices'; export { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode'; + +/** dummy plugin, we just want esUiShared to have its own bundle */ +export function plugin() { + return new (class EsUiSharedPlugin { + setup() {} + start() {} + })(); +} diff --git a/src/plugins/kibana_react/kibana.json b/src/plugins/kibana_react/kibana.json new file mode 100644 index 0000000000000..0add1bee84ae0 --- /dev/null +++ b/src/plugins/kibana_react/kibana.json @@ -0,0 +1,5 @@ +{ + "id": "kibanaReact", + "version": "kibana", + "ui": true +} diff --git a/src/plugins/kibana_react/public/adapters/react_to_ui_component.ts b/src/plugins/kibana_react/public/adapters/react_to_ui_component.ts index b4007b30cf8ca..21bba92ada4c1 100644 --- a/src/plugins/kibana_react/public/adapters/react_to_ui_component.ts +++ b/src/plugins/kibana_react/public/adapters/react_to_ui_component.ts @@ -19,7 +19,7 @@ import { ComponentType, createElement as h } from 'react'; import { render as renderReact, unmountComponentAtNode } from 'react-dom'; -import { UiComponent, UiComponentInstance } from '../../../kibana_utils/common'; +import { UiComponent, UiComponentInstance } from '../../../kibana_utils/public'; /** * Transform a React component into a `UiComponent`. diff --git a/src/plugins/kibana_react/public/adapters/ui_to_react_component.test.tsx b/src/plugins/kibana_react/public/adapters/ui_to_react_component.test.tsx index 939d372b9997f..aefbd66e50fcf 100644 --- a/src/plugins/kibana_react/public/adapters/ui_to_react_component.test.tsx +++ b/src/plugins/kibana_react/public/adapters/ui_to_react_component.test.tsx @@ -19,7 +19,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { UiComponent } from '../../../kibana_utils/common'; +import { UiComponent } from '../../../kibana_utils/public'; import { uiToReactComponent } from './ui_to_react_component'; import { reactToUiComponent } from './react_to_ui_component'; diff --git a/src/plugins/kibana_react/public/adapters/ui_to_react_component.ts b/src/plugins/kibana_react/public/adapters/ui_to_react_component.ts index 9b34880cf4fe3..ee99ea3672672 100644 --- a/src/plugins/kibana_react/public/adapters/ui_to_react_component.ts +++ b/src/plugins/kibana_react/public/adapters/ui_to_react_component.ts @@ -18,7 +18,7 @@ */ import { FC, createElement as h, useRef, useLayoutEffect, useMemo } from 'react'; -import { UiComponent, UiComponentInstance } from '../../../kibana_utils/common'; +import { UiComponent, UiComponentInstance } from '../../../kibana_utils/public'; /** * Transforms `UiComponent` into a React component. diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index e1689e38dbfe0..9ad9f14ac5659 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -31,3 +31,11 @@ export { Markdown, MarkdownSimple } from './markdown'; export { reactToUiComponent, uiToReactComponent } from './adapters'; export { useUrlTracker } from './use_url_tracker'; export { toMountPoint } from './util'; + +/** dummy plugin, we just want kibanaReact to have its own bundle */ +export function plugin() { + return new (class KibanaReactPlugin { + setup() {} + start() {} + })(); +} diff --git a/src/plugins/kibana_utils/kibana.json b/src/plugins/kibana_utils/kibana.json new file mode 100644 index 0000000000000..6fa39d82d1021 --- /dev/null +++ b/src/plugins/kibana_utils/kibana.json @@ -0,0 +1,5 @@ +{ + "id": "kibanaUtils", + "version": "kibana", + "ui": true +} diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 1876e688c989a..2f139050e994a 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -19,7 +19,6 @@ export { calculateObjectHash, - createGetterSetter, defer, Defer, Get, @@ -31,6 +30,8 @@ export { UiComponent, UiComponentInstance, url, + createGetterSetter, + defaultFeedbackMessage, } from '../common'; export * from './core'; export * from './errors'; @@ -75,3 +76,11 @@ export { } from './state_sync'; export { removeQueryParam, redirectWhenMissing, ensureDefaultIndexPattern } from './history'; export { applyDiff } from './state_management/utils/diff_object'; + +/** dummy plugin, we just want kibanaUtils to have its own bundle */ +export function plugin() { + return new (class KibanaUtilsPlugin { + setup() {} + start() {} + })(); +} diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index 2b2fc004a84c6..f532c2c8aa219 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -17,7 +17,7 @@ * under the License. */ -import { UiComponent } from 'src/plugins/kibana_utils/common'; +import { UiComponent } from 'src/plugins/kibana_utils/public'; import { ActionType, ActionContextMapping } from '../types'; export type ActionByType = Action; diff --git a/src/plugins/ui_actions/public/actions/action_definition.ts b/src/plugins/ui_actions/public/actions/action_definition.ts index c590cf8f34ee0..3eaa13572a826 100644 --- a/src/plugins/ui_actions/public/actions/action_definition.ts +++ b/src/plugins/ui_actions/public/actions/action_definition.ts @@ -17,7 +17,7 @@ * under the License. */ -import { UiComponent } from 'src/plugins/kibana_utils/common'; +import { UiComponent } from 'src/plugins/kibana_utils/public'; import { ActionType, ActionContextMapping } from '../types'; export interface ActionDefinition { From f84773faed53cf0c3683406d5eaccb5f51975cda Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 8 Apr 2020 13:16:32 -0600 Subject: [PATCH 34/81] Add basic StatusService (#60335) --- .../kibana-plugin-core-server.coresetup.md | 1 + ...ana-plugin-core-server.coresetup.status.md | 13 + ...in-core-server.corestatus.elasticsearch.md | 11 + .../kibana-plugin-core-server.corestatus.md | 21 ++ ...gin-core-server.corestatus.savedobjects.md | 11 + ...asticsearchstatusmeta.incompatiblenodes.md | 11 + ...gin-core-server.elasticsearchstatusmeta.md | 20 ++ ...er.elasticsearchstatusmeta.warningnodes.md | 11 + .../core/server/kibana-plugin-core-server.md | 8 + ...sversioncompatibility.incompatiblenodes.md | 11 + ....nodesversioncompatibility.iscompatible.md | 11 + ...nodesversioncompatibility.kibanaversion.md | 11 + ...n-core-server.nodesversioncompatibility.md | 22 ++ ...erver.nodesversioncompatibility.message.md | 11 + ....nodesversioncompatibility.warningnodes.md | 11 + ...lugin-core-server.savedobjectstatusmeta.md | 20 ++ ...r.savedobjectstatusmeta.migratedindices.md | 15 ++ ...plugin-core-server.servicestatus.detail.md | 13 + ...e-server.servicestatus.documentationurl.md | 13 + ...-plugin-core-server.servicestatus.level.md | 13 + ...kibana-plugin-core-server.servicestatus.md | 24 ++ ...a-plugin-core-server.servicestatus.meta.md | 13 + ...lugin-core-server.servicestatus.summary.md | 13 + ...a-plugin-core-server.servicestatuslevel.md | 13 + ...-plugin-core-server.servicestatuslevels.md | 37 +++ ...in-core-server.statusservicesetup.core_.md | 13 + ...a-plugin-core-server.statusservicesetup.md | 20 ++ .../kibana-plugin-plugins-data-server.md | 1 - .../elasticsearch_service.mock.ts | 6 + .../elasticsearch/elasticsearch_service.ts | 2 + src/core/server/elasticsearch/index.ts | 1 + src/core/server/elasticsearch/status.test.ts | 222 +++++++++++++++++ src/core/server/elasticsearch/status.ts | 78 ++++++ src/core/server/elasticsearch/types.ts | 8 + .../version_check/ensure_es_version.ts | 2 +- src/core/server/index.ts | 18 +- src/core/server/internal_types.ts | 6 +- src/core/server/legacy/legacy_service.test.ts | 2 + src/core/server/legacy/legacy_service.ts | 3 + src/core/server/mocks.ts | 9 +- src/core/server/plugins/plugin_context.ts | 3 + src/core/server/saved_objects/index.ts | 6 +- .../saved_objects/migrations/core/index.ts | 2 +- .../migrations/core/migration_coordinator.ts | 2 + .../server/saved_objects/migrations/index.ts | 1 + .../saved_objects/migrations/kibana/index.ts | 2 +- .../migrations/kibana/kibana_migrator.mock.ts | 17 +- .../migrations/kibana/kibana_migrator.test.ts | 28 +++ .../migrations/kibana/kibana_migrator.ts | 29 ++- .../saved_objects_service.mock.ts | 7 + .../saved_objects/saved_objects_service.ts | 16 +- src/core/server/saved_objects/status.test.ts | 134 ++++++++++ src/core/server/saved_objects/status.ts | 84 +++++++ src/core/server/saved_objects/types.ts | 13 + src/core/server/server.api.md | 83 +++++++ src/core/server/server.test.mocks.ts | 6 + src/core/server/server.test.ts | 7 + src/core/server/server.ts | 18 +- .../server/status/get_summary_status.test.ts | 180 ++++++++++++++ src/core/server/status/get_summary_status.ts | 84 +++++++ src/core/server/status/index.ts | 21 ++ src/core/server/status/status_service.mock.ts | 71 ++++++ src/core/server/status/status_service.test.ts | 229 ++++++++++++++++++ src/core/server/status/status_service.ts | 78 ++++++ src/core/server/status/test_utils.ts | 25 ++ src/core/server/status/types.ts | 134 ++++++++++ src/core/server/test_utils.ts | 1 + 67 files changed, 2006 insertions(+), 24 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.coresetup.status.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.corestatus.elasticsearch.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.corestatus.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.corestatus.savedobjects.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.message.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.servicestatus.detail.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.servicestatus.documentationurl.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.servicestatus.level.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.servicestatus.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.servicestatus.meta.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.servicestatus.summary.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.servicestatuslevel.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.servicestatuslevels.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.statusservicesetup.core_.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md create mode 100644 src/core/server/elasticsearch/status.test.ts create mode 100644 src/core/server/elasticsearch/status.ts create mode 100644 src/core/server/saved_objects/status.test.ts create mode 100644 src/core/server/saved_objects/status.ts create mode 100644 src/core/server/status/get_summary_status.test.ts create mode 100644 src/core/server/status/get_summary_status.ts create mode 100644 src/core/server/status/index.ts create mode 100644 src/core/server/status/status_service.mock.ts create mode 100644 src/core/server/status/status_service.test.ts create mode 100644 src/core/server/status/status_service.ts create mode 100644 src/core/server/status/test_utils.ts create mode 100644 src/core/server/status/types.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index 29fdc37a81176..c10b460da8b4f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -23,6 +23,7 @@ export interface CoreSetupHttpServiceSetup | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | | [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | +| [status](./kibana-plugin-core-server.coresetup.status.md) | StatusServiceSetup | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | | [uiSettings](./kibana-plugin-core-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | | [uuid](./kibana-plugin-core-server.coresetup.uuid.md) | UuidServiceSetup | [UuidServiceSetup](./kibana-plugin-core-server.uuidservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.status.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.status.md new file mode 100644 index 0000000000000..f5ea627a9f008 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.status.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [status](./kibana-plugin-core-server.coresetup.status.md) + +## CoreSetup.status property + +[StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) + +Signature: + +```typescript +status: StatusServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestatus.elasticsearch.md b/docs/development/core/server/kibana-plugin-core-server.corestatus.elasticsearch.md new file mode 100644 index 0000000000000..b41e7020c38e9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corestatus.elasticsearch.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStatus](./kibana-plugin-core-server.corestatus.md) > [elasticsearch](./kibana-plugin-core-server.corestatus.elasticsearch.md) + +## CoreStatus.elasticsearch property + +Signature: + +```typescript +elasticsearch: ServiceStatus; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestatus.md b/docs/development/core/server/kibana-plugin-core-server.corestatus.md new file mode 100644 index 0000000000000..3fde86a18c58b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corestatus.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStatus](./kibana-plugin-core-server.corestatus.md) + +## CoreStatus interface + +Status of core services. + +Signature: + +```typescript +export interface CoreStatus +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [elasticsearch](./kibana-plugin-core-server.corestatus.elasticsearch.md) | ServiceStatus | | +| [savedObjects](./kibana-plugin-core-server.corestatus.savedobjects.md) | ServiceStatus | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.corestatus.savedobjects.md b/docs/development/core/server/kibana-plugin-core-server.corestatus.savedobjects.md new file mode 100644 index 0000000000000..d554c6f70d720 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corestatus.savedobjects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStatus](./kibana-plugin-core-server.corestatus.md) > [savedObjects](./kibana-plugin-core-server.corestatus.savedobjects.md) + +## CoreStatus.savedObjects property + +Signature: + +```typescript +savedObjects: ServiceStatus; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md new file mode 100644 index 0000000000000..f8a45fe9a5a9c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) > [incompatibleNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md) + +## ElasticsearchStatusMeta.incompatibleNodes property + +Signature: + +```typescript +incompatibleNodes: NodesVersionCompatibility['incompatibleNodes']; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md new file mode 100644 index 0000000000000..2398410fa4b84 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) + +## ElasticsearchStatusMeta interface + + +Signature: + +```typescript +export interface ElasticsearchStatusMeta +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [incompatibleNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md) | NodesVersionCompatibility['incompatibleNodes'] | | +| [warningNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md) | NodesVersionCompatibility['warningNodes'] | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md new file mode 100644 index 0000000000000..7374ccd9e7fa8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) > [warningNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md) + +## ElasticsearchStatusMeta.warningNodes property + +Signature: + +```typescript +warningNodes: NodesVersionCompatibility['warningNodes']; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 793684c1b3796..accab9bf0cb36 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -66,6 +66,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. | | [CoreSetup](./kibana-plugin-core-server.coresetup.md) | Context passed to the plugins setup method. | | [CoreStart](./kibana-plugin-core-server.corestart.md) | Context passed to the plugins start method. | +| [CoreStatus](./kibana-plugin-core-server.corestatus.md) | Status of core services. | | [CustomHttpResponseOptions](./kibana-plugin-core-server.customhttpresponseoptions.md) | HTTP response parameters for a response with adjustable status code. | | [DeprecationAPIClientParams](./kibana-plugin-core-server.deprecationapiclientparams.md) | | | [DeprecationAPIResponse](./kibana-plugin-core-server.deprecationapiresponse.md) | | @@ -75,6 +76,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ElasticsearchError](./kibana-plugin-core-server.elasticsearcherror.md) | | | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | | | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | +| [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) | | | [EnvironmentMode](./kibana-plugin-core-server.environmentmode.md) | | | [ErrorHttpResponseOptions](./kibana-plugin-core-server.errorhttpresponseoptions.md) | HTTP response parameters | | [FakeRequest](./kibana-plugin-core-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | @@ -101,6 +103,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LoggerFactory](./kibana-plugin-core-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | | [LogMeta](./kibana-plugin-core-server.logmeta.md) | Contextual metadata | | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. | +| [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) | | | [OnPostAuthToolkit](./kibana-plugin-core-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | | [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [OnPreResponseExtensions](./kibana-plugin-core-server.onpreresponseextensions.md) | Additional data to extend a response. | @@ -162,15 +165,18 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. | | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | Saved Objects is Kibana's data persistence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods for registering Saved Object types, creating and registering Saved Object client wrappers and factories. | | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceStart API provides a scoped Saved Objects client for interacting with Saved Objects. | +| [SavedObjectStatusMeta](./kibana-plugin-core-server.savedobjectstatusmeta.md) | Meta information about the SavedObjectService's status. Available to plugins via [CoreSetup.status](./kibana-plugin-core-server.coresetup.status.md). | | [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) | | | [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) | Configuration options for the [type](./kibana-plugin-core-server.savedobjectstype.md)'s management section. | | [SavedObjectsTypeMappingDefinition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) | Describe a saved object type mapping. | | [SavedObjectsUpdateOptions](./kibana-plugin-core-server.savedobjectsupdateoptions.md) | | | [SavedObjectsUpdateResponse](./kibana-plugin-core-server.savedobjectsupdateresponse.md) | | +| [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) | The current status of a service at a point in time. | | [SessionCookieValidationResult](./kibana-plugin-core-server.sessioncookievalidationresult.md) | Return type from a function to validate cookie contents. | | [SessionStorage](./kibana-plugin-core-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. | | [SessionStorageCookieOptions](./kibana-plugin-core-server.sessionstoragecookieoptions.md) | Configuration used to create HTTP session storage based on top of cookie mechanism. | | [SessionStorageFactory](./kibana-plugin-core-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | +| [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status. | | [StringValidationRegex](./kibana-plugin-core-server.stringvalidationregex.md) | StringValidation with regex object | | [StringValidationRegexString](./kibana-plugin-core-server.stringvalidationregexstring.md) | StringValidation as regex string | | [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) | UiSettings parameters defined by the plugins. | @@ -184,6 +190,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | Variable | Description | | --- | --- | | [kibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) | Set of helpers used to create KibanaResponse to form HTTP response on an incoming request. Should be returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution. | +| [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md) | The current "level" of availability of a service. | | [validBodyOutput](./kibana-plugin-core-server.validbodyoutput.md) | The set of valid body.output | ## Type Aliases @@ -256,6 +263,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | | [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation | | [ScopeableRequest](./kibana-plugin-core-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.See [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md). | +| [ServiceStatusLevel](./kibana-plugin-core-server.servicestatuslevel.md) | A convenience type that represents the union of each value in [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md). | | [SharedGlobalConfig](./kibana-plugin-core-server.sharedglobalconfig.md) | | | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed start. This should only be used inside handlers registered during setup that will only be executed after start lifecycle. | | [StringValidation](./kibana-plugin-core-server.stringvalidation.md) | Allows regex objects or a regex string | diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md new file mode 100644 index 0000000000000..8e7298d28801c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [incompatibleNodes](./kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md) + +## NodesVersionCompatibility.incompatibleNodes property + +Signature: + +```typescript +incompatibleNodes: NodeInfo[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md new file mode 100644 index 0000000000000..82a4800a3b4b6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [isCompatible](./kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md) + +## NodesVersionCompatibility.isCompatible property + +Signature: + +```typescript +isCompatible: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md new file mode 100644 index 0000000000000..347f2d3474b11 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [kibanaVersion](./kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md) + +## NodesVersionCompatibility.kibanaVersion property + +Signature: + +```typescript +kibanaVersion: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md new file mode 100644 index 0000000000000..6fcfacc3bc908 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) + +## NodesVersionCompatibility interface + +Signature: + +```typescript +export interface NodesVersionCompatibility +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [incompatibleNodes](./kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md) | NodeInfo[] | | +| [isCompatible](./kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md) | boolean | | +| [kibanaVersion](./kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md) | string | | +| [message](./kibana-plugin-core-server.nodesversioncompatibility.message.md) | string | | +| [warningNodes](./kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md) | NodeInfo[] | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.message.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.message.md new file mode 100644 index 0000000000000..415a7825ee2bf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.message.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [message](./kibana-plugin-core-server.nodesversioncompatibility.message.md) + +## NodesVersionCompatibility.message property + +Signature: + +```typescript +message?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md new file mode 100644 index 0000000000000..6c017e9fc800c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [warningNodes](./kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md) + +## NodesVersionCompatibility.warningNodes property + +Signature: + +```typescript +warningNodes: NodeInfo[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.md new file mode 100644 index 0000000000000..3a0b23d18632f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectStatusMeta](./kibana-plugin-core-server.savedobjectstatusmeta.md) + +## SavedObjectStatusMeta interface + +Meta information about the SavedObjectService's status. Available to plugins via [CoreSetup.status](./kibana-plugin-core-server.coresetup.status.md). + +Signature: + +```typescript +export interface SavedObjectStatusMeta +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [migratedIndices](./kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md) | {
[status: string]: number;
skipped: number;
migrated: number;
} | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md new file mode 100644 index 0000000000000..6a29623b2f122 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectStatusMeta](./kibana-plugin-core-server.savedobjectstatusmeta.md) > [migratedIndices](./kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md) + +## SavedObjectStatusMeta.migratedIndices property + +Signature: + +```typescript +migratedIndices: { + [status: string]: number; + skipped: number; + migrated: number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.detail.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.detail.md new file mode 100644 index 0000000000000..fa369aa0bdfbb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.detail.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) > [detail](./kibana-plugin-core-server.servicestatus.detail.md) + +## ServiceStatus.detail property + +A more detailed description of the service status. + +Signature: + +```typescript +detail?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.documentationurl.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.documentationurl.md new file mode 100644 index 0000000000000..5ef8c1251a602 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.documentationurl.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) > [documentationUrl](./kibana-plugin-core-server.servicestatus.documentationurl.md) + +## ServiceStatus.documentationUrl property + +A URL to open in a new tab about how to resolve or troubleshoot the problem. + +Signature: + +```typescript +documentationUrl?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.level.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.level.md new file mode 100644 index 0000000000000..551c10c9bff82 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.level.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) > [level](./kibana-plugin-core-server.servicestatus.level.md) + +## ServiceStatus.level property + +The current availability level of the service. + +Signature: + +```typescript +level: ServiceStatusLevel; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.md new file mode 100644 index 0000000000000..d35fc951c57ff --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) + +## ServiceStatus interface + +The current status of a service at a point in time. + +Signature: + +```typescript +export interface ServiceStatus | unknown = unknown> +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [detail](./kibana-plugin-core-server.servicestatus.detail.md) | string | A more detailed description of the service status. | +| [documentationUrl](./kibana-plugin-core-server.servicestatus.documentationurl.md) | string | A URL to open in a new tab about how to resolve or troubleshoot the problem. | +| [level](./kibana-plugin-core-server.servicestatus.level.md) | ServiceStatusLevel | The current availability level of the service. | +| [meta](./kibana-plugin-core-server.servicestatus.meta.md) | Meta | Any JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained, machine-readable information about the service status. May include status information for underlying features. | +| [summary](./kibana-plugin-core-server.servicestatus.summary.md) | string | A high-level summary of the service status. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.meta.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.meta.md new file mode 100644 index 0000000000000..a48994daa5a4e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.meta.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) > [meta](./kibana-plugin-core-server.servicestatus.meta.md) + +## ServiceStatus.meta property + +Any JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained, machine-readable information about the service status. May include status information for underlying features. + +Signature: + +```typescript +meta?: Meta; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.summary.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.summary.md new file mode 100644 index 0000000000000..db90afd6f74a6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.summary.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) > [summary](./kibana-plugin-core-server.servicestatus.summary.md) + +## ServiceStatus.summary property + +A high-level summary of the service status. + +Signature: + +```typescript +summary: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatuslevel.md b/docs/development/core/server/kibana-plugin-core-server.servicestatuslevel.md new file mode 100644 index 0000000000000..5f995ff5e13e3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatuslevel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatusLevel](./kibana-plugin-core-server.servicestatuslevel.md) + +## ServiceStatusLevel type + +A convenience type that represents the union of each value in [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md). + +Signature: + +```typescript +export declare type ServiceStatusLevel = typeof ServiceStatusLevels[keyof typeof ServiceStatusLevels]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatuslevels.md b/docs/development/core/server/kibana-plugin-core-server.servicestatuslevels.md new file mode 100644 index 0000000000000..a66cec78c736b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatuslevels.md @@ -0,0 +1,37 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md) + +## ServiceStatusLevels variable + +The current "level" of availability of a service. + +Signature: + +```typescript +ServiceStatusLevels: Readonly<{ + available: Readonly<{ + toString: () => "available"; + valueOf: () => 0; + }>; + degraded: Readonly<{ + toString: () => "degraded"; + valueOf: () => 1; + }>; + unavailable: Readonly<{ + toString: () => "unavailable"; + valueOf: () => 2; + }>; + critical: Readonly<{ + toString: () => "critical"; + valueOf: () => 3; + }>; +}> +``` + +## Remarks + +The values implement `valueOf` to allow for easy comparisons between status levels with <, >, etc. Higher values represent higher severities. Note that the default `Array.prototype.sort` implementation does not correctly sort these values. + +A snapshot serializer is available in `src/core/server/test_utils` to ease testing of these values with Jest. + diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.core_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.core_.md new file mode 100644 index 0000000000000..6662e68b44d36 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.core_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) + +## StatusServiceSetup.core$ property + +Current status for all Core services. + +Signature: + +```typescript +core$: Observable; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md new file mode 100644 index 0000000000000..0551a217520ad --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) + +## StatusServiceSetup interface + +API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status. + +Signature: + +```typescript +export interface StatusServiceSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | Observable<CoreStatus> | Current status for all Core services. | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 259d725b3bf0d..e756eb9b72905 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -23,7 +23,6 @@ | Function | Description | | --- | --- | | [getDefaultSearchParams(config)](./kibana-plugin-plugins-data-server.getdefaultsearchparams.md) | | -| [getTotalLoaded({ total, failed, successful })](./kibana-plugin-plugins-data-server.gettotalloaded.md) | | | [parseInterval(interval)](./kibana-plugin-plugins-data-server.parseinterval.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-server.plugin.md) | Static code to be shared externally | | [shouldReadFieldFromDocValues(aggregatable, esType)](./kibana-plugin-plugins-data-server.shouldreadfieldfromdocvalues.md) | | diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index 389d98a0818c8..da8846f6dddbb 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -26,8 +26,10 @@ import { InternalElasticsearchServiceSetup, ElasticsearchServiceSetup, ElasticsearchServiceStart, + ElasticsearchStatusMeta, } from './types'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; +import { ServiceStatus, ServiceStatusLevels } from '../status'; const createScopedClusterClientMock = (): jest.Mocked => ({ callAsInternalUser: jest.fn(), @@ -102,6 +104,10 @@ const createInternalSetupContractMock = () => { warningNodes: [], kibanaVersion: '8.0.0', }), + status$: new BehaviorSubject>({ + level: ServiceStatusLevels.available, + summary: 'Elasticsearch is available', + }), legacy: { config$: new BehaviorSubject({} as ElasticsearchConfig), }, diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index b92a6edf778ed..684f6e15caff9 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -40,6 +40,7 @@ import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/'; import { InternalElasticsearchServiceSetup, ElasticsearchServiceStart } from './types'; import { CallAPIOptions } from './api_types'; import { pollEsNodesVersion } from './version_check/ensure_es_version'; +import { calculateStatus$ } from './status'; /** @internal */ interface CoreClusterClients { @@ -186,6 +187,7 @@ export class ElasticsearchService adminClient: this.adminClient, dataClient, createClient: this.createClient, + status$: calculateStatus$(esNodesCompatibility$), }; } diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index cfd72a6fd5e47..2e45f710c4dcf 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -31,3 +31,4 @@ export { config, configSchema, ElasticsearchConfig } from './elasticsearch_confi export { ElasticsearchError, ElasticsearchErrorHelpers } from './errors'; export * from './api_types'; export * from './types'; +export { NodesVersionCompatibility } from './version_check/ensure_es_version'; diff --git a/src/core/server/elasticsearch/status.test.ts b/src/core/server/elasticsearch/status.test.ts new file mode 100644 index 0000000000000..dd5fb04bfd1c6 --- /dev/null +++ b/src/core/server/elasticsearch/status.test.ts @@ -0,0 +1,222 @@ +/* + * 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 { take } from 'rxjs/operators'; +import { Subject, of } from 'rxjs'; + +import { calculateStatus$ } from './status'; +import { ServiceStatusLevels, ServiceStatus } from '../status'; +import { ServiceStatusLevelSnapshotSerializer } from '../status/test_utils'; +import { NodesVersionCompatibility } from './version_check/ensure_es_version'; + +expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); + +const nodeInfo = { + version: '1.1.1', + ip: '1.1.1.1', + http: { + publish_address: 'https://1.1.1.1:9200', + }, + name: 'node1', +}; + +describe('calculateStatus', () => { + it('starts in unavailable', async () => { + expect( + await calculateStatus$(new Subject()) + .pipe(take(1)) + .toPromise() + ).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: 'Waiting for Elasticsearch', + meta: { + warningNodes: [], + incompatibleNodes: [], + }, + }); + }); + + it('changes to available when isCompatible and no warningNodes', async () => { + expect( + await calculateStatus$( + of({ isCompatible: true, kibanaVersion: '1.1.1', warningNodes: [], incompatibleNodes: [] }) + ) + .pipe(take(2)) + .toPromise() + ).toEqual({ + level: ServiceStatusLevels.available, + summary: 'Elasticsearch is available', + meta: { + warningNodes: [], + incompatibleNodes: [], + }, + }); + }); + + it('changes to degraded when isCompatible and warningNodes present', async () => { + expect( + await calculateStatus$( + of({ + isCompatible: true, + kibanaVersion: '1.1.1', + warningNodes: [nodeInfo], + incompatibleNodes: [], + // this isn't the real message, just used to test that the message + // is forwarded to the status + message: 'Some nodes are a different version', + }) + ) + .pipe(take(2)) + .toPromise() + ).toEqual({ + level: ServiceStatusLevels.degraded, + summary: 'Some nodes are a different version', + meta: { + incompatibleNodes: [], + warningNodes: [nodeInfo], + }, + }); + }); + + it('changes to critical when isCompatible is false', async () => { + expect( + await calculateStatus$( + of({ + isCompatible: false, + kibanaVersion: '2.1.1', + warningNodes: [nodeInfo], + incompatibleNodes: [nodeInfo], + // this isn't the real message, just used to test that the message + // is forwarded to the status + message: 'Incompatible with Elasticsearch', + }) + ) + .pipe(take(2)) + .toPromise() + ).toEqual({ + level: ServiceStatusLevels.critical, + summary: 'Incompatible with Elasticsearch', + meta: { + incompatibleNodes: [nodeInfo], + warningNodes: [nodeInfo], + }, + }); + }); + + it('emits status updates when node compatibility changes', () => { + const nodeCompat$ = new Subject(); + + const statusUpdates: ServiceStatus[] = []; + const subscription = calculateStatus$(nodeCompat$).subscribe(status => + statusUpdates.push(status) + ); + + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '2.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info', + }); + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '2.1.1', + incompatibleNodes: [nodeInfo], + warningNodes: [], + message: 'Incompatible with Elasticsearch', + }); + nodeCompat$.next({ + isCompatible: true, + kibanaVersion: '1.1.1', + warningNodes: [nodeInfo], + incompatibleNodes: [], + message: 'Some nodes are incompatible', + }); + nodeCompat$.next({ + isCompatible: true, + kibanaVersion: '1.1.1', + warningNodes: [], + incompatibleNodes: [], + }); + + subscription.unsubscribe(); + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "level": unavailable, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Waiting for Elasticsearch", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [ + Object { + "http": Object { + "publish_address": "https://1.1.1.1:9200", + }, + "ip": "1.1.1.1", + "name": "node1", + "version": "1.1.1", + }, + ], + "warningNodes": Array [], + }, + "summary": "Incompatible with Elasticsearch", + }, + Object { + "level": degraded, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [ + Object { + "http": Object { + "publish_address": "https://1.1.1.1:9200", + }, + "ip": "1.1.1.1", + "name": "node1", + "version": "1.1.1", + }, + ], + }, + "summary": "Some nodes are incompatible", + }, + Object { + "level": available, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Elasticsearch is available", + }, + ] + `); + }); +}); diff --git a/src/core/server/elasticsearch/status.ts b/src/core/server/elasticsearch/status.ts new file mode 100644 index 0000000000000..1eaa338af1239 --- /dev/null +++ b/src/core/server/elasticsearch/status.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable, merge, of } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { ServiceStatus, ServiceStatusLevels } from '../status'; +import { ElasticsearchStatusMeta } from './types'; +import { NodesVersionCompatibility } from './version_check/ensure_es_version'; + +export const calculateStatus$ = ( + esNodesCompatibility$: Observable +): Observable> => + merge( + of({ + level: ServiceStatusLevels.unavailable, + summary: `Waiting for Elasticsearch`, + meta: { + warningNodes: [], + incompatibleNodes: [], + }, + }), + esNodesCompatibility$.pipe( + map( + ({ + isCompatible, + message, + incompatibleNodes, + warningNodes, + }): ServiceStatus => { + if (!isCompatible) { + return { + level: ServiceStatusLevels.critical, + summary: + // Message should always be present, but this is a safe fallback + message ?? + `Some Elasticsearch nodes are not compatible with this version of Kibana`, + meta: { warningNodes, incompatibleNodes }, + }; + } else if (warningNodes.length > 0) { + return { + level: ServiceStatusLevels.degraded, + summary: + // Message should always be present, but this is a safe fallback + message ?? + `Some Elasticsearch nodes are running different versions than this version of Kibana`, + meta: { warningNodes, incompatibleNodes }, + }; + } + + return { + level: ServiceStatusLevels.available, + summary: `Elasticsearch is available`, + meta: { + warningNodes: [], + incompatibleNodes: [], + }, + }; + } + ) + ) + ); diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index ef8edecfd26ec..3d38935e9fbf0 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -22,6 +22,7 @@ import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchClientConfig } from './elasticsearch_client_config'; import { IClusterClient, ICustomClusterClient } from './cluster_client'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; +import { ServiceStatus } from '../status'; /** * @public @@ -128,4 +129,11 @@ export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceS readonly config$: Observable; }; esNodesCompatibility$: Observable; + status$: Observable>; +} + +/** @public */ +export interface ElasticsearchStatusMeta { + warningNodes: NodesVersionCompatibility['warningNodes']; + incompatibleNodes: NodesVersionCompatibility['incompatibleNodes']; } diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.ts index 3e760ec0efabd..7bd6331978d1d 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.ts @@ -142,7 +142,7 @@ export const pollEsNodesVersion = ({ kibanaVersion, ignoreVersionMismatch, esVersionCheckInterval: healthCheckInterval, -}: PollEsNodesVersionOptions): Observable => { +}: PollEsNodesVersionOptions): Observable => { log.debug('Checking Elasticsearch version'); return timer(0, healthCheckInterval).pipe( exhaustMap(() => { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 56ce16a951aa2..a298f80f96d8f 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -60,6 +60,7 @@ import { import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { UuidServiceSetup } from './uuid'; import { MetricsServiceSetup } from './metrics'; +import { StatusServiceSetup } from './status'; export { bootstrap } from './bootstrap'; export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities'; @@ -95,6 +96,8 @@ export { ElasticsearchErrorHelpers, ElasticsearchServiceSetup, ElasticsearchServiceStart, + ElasticsearchStatusMeta, + NodesVersionCompatibility, APICaller, FakeRequest, ScopeableRequest, @@ -226,6 +229,7 @@ export { SavedObjectsUpdateResponse, SavedObjectsServiceStart, SavedObjectsServiceSetup, + SavedObjectStatusMeta, SavedObjectsDeleteOptions, ISavedObjectsRepository, SavedObjectsRepository, @@ -294,6 +298,14 @@ export { LegacyInternals, } from './legacy'; +export { + CoreStatus, + ServiceStatus, + ServiceStatusLevel, + ServiceStatusLevels, + StatusServiceSetup, +} from './status'; + /** * Plugin specific context passed to a route handler. * @@ -348,14 +360,16 @@ export interface CoreSetup; } diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 825deea99bc23..ede0d3dc9fcc7 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -31,6 +31,7 @@ import { import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings'; import { UuidServiceSetup } from './uuid'; import { InternalMetricsServiceSetup } from './metrics'; +import { InternalStatusServiceSetup } from './status'; /** @internal */ export interface InternalCoreSetup { @@ -38,10 +39,11 @@ export interface InternalCoreSetup { context: ContextSetup; http: InternalHttpServiceSetup; elasticsearch: InternalElasticsearchServiceSetup; - uiSettings: InternalUiSettingsServiceSetup; + metrics: InternalMetricsServiceSetup; savedObjects: InternalSavedObjectsServiceSetup; + status: InternalStatusServiceSetup; + uiSettings: InternalUiSettingsServiceSetup; uuid: UuidServiceSetup; - metrics: InternalMetricsServiceSetup; } /** diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index c6860086e7784..0cf2ebe55ea10 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -48,6 +48,7 @@ import { findLegacyPluginSpecs } from './plugins'; import { LegacyVars, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; import { LegacyService } from './legacy_service'; import { coreMock } from '../mocks'; +import { statusServiceMock } from '../status/status_service.mock'; const MockKbnServer: jest.Mock = KbnServer as any; @@ -106,6 +107,7 @@ beforeEach(() => { rendering: renderingServiceMock, metrics: metricsServiceMock.createInternalSetupContract(), uuid: uuidSetup, + status: statusServiceMock.createInternalSetupContract(), }, plugins: { 'plugin-id': 'plugin-value' }, }; diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index bb5f6d5617aae..f77230301ce02 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -306,6 +306,9 @@ export class LegacyService implements CoreService { registerType: setupDeps.core.savedObjects.registerType, getImportExportObjectLimit: setupDeps.core.savedObjects.getImportExportObjectLimit, }, + status: { + core$: setupDeps.core.status.core$, + }, uiSettings: { register: setupDeps.core.uiSettings.register, }, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 31bf17da041af..faf73044cac4d 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -33,6 +33,7 @@ import { InternalCoreSetup, InternalCoreStart } from './internal_types'; import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; import { metricsServiceMock } from './metrics/metrics_service.mock'; import { uuidServiceMock } from './uuid/uuid_service.mock'; +import { statusServiceMock } from './status/status_service.mock'; export { httpServerMock } from './http/http_server.mocks'; export { sessionStorageMock } from './http/cookie_session_storage.mocks'; @@ -133,9 +134,10 @@ function createCoreSetupMock({ elasticsearch: elasticsearchServiceMock.createSetup(), http: httpMock, savedObjects: savedObjectsServiceMock.createInternalSetupContract(), + status: statusServiceMock.createSetupContract(), + metrics: metricsServiceMock.createSetupContract(), uiSettings: uiSettingsMock, uuid: uuidServiceMock.createSetupContract(), - metrics: metricsServiceMock.createSetupContract(), getStartServices: jest .fn, object, any]>, []>() .mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]), @@ -161,10 +163,11 @@ function createInternalCoreSetupMock() { context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createInternalSetup(), http: httpServiceMock.createSetupContract(), - uiSettings: uiSettingsServiceMock.createSetupContract(), + metrics: metricsServiceMock.createInternalSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), + status: statusServiceMock.createInternalSetupContract(), uuid: uuidServiceMock.createSetupContract(), - metrics: metricsServiceMock.createInternalSetupContract(), + uiSettings: uiSettingsServiceMock.createSetupContract(), }; return setupDeps; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 32662f07a86f0..61d97aea97459 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -175,6 +175,9 @@ export function createPluginSetupContext( registerType: deps.savedObjects.registerType, getImportExportObjectLimit: deps.savedObjects.getImportExportObjectLimit, }, + status: { + core$: deps.status.core$, + }, uiSettings: { register: deps.uiSettings.register, }, diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index b50e47b9eab73..fe4795cad11a5 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -68,7 +68,11 @@ export { SavedObjectMigrationContext, } from './migrations'; -export { SavedObjectsType, SavedObjectsTypeManagementDefinition } from './types'; +export { + SavedObjectStatusMeta, + SavedObjectsType, + SavedObjectsTypeManagementDefinition, +} from './types'; export { savedObjectsConfig, savedObjectsMigrationConfig } from './saved_objects_config'; export { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry'; diff --git a/src/core/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts index 4fbadf90f4b60..466d399f653cd 100644 --- a/src/core/server/saved_objects/migrations/core/index.ts +++ b/src/core/server/saved_objects/migrations/core/index.ts @@ -22,4 +22,4 @@ export { IndexMigrator } from './index_migrator'; export { buildActiveMappings } from './build_active_mappings'; export { CallCluster } from './call_cluster'; export { LogFn } from './migration_logger'; -export { MigrationResult } from './migration_coordinator'; +export { MigrationResult, MigrationStatus } from './migration_coordinator'; diff --git a/src/core/server/saved_objects/migrations/core/migration_coordinator.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.ts index ddd82edd93448..5ba2d0afc692e 100644 --- a/src/core/server/saved_objects/migrations/core/migration_coordinator.ts +++ b/src/core/server/saved_objects/migrations/core/migration_coordinator.ts @@ -39,6 +39,8 @@ import { SavedObjectsMigrationLogger } from './migration_logger'; const DEFAULT_POLL_INTERVAL = 15000; +export type MigrationStatus = 'waiting' | 'running' | 'completed'; + export type MigrationResult = | { status: 'skipped' } | { status: 'patched' } diff --git a/src/core/server/saved_objects/migrations/index.ts b/src/core/server/saved_objects/migrations/index.ts index dc966f0797822..8ddaed3707eb0 100644 --- a/src/core/server/saved_objects/migrations/index.ts +++ b/src/core/server/saved_objects/migrations/index.ts @@ -17,6 +17,7 @@ * under the License. */ +export { MigrationResult } from './core'; export { KibanaMigrator, IKibanaMigrator } from './kibana'; export { SavedObjectMigrationFn, diff --git a/src/core/server/saved_objects/migrations/kibana/index.ts b/src/core/server/saved_objects/migrations/kibana/index.ts index 25772c4c9b0b1..df4751521ac53 100644 --- a/src/core/server/saved_objects/migrations/kibana/index.ts +++ b/src/core/server/saved_objects/migrations/kibana/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { KibanaMigrator, IKibanaMigrator } from './kibana_migrator'; +export { KibanaMigrator, IKibanaMigrator, KibanaMigratorStatus } from './kibana_migrator'; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts index 2ee656721abd0..257b32c1e4c23 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts @@ -17,10 +17,11 @@ * under the License. */ -import { KibanaMigrator } from './kibana_migrator'; +import { KibanaMigrator, KibanaMigratorStatus } from './kibana_migrator'; import { buildActiveMappings } from '../core'; const { mergeTypes } = jest.requireActual('./kibana_migrator'); import { SavedObjectsType } from '../../types'; +import { BehaviorSubject } from 'rxjs'; const defaultSavedObjectTypes: SavedObjectsType[] = [ { @@ -47,6 +48,20 @@ const createMigrator = ( runMigrations: jest.fn(), getActiveMappings: jest.fn(), migrateDocument: jest.fn(), + getStatus$: jest.fn( + () => + new BehaviorSubject({ + status: 'completed', + result: [ + { + status: 'migrated', + destIndex: '.test-kibana_2', + sourceIndex: '.test-kibana_1', + elapsedMs: 10, + }, + ], + }) + ), }; mockMigrator.getActiveMappings.mockReturnValue(buildActiveMappings(mergeTypes(types))); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index fd82bf282266e..336eeff99f47b 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { take } from 'rxjs/operators'; import { KibanaMigratorOptions, KibanaMigrator } from './kibana_migrator'; import { loggingServiceMock } from '../../../logging/logging_service.mock'; @@ -79,6 +80,33 @@ describe('KibanaMigrator', () => { .filter(callClusterPath => callClusterPath === 'cat.templates'); expect(callClusterCommands.length).toBe(1); }); + + it('emits results on getMigratorResult$()', async () => { + const options = mockOptions(); + const clusterStub = jest.fn(() => ({ status: 404 })); + + options.callCluster = clusterStub; + const migrator = new KibanaMigrator(options); + const migratorStatus = migrator + .getStatus$() + .pipe(take(3)) + .toPromise(); + await migrator.runMigrations(); + const { status, result } = await migratorStatus; + expect(status).toEqual('completed'); + expect(result![0]).toMatchObject({ + destIndex: '.my-index_1', + elapsedMs: expect.any(Number), + sourceIndex: '.my-index', + status: 'migrated', + }); + expect(result![1]).toMatchObject({ + destIndex: 'other-index_1', + elapsedMs: expect.any(Number), + sourceIndex: 'other-index', + status: 'migrated', + }); + }); }); }); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index bc29061b380b8..dafd6c5341196 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -24,10 +24,17 @@ import { Logger } from 'src/core/server/logging'; import { KibanaConfigType } from 'src/core/server/kibana_config'; +import { BehaviorSubject } from 'rxjs'; import { IndexMapping, SavedObjectsTypeMappingDefinitions } from '../../mappings'; import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization'; import { docValidator, PropertyValidators } from '../../validation'; -import { buildActiveMappings, CallCluster, IndexMigrator } from '../core'; +import { + buildActiveMappings, + CallCluster, + IndexMigrator, + MigrationResult, + MigrationStatus, +} from '../core'; import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator'; import { createIndexMap } from '../core/build_index_map'; import { SavedObjectsMigrationConfigType } from '../../saved_objects_config'; @@ -46,6 +53,11 @@ export interface KibanaMigratorOptions { export type IKibanaMigrator = Pick; +export interface KibanaMigratorStatus { + status: MigrationStatus; + result?: MigrationResult[]; +} + /** * Manages the shape of mappings and documents in the Kibana index. */ @@ -58,7 +70,10 @@ export class KibanaMigrator { private readonly mappingProperties: SavedObjectsTypeMappingDefinitions; private readonly typeRegistry: ISavedObjectTypeRegistry; private readonly serializer: SavedObjectsSerializer; - private migrationResult?: Promise>; + private migrationResult?: Promise; + private readonly status$ = new BehaviorSubject({ + status: 'waiting', + }); /** * Creates an instance of KibanaMigrator. @@ -109,12 +124,20 @@ export class KibanaMigrator { Array<{ status: string }> > { if (this.migrationResult === undefined || rerun) { - this.migrationResult = this.runMigrationsInternal(); + this.status$.next({ status: 'running' }); + this.migrationResult = this.runMigrationsInternal().then(result => { + this.status$.next({ status: 'completed', result }); + return result; + }); } return this.migrationResult; } + public getStatus$() { + return this.status$.asObservable(); + } + private runMigrationsInternal() { const kibanaIndexName = this.kibanaConfig.index; const indexMap = createIndexMap({ diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index 9fe32b14e6450..7ba4613c857d7 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -17,6 +17,8 @@ * under the License. */ +import { BehaviorSubject } from 'rxjs'; + import { SavedObjectsService, InternalSavedObjectsServiceSetup, @@ -29,6 +31,7 @@ import { savedObjectsClientProviderMock } from './service/lib/scoped_client_prov import { savedObjectsRepositoryMock } from './service/lib/repository.mock'; import { savedObjectsClientMock } from './service/saved_objects_client.mock'; import { typeRegistryMock } from './saved_objects_type_registry.mock'; +import { ServiceStatusLevels } from '../status'; type SavedObjectsServiceContract = PublicMethodsOf; @@ -75,6 +78,10 @@ const createSetupContractMock = () => { const createInternalSetupContractMock = () => { const internalSetupContract: jest.Mocked = { ...createSetupContractMock(), + status$: new BehaviorSubject({ + level: ServiceStatusLevels.available, + summary: `SavedObjects is available`, + }), }; return internalSetupContract; }; diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index aa440c6454569..62027928c0bb5 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -17,8 +17,8 @@ * under the License. */ -import { Subject } from 'rxjs'; -import { first, filter, take } from 'rxjs/operators'; +import { Subject, Observable } from 'rxjs'; +import { first, filter, take, switchMap } from 'rxjs/operators'; import { CoreService } from '../../types'; import { SavedObjectsClient, @@ -38,7 +38,7 @@ import { SavedObjectConfig, } from './saved_objects_config'; import { KibanaRequest, InternalHttpServiceSetup } from '../http'; -import { SavedObjectsClientContract, SavedObjectsType } from './types'; +import { SavedObjectsClientContract, SavedObjectsType, SavedObjectStatusMeta } from './types'; import { ISavedObjectsRepository, SavedObjectsRepository } from './service/lib/repository'; import { SavedObjectsClientFactoryProvider, @@ -50,6 +50,8 @@ import { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objec import { PropertyValidators } from './validation'; import { SavedObjectsSerializer } from './serialization'; import { registerRoutes } from './routes'; +import { ServiceStatus } from '../status'; +import { calculateStatus$ } from './status'; /** * Saved Objects is Kibana's data persistence mechanism allowing plugins to @@ -164,7 +166,9 @@ export interface SavedObjectsServiceSetup { /** * @internal */ -export type InternalSavedObjectsServiceSetup = SavedObjectsServiceSetup; +export interface InternalSavedObjectsServiceSetup extends SavedObjectsServiceSetup { + status$: Observable>; +} /** * Saved Objects is Kibana's data persisentence mechanism allowing plugins to @@ -321,6 +325,10 @@ export class SavedObjectsService }); return { + status$: calculateStatus$( + this.migrator$.pipe(switchMap(migrator => migrator.getStatus$())), + setupDeps.elasticsearch.status$ + ), setClientFactoryProvider: provider => { if (this.started) { throw new Error('cannot call `setClientFactoryProvider` after service startup.'); diff --git a/src/core/server/saved_objects/status.test.ts b/src/core/server/saved_objects/status.test.ts new file mode 100644 index 0000000000000..8efea1e2c00c6 --- /dev/null +++ b/src/core/server/saved_objects/status.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { of, Observable } from 'rxjs'; +import { ServiceStatus, ServiceStatusLevels } from '../status'; +import { calculateStatus$ } from './status'; +import { take } from 'rxjs/operators'; + +describe('calculateStatus$', () => { + const expectUnavailableDueToEs = (status$: Observable) => + expect(status$.pipe(take(1)).toPromise()).resolves.toEqual({ + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is not available without a healthy Elasticearch connection`, + }); + + const expectUnavailableDueToMigrations = (status$: Observable) => + expect(status$.pipe(take(1)).toPromise()).resolves.toEqual({ + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is waiting to start migrations`, + }); + + describe('when elasticsearch is unavailable', () => { + const esStatus$ = of({ + level: ServiceStatusLevels.unavailable, + summary: 'xxx', + }); + + it('is unavailable before migrations have ran', async () => { + await expectUnavailableDueToEs(calculateStatus$(of(), esStatus$)); + }); + it('is unavailable after migrations have ran', async () => { + await expectUnavailableDueToEs( + calculateStatus$(of({ status: 'completed', result: [] }), esStatus$) + ); + }); + }); + + describe('when elasticsearch is critical', () => { + const esStatus$ = of({ + level: ServiceStatusLevels.critical, + summary: 'xxx', + }); + + it('is unavailable before migrations have ran', async () => { + await expectUnavailableDueToEs(calculateStatus$(of(), esStatus$)); + }); + it('is unavailable after migrations have ran', async () => { + await expectUnavailableDueToEs( + calculateStatus$( + of({ status: 'completed', result: [{ status: 'migrated' } as any] }), + esStatus$ + ) + ); + }); + }); + + describe('when elasticsearch is available', () => { + const esStatus$ = of({ + level: ServiceStatusLevels.available, + summary: 'Available', + }); + + it('is unavailable before migrations have ran', async () => { + await expectUnavailableDueToMigrations(calculateStatus$(of(), esStatus$)); + }); + it('is unavailable while migrations are running', async () => { + await expect( + calculateStatus$(of({ status: 'running' }), esStatus$) + .pipe(take(2)) + .toPromise() + ).resolves.toEqual({ + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is running migrations`, + }); + }); + it('is available after migrations have ran', async () => { + await expect( + calculateStatus$( + of({ status: 'completed', result: [{ status: 'skipped' }, { status: 'patched' }] }), + esStatus$ + ) + .pipe(take(2)) + .toPromise() + ).resolves.toEqual({ + level: ServiceStatusLevels.available, + summary: `SavedObjects service has completed migrations and is available`, + meta: { + migratedIndices: { + migrated: 0, + patched: 1, + skipped: 1, + }, + }, + }); + }); + }); + + describe('when elasticsearch is degraded', () => { + const esStatus$ = of({ level: ServiceStatusLevels.degraded, summary: 'xxx' }); + + it('is unavailable before migrations have ran', async () => { + await expectUnavailableDueToMigrations(calculateStatus$(of(), esStatus$)); + }); + it('is degraded after migrations have ran', async () => { + await expect( + calculateStatus$( + of([{ status: 'skipped' }]), + esStatus$ + ) + .pipe(take(2)) + .toPromise() + ).resolves.toEqual({ + level: ServiceStatusLevels.degraded, + summary: 'SavedObjects service is degraded due to Elasticsearch: [xxx]', + }); + }); + }); +}); diff --git a/src/core/server/saved_objects/status.ts b/src/core/server/saved_objects/status.ts new file mode 100644 index 0000000000000..66a6e2baa17a7 --- /dev/null +++ b/src/core/server/saved_objects/status.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable, combineLatest } from 'rxjs'; +import { startWith, map } from 'rxjs/operators'; +import { ServiceStatus, ServiceStatusLevels } from '../status'; +import { SavedObjectStatusMeta } from './types'; +import { KibanaMigratorStatus } from './migrations/kibana'; + +export const calculateStatus$ = ( + rawMigratorStatus$: Observable, + elasticsearchStatus$: Observable +): Observable> => { + const migratorStatus$: Observable> = rawMigratorStatus$.pipe( + map(migrationStatus => { + if (migrationStatus.status === 'waiting') { + return { + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is waiting to start migrations`, + }; + } else if (migrationStatus.status === 'running') { + return { + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is running migrations`, + }; + } + + const statusCounts: SavedObjectStatusMeta['migratedIndices'] = { migrated: 0, skipped: 0 }; + if (migrationStatus.result) { + migrationStatus.result.forEach(({ status }) => { + statusCounts[status] = (statusCounts[status] ?? 0) + 1; + }); + } + + return { + level: ServiceStatusLevels.available, + summary: `SavedObjects service has completed migrations and is available`, + meta: { + migratedIndices: statusCounts, + }, + }; + }), + startWith({ + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is waiting to start migrations`, + }) + ); + + return combineLatest([elasticsearchStatus$, migratorStatus$]).pipe( + map(([esStatus, migratorStatus]) => { + if (esStatus.level >= ServiceStatusLevels.unavailable) { + return { + level: ServiceStatusLevels.unavailable, + summary: `SavedObjects service is not available without a healthy Elasticearch connection`, + }; + } else if (migratorStatus.level === ServiceStatusLevels.unavailable) { + return migratorStatus; + } else if (esStatus.level === ServiceStatusLevels.degraded) { + return { + level: esStatus.level, + summary: `SavedObjects service is degraded due to Elasticsearch: [${esStatus.summary}]`, + }; + } else { + return migratorStatus; + } + }) + ); +}; diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 962965a08f8b2..f14e9d9efb5e3 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -46,6 +46,19 @@ export { SavedObjectsMigrationVersion, } from '../../types'; +/** + * Meta information about the SavedObjectService's status. Available to plugins via {@link CoreSetup.status}. + * + * @public + */ +export interface SavedObjectStatusMeta { + migratedIndices: { + [status: string]: number; + skipped: number; + migrated: number; + }; +} + /** * * @public diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index e4e2b8d7adbb7..f3e3b7736d8d3 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -638,6 +638,8 @@ export interface CoreSetup ISavedObjectTypeRegistry; } +// @public +export interface SavedObjectStatusMeta { + // (undocumented) + migratedIndices: { + [status: string]: number; + skipped: number; + migrated: number; + }; +} + // @public (undocumented) export interface SavedObjectsType { convertToAliasScript?: string; @@ -2237,6 +2283,38 @@ export class ScopedClusterClient implements IScopedClusterClient { callAsInternalUser(endpoint: string, clientParams?: Record, options?: CallAPIOptions): Promise; } +// @public +export interface ServiceStatus | unknown = unknown> { + detail?: string; + documentationUrl?: string; + level: ServiceStatusLevel; + meta?: Meta; + summary: string; +} + +// @public +export type ServiceStatusLevel = typeof ServiceStatusLevels[keyof typeof ServiceStatusLevels]; + +// @public +export const ServiceStatusLevels: Readonly<{ + available: Readonly<{ + toString: () => "available"; + valueOf: () => 0; + }>; + degraded: Readonly<{ + toString: () => "degraded"; + valueOf: () => 1; + }>; + unavailable: Readonly<{ + toString: () => "unavailable"; + valueOf: () => 2; + }>; + critical: Readonly<{ + toString: () => "critical"; + valueOf: () => 3; + }>; +}>; + // @public export interface SessionCookieValidationResult { isValid: boolean; @@ -2274,6 +2352,11 @@ export type SharedGlobalConfig = RecursiveReadonly_2<{ // @public export type StartServicesAccessor = () => Promise<[CoreStart, TPluginsStart, TStart]>; +// @public +export interface StatusServiceSetup { + core$: Observable; +} + // @public export type StringValidation = StringValidationRegex | StringValidationRegexString; diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 53d1b742a6494..5d535c9845724 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -85,3 +85,9 @@ export const mockMetricsService = metricsServiceMock.create(); jest.doMock('./metrics/metrics_service', () => ({ MetricsService: jest.fn(() => mockMetricsService), })); + +import { statusServiceMock } from './status/status_service.mock'; +export const mockStatusService = statusServiceMock.create(); +jest.doMock('./status/status_service', () => ({ + StatusService: jest.fn(() => mockStatusService), +})); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index a4b5a9d81df20..24c41d511180a 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -29,6 +29,7 @@ import { mockUiSettingsService, mockRenderingService, mockMetricsService, + mockStatusService, } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -63,6 +64,7 @@ test('sets up services on "setup"', async () => { expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockRenderingService.setup).not.toHaveBeenCalled(); expect(mockMetricsService.setup).not.toHaveBeenCalled(); + expect(mockStatusService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -74,6 +76,7 @@ test('sets up services on "setup"', async () => { expect(mockUiSettingsService.setup).toHaveBeenCalledTimes(1); expect(mockRenderingService.setup).toHaveBeenCalledTimes(1); expect(mockMetricsService.setup).toHaveBeenCalledTimes(1); + expect(mockStatusService.setup).toHaveBeenCalledTimes(1); }); test('injects legacy dependency to context#setup()', async () => { @@ -141,6 +144,7 @@ test('stops services on "stop"', async () => { expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); expect(mockUiSettingsService.stop).not.toHaveBeenCalled(); expect(mockMetricsService.stop).not.toHaveBeenCalled(); + expect(mockStatusService.stop).not.toHaveBeenCalled(); await server.stop(); @@ -151,6 +155,7 @@ test('stops services on "stop"', async () => { expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.stop).toHaveBeenCalledTimes(1); expect(mockMetricsService.stop).toHaveBeenCalledTimes(1); + expect(mockStatusService.stop).toHaveBeenCalledTimes(1); }); test(`doesn't setup core services if config validation fails`, async () => { @@ -167,6 +172,7 @@ test(`doesn't setup core services if config validation fails`, async () => { expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockRenderingService.setup).not.toHaveBeenCalled(); expect(mockMetricsService.setup).not.toHaveBeenCalled(); + expect(mockStatusService.setup).not.toHaveBeenCalled(); }); test(`doesn't setup core services if legacy config validation fails`, async () => { @@ -187,4 +193,5 @@ test(`doesn't setup core services if legacy config validation fails`, async () = expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockMetricsService.setup).not.toHaveBeenCalled(); + expect(mockStatusService.setup).not.toHaveBeenCalled(); }); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 222be572b75e4..07ea431dd3a0d 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -36,6 +36,9 @@ import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; import { SavedObjectsService } from '../server/saved_objects'; import { MetricsService, opsConfig } from './metrics'; +import { CapabilitiesService } from './capabilities'; +import { UuidService } from './uuid'; +import { StatusService } from './status/status_service'; import { config as cspConfig } from './csp'; import { config as elasticsearchConfig } from './elasticsearch'; @@ -50,8 +53,6 @@ import { mapToObject } from '../utils'; import { ContextService } from './context'; import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart } from './internal_types'; -import { CapabilitiesService } from './capabilities'; -import { UuidService } from './uuid'; const coreId = Symbol('core'); const rootConfigPath = ''; @@ -70,6 +71,7 @@ export class Server { private readonly uiSettings: UiSettingsService; private readonly uuid: UuidService; private readonly metrics: MetricsService; + private readonly status: StatusService; private readonly coreApp: CoreApp; private pluginsInitialized?: boolean; @@ -95,6 +97,7 @@ export class Server { this.capabilities = new CapabilitiesService(core); this.uuid = new UuidService(core); this.metrics = new MetricsService(core); + this.status = new StatusService(core); this.coreApp = new CoreApp(core); } @@ -145,15 +148,21 @@ export class Server { const metricsSetup = await this.metrics.setup({ http: httpSetup }); + const statusSetup = this.status.setup({ + elasticsearch: elasticsearchServiceSetup, + savedObjects: savedObjectsSetup, + }); + const coreSetup: InternalCoreSetup = { capabilities: capabilitiesSetup, context: contextServiceSetup, elasticsearch: elasticsearchServiceSetup, http: httpSetup, - uiSettings: uiSettingsSetup, + metrics: metricsSetup, savedObjects: savedObjectsSetup, + status: statusSetup, + uiSettings: uiSettingsSetup, uuid: uuidSetup, - metrics: metricsSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); @@ -220,6 +229,7 @@ export class Server { await this.uiSettings.stop(); await this.rendering.stop(); await this.metrics.stop(); + await this.status.stop(); } private registerCoreContext(coreSetup: InternalCoreSetup, rendering: RenderingServiceSetup) { diff --git a/src/core/server/status/get_summary_status.test.ts b/src/core/server/status/get_summary_status.test.ts new file mode 100644 index 0000000000000..7516e82ee784d --- /dev/null +++ b/src/core/server/status/get_summary_status.test.ts @@ -0,0 +1,180 @@ +/* + * 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 { ServiceStatus, ServiceStatusLevels } from './types'; +import { getSummaryStatus } from './get_summary_status'; + +describe('getSummaryStatus', () => { + const available: ServiceStatus = { level: ServiceStatusLevels.available, summary: 'Available' }; + const degraded: ServiceStatus = { + level: ServiceStatusLevels.degraded, + summary: 'This is degraded!', + }; + const unavailable: ServiceStatus = { + level: ServiceStatusLevels.unavailable, + summary: 'This is unavailable!', + }; + const critical: ServiceStatus = { + level: ServiceStatusLevels.critical, + summary: 'This is critical!', + }; + + it('returns available when all status are available', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: available, + s2: available, + s3: available, + }) + ) + ).toMatchObject({ + level: ServiceStatusLevels.available, + }); + }); + + it('returns degraded when the worst status is degraded', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: available, + s2: degraded, + s3: available, + }) + ) + ).toMatchObject({ + level: ServiceStatusLevels.degraded, + }); + }); + + it('returns unavailable when the worst status is unavailable', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: available, + s2: degraded, + s3: unavailable, + }) + ) + ).toMatchObject({ + level: ServiceStatusLevels.unavailable, + }); + }); + + it('returns critical when the worst status is critical', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: critical, + s2: degraded, + s3: unavailable, + }) + ) + ).toMatchObject({ + level: ServiceStatusLevels.critical, + }); + }); + + describe('summary', () => { + describe('when a single service is at highest level', () => { + it('returns all information about that single service', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: degraded, + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + detail: 'Vivamus pulvinar sem ac luctus ultrices.', + documentationUrl: 'http://helpmenow.com/problem1', + meta: { + custom: { data: 'here' }, + }, + }, + }) + ) + ).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[s2]: Lorem ipsum', + detail: 'Vivamus pulvinar sem ac luctus ultrices.', + documentationUrl: 'http://helpmenow.com/problem1', + meta: { + custom: { data: 'here' }, + }, + }); + }); + }); + + describe('when multiple services is at highest level', () => { + it('returns aggregated information about the affected services', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: degraded, + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + detail: 'Vivamus pulvinar sem ac luctus ultrices.', + documentationUrl: 'http://helpmenow.com/problem1', + meta: { + custom: { data: 'here' }, + }, + }, + s3: { + level: ServiceStatusLevels.unavailable, + summary: 'Proin mattis', + detail: 'Nunc quis nulla at mi lobortis pretium.', + documentationUrl: 'http://helpmenow.com/problem2', + meta: { + other: { data: 'over there' }, + }, + }, + }) + ) + ).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[2] services are unavailable', + detail: 'See the status page for more information', + meta: { + affectedServices: { + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + detail: 'Vivamus pulvinar sem ac luctus ultrices.', + documentationUrl: 'http://helpmenow.com/problem1', + meta: { + custom: { data: 'here' }, + }, + }, + s3: { + level: ServiceStatusLevels.unavailable, + summary: 'Proin mattis', + detail: 'Nunc quis nulla at mi lobortis pretium.', + documentationUrl: 'http://helpmenow.com/problem2', + meta: { + other: { data: 'over there' }, + }, + }, + }, + }, + }); + }); + }); + }); +}); diff --git a/src/core/server/status/get_summary_status.ts b/src/core/server/status/get_summary_status.ts new file mode 100644 index 0000000000000..748a54f0bf8bb --- /dev/null +++ b/src/core/server/status/get_summary_status.ts @@ -0,0 +1,84 @@ +/* + * 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 { ServiceStatus, ServiceStatusLevels, ServiceStatusLevel } from './types'; + +/** + * Returns a single {@link ServiceStatus} that summarizes the most severe status level from a group of statuses. + * @param statuses + */ +export const getSummaryStatus = (statuses: Array<[string, ServiceStatus]>): ServiceStatus => { + const grouped = groupByLevel(statuses); + const highestSeverityLevel = getHighestSeverityLevel(grouped.keys()); + const highestSeverityGroup = grouped.get(highestSeverityLevel)!; + + if (highestSeverityLevel === ServiceStatusLevels.available) { + return { + level: ServiceStatusLevels.available, + summary: `All services are available`, + }; + } else if (highestSeverityGroup.size === 1) { + const [serviceName, status] = [...highestSeverityGroup.entries()][0]; + return { + ...status, + summary: `[${serviceName}]: ${status.summary!}`, + }; + } else { + return { + level: highestSeverityLevel, + summary: `[${highestSeverityGroup.size}] services are ${highestSeverityLevel.toString()}`, + // TODO: include URL to status page + detail: `See the status page for more information`, + meta: { + affectedServices: Object.fromEntries([...highestSeverityGroup]), + }, + }; + } +}; + +const groupByLevel = ( + statuses: Array<[string, ServiceStatus]> +): Map> => { + const byLevel = new Map>(); + + for (const [serviceName, status] of statuses) { + let levelMap = byLevel.get(status.level); + if (!levelMap) { + levelMap = new Map(); + byLevel.set(status.level, levelMap); + } + + levelMap.set(serviceName, status); + } + + return byLevel; +}; + +const getHighestSeverityLevel = (levels: Iterable): ServiceStatusLevel => { + const sorted = [...levels].sort((a, b) => { + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } else { + return 0; + } + }); + return sorted[sorted.length - 1] ?? ServiceStatusLevels.available; +}; diff --git a/src/core/server/status/index.ts b/src/core/server/status/index.ts new file mode 100644 index 0000000000000..c39115d55a682 --- /dev/null +++ b/src/core/server/status/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { StatusService } from './status_service'; +export * from './types'; diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts new file mode 100644 index 0000000000000..d550c2f06750b --- /dev/null +++ b/src/core/server/status/status_service.mock.ts @@ -0,0 +1,71 @@ +/* + * 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 { StatusService } from './status_service'; +import { + InternalStatusServiceSetup, + StatusServiceSetup, + ServiceStatusLevels, + ServiceStatus, + CoreStatus, +} from './types'; +import { BehaviorSubject } from 'rxjs'; + +const available: ServiceStatus = { + level: ServiceStatusLevels.available, + summary: 'Service is working', +}; +const availableCoreStatus: CoreStatus = { + elasticsearch: available, + savedObjects: available, +}; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + core$: new BehaviorSubject(availableCoreStatus), + }; + + return setupContract; +}; + +const createInternalSetupContractMock = () => { + const setupContract: jest.Mocked = { + core$: new BehaviorSubject(availableCoreStatus), + overall$: new BehaviorSubject(available), + }; + + return setupContract; +}; + +type StatusServiceContract = PublicMethodsOf; + +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn().mockReturnValue(createInternalSetupContractMock()), + start: jest.fn(), + stop: jest.fn(), + }; + return mocked; +}; + +export const statusServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, + createInternalSetupContract: createInternalSetupContractMock, +}; diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts new file mode 100644 index 0000000000000..6d92a266369b9 --- /dev/null +++ b/src/core/server/status/status_service.test.ts @@ -0,0 +1,229 @@ +/* + * 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 { of, BehaviorSubject } from 'rxjs'; + +import { ServiceStatus, ServiceStatusLevels, CoreStatus } from './types'; +import { StatusService } from './status_service'; +import { first } from 'rxjs/operators'; +import { mockCoreContext } from '../core_context.mock'; +import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; + +expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); + +describe('StatusService', () => { + const available: ServiceStatus = { + level: ServiceStatusLevels.available, + summary: 'Available', + }; + const degraded: ServiceStatus = { + level: ServiceStatusLevels.degraded, + summary: 'This is degraded!', + }; + + describe('setup', () => { + describe('core$', () => { + it('rolls up core status observables into single observable', async () => { + const setup = new StatusService(mockCoreContext.create()).setup({ + elasticsearch: { + status$: of(available), + }, + savedObjects: { + status$: of(degraded), + }, + }); + expect(await setup.core$.pipe(first()).toPromise()).toEqual({ + elasticsearch: available, + savedObjects: degraded, + }); + }); + + it('replays last event', async () => { + const setup = new StatusService(mockCoreContext.create()).setup({ + elasticsearch: { + status$: of(available), + }, + savedObjects: { + status$: of(degraded), + }, + }); + const subResult1 = await setup.core$.pipe(first()).toPromise(); + const subResult2 = await setup.core$.pipe(first()).toPromise(); + const subResult3 = await setup.core$.pipe(first()).toPromise(); + expect(subResult1).toEqual({ + elasticsearch: available, + savedObjects: degraded, + }); + expect(subResult2).toEqual({ + elasticsearch: available, + savedObjects: degraded, + }); + expect(subResult3).toEqual({ + elasticsearch: available, + savedObjects: degraded, + }); + }); + + it('does not emit duplicate events', () => { + const elasticsearch$ = new BehaviorSubject(available); + const savedObjects$ = new BehaviorSubject(degraded); + const setup = new StatusService(mockCoreContext.create()).setup({ + elasticsearch: { + status$: elasticsearch$, + }, + savedObjects: { + status$: savedObjects$, + }, + }); + + const statusUpdates: CoreStatus[] = []; + const subscription = setup.core$.subscribe(status => statusUpdates.push(status)); + + elasticsearch$.next(available); + elasticsearch$.next(available); + elasticsearch$.next({ + level: ServiceStatusLevels.available, + summary: `Wow another summary`, + }); + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(available); + subscription.unsubscribe(); + + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "elasticsearch": Object { + "level": available, + "summary": "Available", + }, + "savedObjects": Object { + "level": degraded, + "summary": "This is degraded!", + }, + }, + Object { + "elasticsearch": Object { + "level": available, + "summary": "Wow another summary", + }, + "savedObjects": Object { + "level": degraded, + "summary": "This is degraded!", + }, + }, + Object { + "elasticsearch": Object { + "level": available, + "summary": "Wow another summary", + }, + "savedObjects": Object { + "level": available, + "summary": "Available", + }, + }, + ] + `); + }); + }); + + describe('overall$', () => { + it('exposes an overall summary', async () => { + const setup = new StatusService(mockCoreContext.create()).setup({ + elasticsearch: { + status$: of(degraded), + }, + savedObjects: { + status$: of(degraded), + }, + }); + expect(await setup.overall$.pipe(first()).toPromise()).toMatchObject({ + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + }); + }); + + it('replays last event', async () => { + const setup = new StatusService(mockCoreContext.create()).setup({ + elasticsearch: { + status$: of(degraded), + }, + savedObjects: { + status$: of(degraded), + }, + }); + const subResult1 = await setup.overall$.pipe(first()).toPromise(); + const subResult2 = await setup.overall$.pipe(first()).toPromise(); + const subResult3 = await setup.overall$.pipe(first()).toPromise(); + expect(subResult1).toMatchObject({ + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + }); + expect(subResult2).toMatchObject({ + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + }); + expect(subResult3).toMatchObject({ + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + }); + }); + + it('does not emit duplicate events', () => { + const elasticsearch$ = new BehaviorSubject(available); + const savedObjects$ = new BehaviorSubject(degraded); + const setup = new StatusService(mockCoreContext.create()).setup({ + elasticsearch: { + status$: elasticsearch$, + }, + savedObjects: { + status$: savedObjects$, + }, + }); + + const statusUpdates: ServiceStatus[] = []; + const subscription = setup.overall$.subscribe(status => statusUpdates.push(status)); + + elasticsearch$.next(available); + elasticsearch$.next(available); + elasticsearch$.next({ + level: ServiceStatusLevels.available, + summary: `Wow another summary`, + }); + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(available); + subscription.unsubscribe(); + + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "level": degraded, + "summary": "[savedObjects]: This is degraded!", + }, + Object { + "level": available, + "summary": "All services are available", + }, + ] + `); + }); + }); + }); +}); diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts new file mode 100644 index 0000000000000..b6697d8221951 --- /dev/null +++ b/src/core/server/status/status_service.ts @@ -0,0 +1,78 @@ +/* + * 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 */ + +import { Observable, combineLatest } from 'rxjs'; +import { map, distinctUntilChanged, shareReplay } from 'rxjs/operators'; +import { isDeepStrictEqual } from 'util'; + +import { CoreService } from '../../types'; +import { CoreContext } from '../core_context'; +import { Logger } from '../logging'; +import { InternalElasticsearchServiceSetup } from '../elasticsearch'; +import { InternalSavedObjectsServiceSetup } from '../saved_objects'; + +import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; +import { getSummaryStatus } from './get_summary_status'; + +interface SetupDeps { + elasticsearch: Pick; + savedObjects: Pick; +} + +export class StatusService implements CoreService { + private readonly logger: Logger; + + constructor(coreContext: CoreContext) { + this.logger = coreContext.logger.get('status'); + } + + public setup(core: SetupDeps) { + const core$ = this.setupCoreStatus(core); + const overall$: Observable = core$.pipe( + map(coreStatus => { + const summary = getSummaryStatus(Object.entries(coreStatus)); + this.logger.debug(`Recalculated overall status`, { status: summary }); + return summary; + }), + distinctUntilChanged(isDeepStrictEqual) + ); + + return { + core$, + overall$, + }; + } + + public start() {} + + public stop() {} + + private setupCoreStatus({ elasticsearch, savedObjects }: SetupDeps): Observable { + return combineLatest([elasticsearch.status$, savedObjects.status$]).pipe( + map(([elasticsearchStatus, savedObjectsStatus]) => ({ + elasticsearch: elasticsearchStatus, + savedObjects: savedObjectsStatus, + })), + distinctUntilChanged(isDeepStrictEqual), + shareReplay(1) + ); + } +} diff --git a/src/core/server/status/test_utils.ts b/src/core/server/status/test_utils.ts new file mode 100644 index 0000000000000..765fa8771f375 --- /dev/null +++ b/src/core/server/status/test_utils.ts @@ -0,0 +1,25 @@ +/* + * 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 { ServiceStatusLevels, ServiceStatusLevel } from './types'; + +export const ServiceStatusLevelSnapshotSerializer: jest.SnapshotSerializerPlugin = { + test: (val: any) => Object.values(ServiceStatusLevels).includes(val), + print: (val: ServiceStatusLevel) => val.toString(), +}; diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts new file mode 100644 index 0000000000000..84a7356c66bbf --- /dev/null +++ b/src/core/server/status/types.ts @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { deepFreeze } from '../../utils'; + +/** + * The current status of a service at a point in time. + * + * @typeParam Meta - JSON-serializable object. Plugins should export this type to allow other plugins to read the `meta` + * field in a type-safe way. + * @public + */ +export interface ServiceStatus | unknown = unknown> { + /** + * The current availability level of the service. + */ + level: ServiceStatusLevel; + /** + * A high-level summary of the service status. + */ + summary: string; + /** + * A more detailed description of the service status. + */ + detail?: string; + /** + * A URL to open in a new tab about how to resolve or troubleshoot the problem. + */ + documentationUrl?: string; + /** + * Any JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained, + * machine-readable information about the service status. May include status information for underlying features. + */ + meta?: Meta; +} + +/** + * The current "level" of availability of a service. + * + * @remarks + * The values implement `valueOf` to allow for easy comparisons between status levels with <, >, etc. Higher values + * represent higher severities. Note that the default `Array.prototype.sort` implementation does not correctly sort + * these values. + * + * A snapshot serializer is available in `src/core/server/test_utils` to ease testing of these values with Jest. + * + * @public + */ +export const ServiceStatusLevels = deepFreeze({ + /** + * Everything is working! + */ + available: { + toString: () => 'available', + valueOf: () => 0, + }, + /** + * Some features may not be working. + */ + degraded: { + toString: () => 'degraded', + valueOf: () => 1, + }, + /** + * The service is unavailable, but other functions that do not depend on this service should work. + */ + unavailable: { + toString: () => 'unavailable', + valueOf: () => 2, + }, + /** + * Block all user functions and display the status page, reserved for Core services only. + */ + critical: { + toString: () => 'critical', + valueOf: () => 3, + }, +}); + +/** + * A convenience type that represents the union of each value in {@link ServiceStatusLevels}. + * @public + */ +export type ServiceStatusLevel = typeof ServiceStatusLevels[keyof typeof ServiceStatusLevels]; + +/** + * Status of core services. + * + * @internalRemarks + * Only contains entries for backend services that could have a non-available `status`. + * For example, `context` cannot possibly be broken, so it is not included. + * + * @public + */ +export interface CoreStatus { + elasticsearch: ServiceStatus; + savedObjects: ServiceStatus; +} + +/** + * API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status. + * @public + */ +export interface StatusServiceSetup { + /** + * Current status for all Core services. + */ + core$: Observable; +} + +/** @internal */ +export interface InternalStatusServiceSetup extends StatusServiceSetup { + /** + * Overall system status used for HTTP API + */ + overall$: Observable; +} diff --git a/src/core/server/test_utils.ts b/src/core/server/test_utils.ts index 470b1c2d135b7..f7e6fbcd0c131 100644 --- a/src/core/server/test_utils.ts +++ b/src/core/server/test_utils.ts @@ -18,3 +18,4 @@ */ export { createHttpServer } from './http/test_utils'; +export { ServiceStatusLevelSnapshotSerializer } from './status/test_utils'; From e0a519424fce5758434b914c38d877cbc7588f93 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Wed, 8 Apr 2020 15:10:44 -0500 Subject: [PATCH 35/81] Index pattern management plugin - src/legacy/core_plugins/management => new platform plugin (#62594) * implement index pattern management plugin in new platform --- .i18nrc.json | 1 + .../services => kibana/public}/index.ts | 5 +- .../step_index_pattern.test.tsx | 2 +- .../step_index_pattern/step_index_pattern.tsx | 2 +- .../step_time_field/step_time_field.test.tsx | 2 +- .../step_time_field/step_time_field.tsx | 2 +- .../create_index_pattern_wizard/index.js | 3 +- .../lib/get_indices.test.ts | 2 +- .../lib/get_indices.ts | 2 +- .../edit_index_pattern/edit_index_pattern.js | 13 ++--- .../sections/index_patterns/index.js | 5 +- .../__jest__/objects_table.test.js | 4 +- .../components/flyout/__jest__/flyout.test.js | 4 +- src/legacy/core_plugins/management/index.ts | 37 ------------- .../core_plugins/management/package.json | 5 -- .../core_plugins/management/public/index.ts | 38 -------------- .../core_plugins/management/public/legacy.ts | 45 ---------------- .../new_platform/new_platform.karma_mock.js | 15 ++++++ .../ui/public/new_platform/new_platform.ts | 6 +++ .../index_pattern_management/kibana.json | 7 +++ .../index_pattern_management/public}/index.ts | 11 ++-- .../index_pattern_management/public}/mocks.ts | 52 +++++++++---------- .../public}/plugin.ts | 41 +++++++-------- .../public/service}/creation/config.ts | 8 +-- .../public/service}/creation/index.ts | 0 .../public/service}/creation/manager.ts | 21 ++++++-- .../public/service}/index.ts | 0 .../index_pattern_management_service.ts | 51 ++++++++---------- .../public/service}/list/config.ts | 9 ++-- .../public/service}/list/index.ts | 0 .../public/service}/list/manager.ts | 18 +++++-- x-pack/legacy/plugins/rollup/kibana.json | 3 +- .../rollup_index_pattern_creation_config.js | 2 +- .../rollup_index_pattern_list_config.js | 2 +- x-pack/legacy/plugins/rollup/public/legacy.ts | 8 +-- x-pack/legacy/plugins/rollup/public/plugin.ts | 17 ++---- .../components/copy_to_space_flyout.test.tsx | 6 --- .../components/copy_to_space_flyout.tsx | 2 +- .../copy_to_space_flyout_footer.tsx | 2 +- .../components/processing_copy_to_space.tsx | 2 +- .../summarize_copy_result.test.ts | 2 +- .../summarize_copy_result.ts | 2 +- .../translations/translations/ja-JP.json | 10 ++-- .../translations/translations/zh-CN.json | 10 ++-- 44 files changed, 183 insertions(+), 296 deletions(-) rename src/legacy/core_plugins/{management/public/np_ready/services => kibana/public}/index.ts (86%) delete mode 100644 src/legacy/core_plugins/management/index.ts delete mode 100644 src/legacy/core_plugins/management/package.json delete mode 100644 src/legacy/core_plugins/management/public/index.ts delete mode 100644 src/legacy/core_plugins/management/public/legacy.ts create mode 100644 src/plugins/index_pattern_management/kibana.json rename src/{legacy/core_plugins/management/public/np_ready => plugins/index_pattern_management/public}/index.ts (83%) rename src/{legacy/core_plugins/management/public/np_ready => plugins/index_pattern_management/public}/mocks.ts (57%) rename src/{legacy/core_plugins/management/public/np_ready => plugins/index_pattern_management/public}/plugin.ts (60%) rename src/{legacy/core_plugins/management/public/np_ready/services/index_pattern_management => plugins/index_pattern_management/public/service}/creation/config.ts (88%) rename src/{legacy/core_plugins/management/public/np_ready/services/index_pattern_management => plugins/index_pattern_management/public/service}/creation/index.ts (100%) rename src/{legacy/core_plugins/management/public/np_ready/services/index_pattern_management => plugins/index_pattern_management/public/service}/creation/manager.ts (79%) rename src/{legacy/core_plugins/management/public/np_ready/services/index_pattern_management => plugins/index_pattern_management/public/service}/index.ts (100%) rename src/{legacy/core_plugins/management/public/np_ready/services/index_pattern_management => plugins/index_pattern_management/public/service}/index_pattern_management_service.ts (51%) rename src/{legacy/core_plugins/management/public/np_ready/services/index_pattern_management => plugins/index_pattern_management/public/service}/list/config.ts (87%) rename src/{legacy/core_plugins/management/public/np_ready/services/index_pattern_management => plugins/index_pattern_management/public/service}/list/index.ts (100%) rename src/{legacy/core_plugins/management/public/np_ready/services/index_pattern_management => plugins/index_pattern_management/public/service}/list/manager.ts (75%) diff --git a/.i18nrc.json b/.i18nrc.json index 3b0b40b40792e..19d361aed9344 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -24,6 +24,7 @@ "src/legacy/core_plugins/management", "src/plugins/management" ], + "indexPatternManagement": "src/plugins/index_pattern_management", "advancedSettings": "src/plugins/advanced_settings", "kibana_legacy": "src/plugins/kibana_legacy", "kibana_react": "src/legacy/core_plugins/kibana_react", diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index.ts b/src/legacy/core_plugins/kibana/public/index.ts similarity index 86% rename from src/legacy/core_plugins/management/public/np_ready/services/index.ts rename to src/legacy/core_plugins/kibana/public/index.ts index 9df010223542b..a4fffc6eec26d 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index.ts +++ b/src/legacy/core_plugins/kibana/public/index.ts @@ -17,4 +17,7 @@ * under the License. */ -export * from './index_pattern_management'; +export { + ProcessedImportResponse, + processImportResponse, +} from './management/sections/objects/lib/process_import_response'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx index 25bd36829b6d0..40471b95d774c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { StepIndexPattern } from '../step_index_pattern'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; import { Header } from './components/header'; -import { IndexPatternCreationConfig } from '../../../../../../../../management/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../../../plugins/index_pattern_management/public'; import { coreMock } from '../../../../../../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../../../../../../../plugins/data/public/mocks'; import { SavedObjectsFindResponsePublic } from '../../../../../../../../../../core/public'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index bbb6bf26e5b31..648bf7f8f9738 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -39,7 +39,7 @@ import { LoadingIndices } from './components/loading_indices'; import { StatusMessage } from './components/status_message'; import { IndicesList } from './components/indices_list'; import { Header } from './components/header'; -import { IndexPatternCreationConfig } from '../../../../../../../../management/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../../../plugins/index_pattern_management/public'; import { MatchedIndex } from '../../types'; interface StepIndexPatternProps { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.test.tsx index e0c43105cb320..b23b1e3ad9051 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.test.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { IndexPatternCreationConfig } from '../../../../../../../../management/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../../../plugins/index_pattern_management/public'; import { IFieldType } from '../../../../../../../../../../plugins/data/public'; import { StepTimeField } from '../step_time_field'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx index 80582cc1fbd92..a58bf10c9dab8 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx @@ -34,7 +34,7 @@ import { Header } from './components/header'; import { TimeField } from './components/time_field'; import { AdvancedOptions } from './components/advanced_options'; import { ActionButtons } from './components/action_buttons'; -import { IndexPatternCreationConfig } from '../../../../../../../../management/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../../../plugins/index_pattern_management/public'; import { DataPublicPluginStart } from '../../../../../../../../../../plugins/data/public'; interface StepTimeFieldProps { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js index 50c5a58d35db3..47cb773258cb4 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js @@ -20,7 +20,6 @@ import uiRoutes from 'ui/routes'; import angularTemplate from './angular_template.html'; import { npStart } from 'ui/new_platform'; -import { setup as managementSetup } from '../../../../../../management/public/legacy'; import { getCreateBreadcrumbs } from '../breadcrumbs'; import { renderCreateIndexPatternWizard, destroyCreateIndexPatternWizard } from './render'; @@ -33,7 +32,7 @@ uiRoutes.when('/management/kibana/index_pattern', { const kbnUrl = $injector.get('kbnUrl'); $scope.$$postDigest(() => { const $routeParams = $injector.get('$routeParams'); - const indexPatternCreationType = managementSetup.indexPattern.creation.getType( + const indexPatternCreationType = npStart.plugins.indexPatternManagement.creation.getType( $routeParams.type ); const services = { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts index 5a8460fcb51ba..40583af7177fe 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts @@ -18,7 +18,7 @@ */ import { getIndices } from './get_indices'; -import { IndexPatternCreationConfig } from './../../../../../../../management/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../../plugins/index_pattern_management/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LegacyApiCaller } from '../../../../../../../../../plugins/data/public/search'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.ts index 3848c425e2d49..3b1b7a3b52a5b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.ts @@ -18,7 +18,7 @@ */ import { get, sortBy } from 'lodash'; -import { IndexPatternCreationConfig } from '../../../../../../../management/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../../plugins/index_pattern_management/public'; import { DataPublicPluginStart } from '../../../../../../../../../plugins/data/public'; import { MatchedIndex } from '../types'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js index 6d302ac5a74f3..594430ca01f4c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js @@ -29,7 +29,6 @@ import { uiModules } from 'ui/modules'; import template from './edit_index_pattern.html'; import { fieldWildcardMatcher } from '../../../../../../../../plugins/kibana_utils/public'; import { subscribeWithScope } from '../../../../../../../../plugins/kibana_legacy/public'; -import { setup as managementSetup } from '../../../../../../management/public/legacy'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { SourceFiltersTable } from './source_filters_table'; @@ -239,14 +238,12 @@ uiModules $scope.editSectionsProvider = Private(IndicesEditSectionsProvider); $scope.kbnUrl = Private(KbnUrlProvider); $scope.indexPattern = $route.current.locals.indexPattern; - $scope.indexPatternListProvider = managementSetup.indexPattern.list; - $scope.indexPattern.tags = managementSetup.indexPattern.list.getIndexPatternTags( + $scope.indexPatternListProvider = npStart.plugins.indexPatternManagement.list; + $scope.indexPattern.tags = npStart.plugins.indexPatternManagement.list.getIndexPatternTags( $scope.indexPattern, $scope.indexPattern.id === config.get('defaultIndex') ); - $scope.getFieldInfo = managementSetup.indexPattern.list.getFieldInfo.bind( - managementSetup.indexPattern.list - ); + $scope.getFieldInfo = npStart.plugins.indexPatternManagement.list.getFieldInfo; docTitle.change($scope.indexPattern.title); const otherPatterns = _.filter($route.current.locals.indexPatterns, pattern => { @@ -257,7 +254,7 @@ uiModules $scope.editSections = $scope.editSectionsProvider( $scope.indexPattern, $scope.fieldFilter, - managementSetup.indexPattern.list + npStart.plugins.indexPatternManagement.list ); $scope.refreshFilters(); $scope.fields = $scope.indexPattern.getNonScriptedFields(); @@ -363,7 +360,7 @@ uiModules $scope.editSections = $scope.editSectionsProvider( $scope.indexPattern, $scope.fieldFilter, - managementSetup.indexPattern.list + npStart.plugins.indexPatternManagement.list ); if ($scope.fieldFilter === undefined) { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js index 310797a7f3a0c..a8376c0e84bf9 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js @@ -18,7 +18,6 @@ */ import { management } from 'ui/management'; -import { setup as managementSetup } from '../../../../../management/public/legacy'; import './create_index_pattern_wizard'; import './edit_index_pattern'; import uiRoutes from 'ui/routes'; @@ -111,7 +110,7 @@ uiModules transclude: true, template: indexTemplate, link: async function($scope) { - const indexPatternCreationOptions = await managementSetup.indexPattern.creation.getIndexPatternCreationOptions( + const indexPatternCreationOptions = await npStart.plugins.indexPatternManagement.creation.getIndexPatternCreationOptions( url => { $scope.$evalAsync(() => kbnUrl.change(url)); } @@ -124,7 +123,7 @@ uiModules const id = pattern.id; const title = pattern.get('title'); const isDefault = $scope.defaultIndex === id; - const tags = managementSetup.indexPattern.list.getIndexPatternTags( + const tags = npStart.plugins.indexPatternManagement.list.getIndexPatternTags( pattern, isDefault ); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js index a5e34f8955fe3..7b9c17640a0f3 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js @@ -19,7 +19,7 @@ import React from 'react'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { mockManagementPlugin } from '../../../../../../../../management/public/np_ready/mocks'; +import { mockManagementPlugin } from '../../../../../../../../../../plugins/index_pattern_management/public/mocks'; import { Query } from '@elastic/eui'; import { ObjectsTable, POSSIBLE_TYPES } from '../objects_table'; @@ -30,7 +30,7 @@ import { extractExportDetails } from '../../../lib/extract_export_details'; jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); -jest.mock('../../../../../../../../management/public/legacy', () => ({ +jest.mock('../../../../../../../../../../plugins/index_pattern_management/public', () => ({ setup: mockManagementPlugin.createSetupContract(), start: mockManagementPlugin.createStartContract(), })); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js index 97c0d5b89d657..5d14c4609b918 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js @@ -19,7 +19,7 @@ import React from 'react'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { mockManagementPlugin } from '../../../../../../../../../../management/public/np_ready/mocks'; +import { mockManagementPlugin } from '../../../../../../../../../../../../plugins/index_pattern_management/public/mocks'; import { Flyout } from '../flyout'; jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); @@ -48,7 +48,7 @@ jest.mock('../../../../../lib/resolve_saved_objects', () => ({ saveObjects: jest.fn(), })); -jest.mock('../../../../../../../../../../management/public/legacy', () => ({ +jest.mock('../../../../../../../../../../../../plugins/index_pattern_management/public', () => ({ setup: mockManagementPlugin.createSetupContract(), start: mockManagementPlugin.createStartContract(), })); diff --git a/src/legacy/core_plugins/management/index.ts b/src/legacy/core_plugins/management/index.ts deleted file mode 100644 index 4962c948f842f..0000000000000 --- a/src/legacy/core_plugins/management/index.ts +++ /dev/null @@ -1,37 +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'; - -// eslint-disable-next-line import/no-default-export -export default function ManagementPlugin(kibana: any) { - const config: Legacy.PluginSpecOptions = { - id: 'stack-management', - publicDir: resolve(__dirname, 'public'), - config: (Joi: any) => { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - init: (server: Legacy.Server) => ({}), - }; - - return new kibana.Plugin(config); -} diff --git a/src/legacy/core_plugins/management/package.json b/src/legacy/core_plugins/management/package.json deleted file mode 100644 index 77d33a7bce3b6..0000000000000 --- a/src/legacy/core_plugins/management/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "management", - "version": "kibana" -} - \ No newline at end of file diff --git a/src/legacy/core_plugins/management/public/index.ts b/src/legacy/core_plugins/management/public/index.ts deleted file mode 100644 index bc3737524e125..0000000000000 --- a/src/legacy/core_plugins/management/public/index.ts +++ /dev/null @@ -1,38 +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. - */ - -/** - * Static np-ready code, re-exported here so consumers can import from - * `src/legacy/core_plugins/management/public` - * - * @public - */ - -export { - ManagementSetup, - ManagementStart, - plugin, - IndexPatternCreationConfig, - IndexPatternListConfig, -} from './np_ready'; - -export { - processImportResponse, - ProcessedImportResponse, -} from '../../kibana/public/management/sections/objects/lib/process_import_response'; diff --git a/src/legacy/core_plugins/management/public/legacy.ts b/src/legacy/core_plugins/management/public/legacy.ts deleted file mode 100644 index 96d2c74398a0e..0000000000000 --- a/src/legacy/core_plugins/management/public/legacy.ts +++ /dev/null @@ -1,45 +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. - */ - -/** - * New Platform Shim - * - * In this file, we import any legacy dependencies we have, and shim them into - * our plugin by manually constructing the values that the new platform will - * eventually be passing to the `setup/start` method of our plugin definition. - * - * The idea is that our `plugin.ts` can stay "pure" and not contain any legacy - * world code. Then when it comes time to migrate to the new platform, we can - * simply delete this shim file. - * - * We are also calling `setup/start` here and exporting our public contract so that - * other legacy plugins are able to import from '../core_plugins/management/legacy' - * and receive the response value of the `setup/start` contract, mimicking the - * data that will eventually be injected by the new platform. - */ - -import { PluginInitializerContext } from 'src/core/public'; -import { npSetup, npStart } from 'ui/new_platform'; - -import { plugin } from '.'; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, { home: npSetup.plugins.home }); -export const start = pluginInstance.start(npStart.core, {}); diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index f70ef069dd134..0779d6472671c 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -290,6 +290,10 @@ export const npSetup = { }), }, }, + indexPatternManagement: { + list: { addListConfig: sinon.fake() }, + creation: { addCreationConfig: sinon.fake() }, + }, discover: { docViews: { addDocView: sinon.fake(), @@ -325,6 +329,17 @@ export const npStart = { }), }, }, + indexPatternManagement: { + list: { + getType: sinon.fake(), + getIndexPatternCreationOptions: sinon.fake(), + }, + creation: { + getIndexPatternTags: sinon.fake(), + getFieldInfo: sinon.fake(), + areScriptedFieldsEnabled: sinon.fake(), + }, + }, embeddable: { getEmbeddableFactory: sinon.fake(), getEmbeddableFactories: sinon.fake(), diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index b4b5099081759..cdd7e1a994912 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -47,6 +47,10 @@ import { AdvancedSettingsStart, } from '../../../../plugins/advanced_settings/public'; import { ManagementSetup, ManagementStart } from '../../../../plugins/management/public'; +import { + IndexPatternManagementSetup, + IndexPatternManagementStart, +} from '../../../../plugins/index_pattern_management/public'; import { BfetchPublicSetup, BfetchPublicStart } from '../../../../plugins/bfetch/public'; import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; import { TelemetryPluginSetup, TelemetryPluginStart } from '../../../../plugins/telemetry/public'; @@ -86,6 +90,7 @@ export interface PluginsSetup { visualizations: VisualizationsSetup; telemetry?: TelemetryPluginSetup; savedObjectsManagement: SavedObjectsManagementPluginSetup; + indexPatternManagement: IndexPatternManagementSetup; } export interface PluginsStart { @@ -107,6 +112,7 @@ export interface PluginsStart { telemetry?: TelemetryPluginStart; dashboard: DashboardStart; savedObjectsManagement: SavedObjectsManagementPluginStart; + indexPatternManagement: IndexPatternManagementStart; } export const npSetup = { diff --git a/src/plugins/index_pattern_management/kibana.json b/src/plugins/index_pattern_management/kibana.json new file mode 100644 index 0000000000000..d5397a11184aa --- /dev/null +++ b/src/plugins/index_pattern_management/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "indexPatternManagement", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": [] +} diff --git a/src/legacy/core_plugins/management/public/np_ready/index.ts b/src/plugins/index_pattern_management/public/index.ts similarity index 83% rename from src/legacy/core_plugins/management/public/np_ready/index.ts rename to src/plugins/index_pattern_management/public/index.ts index bae0f1d3e23cd..da482c0c51f0a 100644 --- a/src/legacy/core_plugins/management/public/np_ready/index.ts +++ b/src/plugins/index_pattern_management/public/index.ts @@ -29,14 +29,11 @@ * either types, or static code. */ import { PluginInitializerContext } from 'src/core/public'; -import { ManagementPlugin } from './plugin'; -export { ManagementSetup, ManagementStart } from './plugin'; +import { IndexPatternManagementPlugin } from './plugin'; +export { IndexPatternManagementSetup, IndexPatternManagementStart } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { - return new ManagementPlugin(initializerContext); + return new IndexPatternManagementPlugin(initializerContext); } -export { - IndexPatternCreationConfig, - IndexPatternListConfig, -} from './services/index_pattern_management'; +export { IndexPatternCreationConfig, IndexPatternListConfig } from './service'; diff --git a/src/legacy/core_plugins/management/public/np_ready/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts similarity index 57% rename from src/legacy/core_plugins/management/public/np_ready/mocks.ts rename to src/plugins/index_pattern_management/public/mocks.ts index ae0be98de63f3..bc97f46c302e3 100644 --- a/src/legacy/core_plugins/management/public/np_ready/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -18,42 +18,38 @@ */ import { PluginInitializerContext } from 'src/core/public'; -import { coreMock } from '../../../../../core/public/mocks'; +import { coreMock } from '../../../core/public/mocks'; import { - ManagementSetup, - ManagementStart, - ManagementPlugin, - ManagementPluginSetupDependencies, + IndexPatternManagementSetup, + IndexPatternManagementStart, + IndexPatternManagementPlugin, } from './plugin'; -const createSetupContract = (): ManagementSetup => ({ - indexPattern: { - creation: { - add: jest.fn(), - getType: jest.fn(), - getIndexPatternCreationOptions: jest.fn(), - } as any, - list: { - add: jest.fn(), - getIndexPatternTags: jest.fn(), - getFieldInfo: jest.fn(), - areScriptedFieldsEnabled: jest.fn(), - } as any, - }, +const createSetupContract = (): IndexPatternManagementSetup => ({ + creation: { + addCreationConfig: jest.fn(), + } as any, + list: { + addListConfig: jest.fn(), + } as any, }); -const createStartContract = (): ManagementStart => ({}); +const createStartContract = (): IndexPatternManagementStart => ({ + creation: { + getType: jest.fn(), + getIndexPatternCreationOptions: jest.fn(), + } as any, + list: { + getIndexPatternTags: jest.fn(), + getFieldInfo: jest.fn(), + areScriptedFieldsEnabled: jest.fn(), + } as any, +}); const createInstance = async () => { - const plugin = new ManagementPlugin({} as PluginInitializerContext); + const plugin = new IndexPatternManagementPlugin({} as PluginInitializerContext); - const setup = plugin.setup(coreMock.createSetup(), ({ - home: { - featureCatalogue: { - register: jest.fn(), - }, - }, - } as unknown) as ManagementPluginSetupDependencies); + const setup = plugin.setup(coreMock.createSetup()); const doStart = () => plugin.start(coreMock.createStart(), {}); return { diff --git a/src/legacy/core_plugins/management/public/np_ready/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts similarity index 60% rename from src/legacy/core_plugins/management/public/np_ready/plugin.ts rename to src/plugins/index_pattern_management/public/plugin.ts index 2a8ef10c817cc..93bb0ead1df4a 100644 --- a/src/legacy/core_plugins/management/public/np_ready/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -17,43 +17,40 @@ * under the License. */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { HomePublicPluginSetup } from 'src/plugins/home/public'; -import { IndexPatternManagementService, IndexPatternManagementSetup } from './services'; +import { + IndexPatternManagementService, + IndexPatternManagementServiceSetup, + IndexPatternManagementServiceStart, +} from './service'; -export interface ManagementPluginSetupDependencies { - home: HomePublicPluginSetup; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface IndexPatternManagementSetupDependencies {} // eslint-disable-next-line @typescript-eslint/no-empty-interface -interface ManagementPluginStartDependencies {} +export interface IndexPatternManagementStartDependencies {} -export interface ManagementSetup { - indexPattern: IndexPatternManagementSetup; -} +export type IndexPatternManagementSetup = IndexPatternManagementServiceSetup; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ManagementStart {} +export type IndexPatternManagementStart = IndexPatternManagementServiceStart; -export class ManagementPlugin +export class IndexPatternManagementPlugin implements Plugin< - ManagementSetup, - ManagementStart, - ManagementPluginSetupDependencies, - ManagementPluginStartDependencies + IndexPatternManagementSetup, + IndexPatternManagementStart, + IndexPatternManagementSetupDependencies, + IndexPatternManagementStartDependencies > { private readonly indexPattern = new IndexPatternManagementService(); constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { home }: ManagementPluginSetupDependencies) { - return { - indexPattern: this.indexPattern.setup({ httpClient: core.http, home }), - }; + public setup(core: CoreSetup) { + return this.indexPattern.setup({ httpClient: core.http }); } - public start(core: CoreStart, plugins: ManagementPluginStartDependencies) { - return {}; + public start(core: CoreStart, plugins: IndexPatternManagementStartDependencies) { + return this.indexPattern.start(); } public stop() { diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/config.ts b/src/plugins/index_pattern_management/public/service/creation/config.ts similarity index 88% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/config.ts rename to src/plugins/index_pattern_management/public/service/creation/config.ts index 5714fa3338962..29ab0ebfc3d5f 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/config.ts +++ b/src/plugins/index_pattern_management/public/service/creation/config.ts @@ -18,20 +18,20 @@ */ import { i18n } from '@kbn/i18n'; -import { MatchedIndex } from '../../../../../../kibana/public/management/sections/index_patterns/create_index_pattern_wizard/types'; +import { MatchedIndex } from '../../../../../legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/types'; const indexPatternTypeName = i18n.translate( - 'management.editIndexPattern.createIndex.defaultTypeName', + 'indexPatternManagement.editIndexPattern.createIndex.defaultTypeName', { defaultMessage: 'index pattern' } ); const indexPatternButtonText = i18n.translate( - 'management.editIndexPattern.createIndex.defaultButtonText', + 'indexPatternManagement.editIndexPattern.createIndex.defaultButtonText', { defaultMessage: 'Standard index pattern' } ); const indexPatternButtonDescription = i18n.translate( - 'management.editIndexPattern.createIndex.defaultButtonDescription', + 'indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription', { defaultMessage: 'Perform full aggregations against any data' } ); diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/index.ts b/src/plugins/index_pattern_management/public/service/creation/index.ts similarity index 100% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/index.ts rename to src/plugins/index_pattern_management/public/service/creation/index.ts diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/manager.ts b/src/plugins/index_pattern_management/public/service/creation/manager.ts similarity index 79% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/manager.ts rename to src/plugins/index_pattern_management/public/service/creation/manager.ts index e7fa13409ab04..32b3e7ee7a133 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/manager.ts +++ b/src/plugins/index_pattern_management/public/service/creation/manager.ts @@ -17,23 +17,25 @@ * under the License. */ -import { HttpSetup } from '../../../../../../../../core/public'; +import { HttpSetup } from '../../../../../core/public'; import { IndexPatternCreationConfig, UrlHandler, IndexPatternCreationOption } from './config'; export class IndexPatternCreationManager { private configs: IndexPatternCreationConfig[]; - constructor(private readonly httpClient: HttpSetup) { + constructor() { this.configs = []; } - public add(Config: typeof IndexPatternCreationConfig) { - const config = new Config({ httpClient: this.httpClient }); + public addCreationConfig = (httpClient: HttpSetup) => ( + Config: typeof IndexPatternCreationConfig + ) => { + const config = new Config({ httpClient }); if (this.configs.findIndex(c => c.key === config.key) !== -1) { throw new Error(`${config.key} exists in IndexPatternCreationManager.`); } this.configs.push(config); - } + }; public getType(key: string | undefined): IndexPatternCreationConfig | null { if (key) { @@ -58,4 +60,13 @@ export class IndexPatternCreationManager { ); return options; } + + setup = (httpClient: HttpSetup) => ({ + addCreationConfig: this.addCreationConfig(httpClient).bind(this), + }); + + start = () => ({ + getType: this.getType.bind(this), + getIndexPatternCreationOptions: this.getIndexPatternCreationOptions.bind(this), + }); } diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index.ts b/src/plugins/index_pattern_management/public/service/index.ts similarity index 100% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index.ts rename to src/plugins/index_pattern_management/public/service/index.ts diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts similarity index 51% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts rename to src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts index 2b6f008dd928a..4780fa00ed468 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts +++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts @@ -17,18 +17,12 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -import { - FeatureCatalogueCategory, - HomePublicPluginSetup, -} from '../../../../../../../plugins/home/public'; -import { HttpSetup } from '../../../../../../../core/public'; +import { HttpSetup } from '../../../../core/public'; import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; import { IndexPatternListManager, IndexPatternListConfig } from './list'; interface SetupDependencies { httpClient: HttpSetup; - home: HomePublicPluginSetup; } /** @@ -37,31 +31,29 @@ interface SetupDependencies { * @internal */ export class IndexPatternManagementService { - public setup({ httpClient, home }: SetupDependencies) { - const creation = new IndexPatternCreationManager(httpClient); - const list = new IndexPatternListManager(); + indexPatternCreationManager: IndexPatternCreationManager; + indexPatternListConfig: IndexPatternListManager; - creation.add(IndexPatternCreationConfig); - list.add(IndexPatternListConfig); + constructor() { + this.indexPatternCreationManager = new IndexPatternCreationManager(); + this.indexPatternListConfig = new IndexPatternListManager(); + } + + public setup({ httpClient }: SetupDependencies) { + const creationManagerSetup = this.indexPatternCreationManager.setup(httpClient); + creationManagerSetup.addCreationConfig(IndexPatternCreationConfig); + this.indexPatternListConfig.setup().addListConfig(IndexPatternListConfig); - home.featureCatalogue.register({ - id: 'index_patterns', - title: i18n.translate('management.indexPatternHeader', { - defaultMessage: 'Index Patterns', - }), - description: i18n.translate('management.indexPatternLabel', { - defaultMessage: - 'Manage the index patterns that help retrieve your data from Elasticsearch.', - }), - icon: 'indexPatternApp', - path: '/app/kibana#/management/kibana/index_patterns', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }); + return { + creation: creationManagerSetup, + list: this.indexPatternListConfig.setup(), + }; + } + public start() { return { - creation, - list, + creation: this.indexPatternCreationManager.start(), + list: this.indexPatternListConfig.start(), }; } @@ -71,4 +63,5 @@ export class IndexPatternManagementService { } /** @internal */ -export type IndexPatternManagementSetup = ReturnType; +export type IndexPatternManagementServiceSetup = ReturnType; +export type IndexPatternManagementServiceStart = ReturnType; diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/config.ts b/src/plugins/index_pattern_management/public/service/list/config.ts similarity index 87% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/config.ts rename to src/plugins/index_pattern_management/public/service/list/config.ts index dd4d77a681171..87c246e8913e5 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/config.ts +++ b/src/plugins/index_pattern_management/public/service/list/config.ts @@ -33,9 +33,12 @@ export class IndexPatternListConfig { ? [ { key: 'default', - name: i18n.translate('management.editIndexPattern.list.defaultIndexPatternListName', { - defaultMessage: 'Default', - }), + name: i18n.translate( + 'indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName', + { + defaultMessage: 'Default', + } + ), }, ] : []; diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/index.ts b/src/plugins/index_pattern_management/public/service/list/index.ts similarity index 100% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/index.ts rename to src/plugins/index_pattern_management/public/service/list/index.ts diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/manager.ts b/src/plugins/index_pattern_management/public/service/list/manager.ts similarity index 75% rename from src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/manager.ts rename to src/plugins/index_pattern_management/public/service/list/manager.ts index 73ca33ae914a9..3a2910a222cd7 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/list/manager.ts +++ b/src/plugins/index_pattern_management/public/service/list/manager.ts @@ -27,7 +27,7 @@ export class IndexPatternListManager { this.configs = []; } - public add(Config: typeof IndexPatternListConfig) { + private addListConfig(Config: typeof IndexPatternListConfig) { const config = new Config(); if (this.configs.findIndex(c => c.key === config.key) !== -1) { throw new Error(`${config.key} exists in IndexPatternListManager.`); @@ -35,7 +35,7 @@ export class IndexPatternListManager { this.configs.push(config); } - public getIndexPatternTags(indexPattern: IIndexPattern, isDefault: boolean) { + private getIndexPatternTags(indexPattern: IIndexPattern, isDefault: boolean) { return this.configs.reduce((tags: IndexPatternTag[], config) => { return config.getIndexPatternTags ? tags.concat(config.getIndexPatternTags(indexPattern, isDefault)) @@ -43,15 +43,25 @@ export class IndexPatternListManager { }, []); } - public getFieldInfo(indexPattern: IIndexPattern, field: IFieldType): string[] { + private getFieldInfo(indexPattern: IIndexPattern, field: IFieldType): string[] { return this.configs.reduce((info: string[], config) => { return config.getFieldInfo ? info.concat(config.getFieldInfo(indexPattern, field)) : info; }, []); } - public areScriptedFieldsEnabled(indexPattern: IIndexPattern): boolean { + private areScriptedFieldsEnabled(indexPattern: IIndexPattern): boolean { return this.configs.every(config => { return config.areScriptedFieldsEnabled ? config.areScriptedFieldsEnabled(indexPattern) : true; }); } + + setup = () => ({ + addListConfig: this.addListConfig.bind(this), + }); + + start = () => ({ + getIndexPatternTags: this.getIndexPatternTags.bind(this), + getFieldInfo: this.getFieldInfo.bind(this), + areScriptedFieldsEnabled: this.areScriptedFieldsEnabled.bind(this), + }); } diff --git a/x-pack/legacy/plugins/rollup/kibana.json b/x-pack/legacy/plugins/rollup/kibana.json index 3781d59d8c0f3..3df8bd7c187d5 100644 --- a/x-pack/legacy/plugins/rollup/kibana.json +++ b/x-pack/legacy/plugins/rollup/kibana.json @@ -4,7 +4,8 @@ "requiredPlugins": [ "home", "index_management", - "metrics" + "metrics", + "indexPatternManagement" ], "optionalPlugins": [ "usageCollection" diff --git a/x-pack/legacy/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js b/x-pack/legacy/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js index f0eb21a219442..f4de2a3098127 100644 --- a/x-pack/legacy/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js +++ b/x-pack/legacy/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { RollupPrompt } from './components/rollup_prompt'; -import { IndexPatternCreationConfig } from '../../../../../../src/legacy/core_plugins/management/public'; +import { IndexPatternCreationConfig } from '../../../../../../src/plugins/index_pattern_management/public'; const rollupIndexPatternTypeName = i18n.translate( 'xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultTypeName', diff --git a/x-pack/legacy/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js b/x-pack/legacy/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js index fbf2612b83aa8..809a76d1868b2 100644 --- a/x-pack/legacy/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js +++ b/x-pack/legacy/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { IndexPatternListConfig } from '../../../../../../src/legacy/core_plugins/management/public'; +import { IndexPatternListConfig } from '../../../../../../src/plugins/index_pattern_management/public'; function isRollup(indexPattern) { return ( diff --git a/x-pack/legacy/plugins/rollup/public/legacy.ts b/x-pack/legacy/plugins/rollup/public/legacy.ts index ec530e63408f4..83945110c2c76 100644 --- a/x-pack/legacy/plugins/rollup/public/legacy.ts +++ b/x-pack/legacy/plugins/rollup/public/legacy.ts @@ -6,14 +6,8 @@ import { npSetup, npStart } from 'ui/new_platform'; import { RollupPlugin } from './plugin'; -import { setup as management } from '../../../../../src/legacy/core_plugins/management/public/legacy'; const plugin = new RollupPlugin(); -export const setup = plugin.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - managementLegacy: management, - }, -}); +export const setup = plugin.setup(npSetup.core, npSetup.plugins); export const start = plugin.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/rollup/public/plugin.ts b/x-pack/legacy/plugins/rollup/public/plugin.ts index c58975419e20f..5782e88c3448b 100644 --- a/x-pack/legacy/plugins/rollup/public/plugin.ts +++ b/x-pack/legacy/plugins/rollup/public/plugin.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { PluginsStart } from './legacy_imports'; -import { ManagementSetup as ManagementSetupLegacy } from '../../../../../src/legacy/core_plugins/management/public/np_ready'; import { rollupBadgeExtension, rollupToggleExtension } from './extend_index_management'; // @ts-ignore import { RollupIndexPatternCreationConfig } from './index_pattern_creation/rollup_index_pattern_creation_config'; @@ -26,6 +25,7 @@ import { import { CRUD_APP_BASE_PATH } from './crud_app/constants'; import { ManagementSetup } from '../../../../../src/plugins/management/public'; import { IndexMgmtSetup } from '../../../../plugins/index_management/public'; +import { IndexPatternManagementSetup } from '../../../../../src/plugins/index_pattern_management/public'; import { search } from '../../../../../src/plugins/data/public'; // @ts-ignore import { setEsBaseAndXPackBase, setHttp } from './crud_app/services'; @@ -33,23 +33,16 @@ import { setNotifications, setFatalErrors } from './kibana_services'; import { renderApp } from './application'; export interface RollupPluginSetupDependencies { - __LEGACY: { - managementLegacy: ManagementSetupLegacy; - }; home?: HomePublicPluginSetup; management: ManagementSetup; indexManagement?: IndexMgmtSetup; + indexPatternManagement: IndexPatternManagementSetup; } export class RollupPlugin implements Plugin { setup( core: CoreSetup, - { - __LEGACY: { managementLegacy }, - home, - management, - indexManagement, - }: RollupPluginSetupDependencies + { home, management, indexManagement, indexPatternManagement }: RollupPluginSetupDependencies ) { setFatalErrors(core.fatalErrors); @@ -61,8 +54,8 @@ export class RollupPlugin implements Plugin { const isRollupIndexPatternsEnabled = core.uiSettings.get(CONFIG_ROLLUPS); if (isRollupIndexPatternsEnabled) { - managementLegacy.indexPattern.creation.add(RollupIndexPatternCreationConfig); - managementLegacy.indexPattern.list.add(RollupIndexPatternListConfig); + indexPatternManagement.creation.addCreationConfig(RollupIndexPatternCreationConfig); + indexPatternManagement.list.addListConfig(RollupIndexPatternListConfig); } if (home) { diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx index 7809b511adda4..99b4e184c071a 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx @@ -6,7 +6,6 @@ import React from 'react'; import Boom from 'boom'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { mockManagementPlugin } from '../../../../../../src/legacy/core_plugins/management/public/np_ready/mocks'; import { CopySavedObjectsToSpaceFlyout } from './copy_to_space_flyout'; import { CopyToSpaceForm } from './copy_to_space_form'; import { EuiLoadingSpinner, EuiEmptyPrompt } from '@elastic/eui'; @@ -19,11 +18,6 @@ import { spacesManagerMock } from '../../spaces_manager/mocks'; import { SpacesManager } from '../../spaces_manager'; import { ToastsApi } from 'src/core/public'; -jest.mock('../../../../../../src/legacy/core_plugins/management/public/legacy', () => ({ - setup: mockManagementPlugin.createSetupContract(), - start: mockManagementPlugin.createStartContract(), -})); - interface SetupOpts { mockSpaces?: Space[]; returnBeforeSpacesLoad?: boolean; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx index 4d92505c4aebb..fee41fc7e36d3 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx @@ -25,7 +25,7 @@ import { ToastsStart } from 'src/core/public'; import { ProcessedImportResponse, processImportResponse, -} from '../../../../../../src/legacy/core_plugins/management/public'; +} from '../../../../../../src/legacy/core_plugins/kibana/public'; import { SavedObjectsManagementRecord } from '../../../../../../src/plugins/saved_objects_management/public'; import { Space } from '../../../common/model/space'; import { SpacesManager } from '../../spaces_manager'; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx index b22cec0af5ea8..4f6ff55dbfbb2 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiStat, EuiHorizontalRule } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { ProcessedImportResponse } from '../../../../../../src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from '../../../../../../src/legacy/core_plugins/kibana/public'; import { ImportRetry } from '../types'; interface Props { diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx index 96cbac4b48065..ea74fc92b95ea 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx @@ -13,7 +13,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ProcessedImportResponse } from '../../../../../../src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from '../../../../../../src/legacy/core_plugins/kibana/public'; import { SavedObjectsManagementRecord } from '../../../../../../src/plugins/saved_objects_management/public'; import { Space } from '../../../common/model/space'; import { CopyOptions, ImportRetry } from '../types'; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts index fb2616619c644..65a0cabfeb716 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts @@ -5,7 +5,7 @@ */ import { summarizeCopyResult } from './summarize_copy_result'; -import { ProcessedImportResponse } from 'src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from 'src/legacy/core_plugins/kibana/public'; const createSavedObjectsManagementRecord = () => ({ type: 'dashboard', diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts index 96e642b0f45d8..44c9e9993bf10 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessedImportResponse } from 'src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from 'src/legacy/core_plugins/kibana/public'; import { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public'; export interface SummarizedSavedObjectResult { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d07026c0883b8..00ac5b77d00f3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2523,12 +2523,10 @@ "management.breadcrumb": "管理", "management.connectDataDisplayName": "データに接続", "management.displayName": "管理", - "management.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全集約を実行", - "management.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", - "management.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン", - "management.editIndexPattern.list.defaultIndexPatternListName": "デフォルト", - "management.indexPatternHeader": "インデックスパターン", - "management.indexPatternLabel": "Elasticsearch からのデータの取得に役立つインデックスパターンを管理します。", + "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全集約を実行", + "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", + "indexPatternManagement.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン", + "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "デフォルト", "management.nav.label": "管理", "management.nav.menu": "管理メニュー", "management.stackManagement.managementDescription": "Elastic Stack の管理を行うセンターコンソールです。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7e64ff6301faf..f6d84431bef7f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2524,12 +2524,10 @@ "management.breadcrumb": "管理", "management.connectDataDisplayName": "连接数据", "management.displayName": "管理", - "management.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", - "management.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", - "management.editIndexPattern.createIndex.defaultTypeName": "索引模式", - "management.editIndexPattern.list.defaultIndexPatternListName": "默认值", - "management.indexPatternHeader": "索引模式", - "management.indexPatternLabel": "管理帮助从 Elasticsearch 检索数据的索引模式。", + "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", + "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", + "indexPatternManagement.editIndexPattern.createIndex.defaultTypeName": "索引模式", + "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "默认值", "management.nav.label": "管理", "management.nav.menu": "管理菜单", "management.stackManagement.managementDescription": "您用于管理 Elastic Stack 的中心控制台。", From fdb4a37a6016c8afce52203ecccbe03e6fc4064e Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Wed, 8 Apr 2020 20:33:21 +0000 Subject: [PATCH 36/81] restore empty_kibana after saved objects test (#62951) --- test/functional/apps/management/_mgmt_import_saved_objects.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js index 53b7e7062ee2d..2f9d9f9bfb178 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.js @@ -35,6 +35,7 @@ export default function({ getService, getPageObjects }) { afterEach(async function() { await esArchiver.unload('discover'); + await esArchiver.load('empty_kibana'); }); it('should import saved objects mgmt', async function() { From 86a25876601053604d08ad4884db796066b819b2 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 8 Apr 2020 13:33:51 -0700 Subject: [PATCH 37/81] [Ingest] Data source configuration validation UI (#61180) * Initial pass at datasource configuration validation * Show error icon and red text at input and stream levels * Add tests, fix bugs in validation method * Fix typings --- .../components/datasource_input_config.tsx | 63 ++- .../components/datasource_input_panel.tsx | 50 +- .../datasource_input_stream_config.tsx | 55 +- .../components/datasource_input_var_field.tsx | 28 +- .../components/index.ts | 1 + .../create_datasource_page/index.tsx | 16 +- .../create_datasource_page/services/index.ts | 7 + .../services/validate_datasource.test.ts | 504 ++++++++++++++++++ .../services/validate_datasource.ts | 232 ++++++++ .../step_configure_datasource.tsx | 169 ++++-- .../ingest_manager/services/index.ts | 2 + .../ingest_manager/types/index.ts | 3 + 12 files changed, 1038 insertions(+), 92 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx index 356739af1ff9a..0e8763cb2d4c0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx @@ -6,26 +6,38 @@ import React, { useState, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiText, + EuiTextColor, EuiSpacer, EuiButtonEmpty, EuiTitle, + EuiIconTip, } from '@elastic/eui'; import { DatasourceInput, RegistryVarsEntry } from '../../../../types'; -import { isAdvancedVar } from '../services'; +import { isAdvancedVar, DatasourceConfigValidationResults, validationHasErrors } from '../services'; import { DatasourceInputVarField } from './datasource_input_var_field'; export const DatasourceInputConfig: React.FunctionComponent<{ packageInputVars?: RegistryVarsEntry[]; datasourceInput: DatasourceInput; updateDatasourceInput: (updatedInput: Partial) => void; -}> = ({ packageInputVars, datasourceInput, updateDatasourceInput }) => { + inputVarsValidationResults: DatasourceConfigValidationResults; + forceShowErrors?: boolean; +}> = ({ + packageInputVars, + datasourceInput, + updateDatasourceInput, + inputVarsValidationResults, + forceShowErrors, +}) => { // Showing advanced options toggle state const [isShowingAdvanced, setIsShowingAdvanced] = useState(false); + // Errors state + const hasErrors = forceShowErrors && validationHasErrors(inputVarsValidationResults); + const requiredVars: RegistryVarsEntry[] = []; const advancedVars: RegistryVarsEntry[] = []; @@ -40,15 +52,36 @@ export const DatasourceInputConfig: React.FunctionComponent<{ } return ( - - + + -

- -

+ + +

+ + + +

+
+ {hasErrors ? ( + + + } + position="right" + type="alert" + iconProps={{ color: 'danger' }} + /> + + ) : null} +
@@ -60,7 +93,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{

- + {requiredVars.map(varDef => { const { name: varName, type: varType } = varDef; @@ -81,6 +114,8 @@ export const DatasourceInputConfig: React.FunctionComponent<{ }, }); }} + errors={inputVarsValidationResults.config![varName]} + forceShowErrors={forceShowErrors} /> ); @@ -123,6 +158,8 @@ export const DatasourceInputConfig: React.FunctionComponent<{ }, }); }} + errors={inputVarsValidationResults.config![varName]} + forceShowErrors={forceShowErrors} /> ); @@ -132,6 +169,6 @@ export const DatasourceInputConfig: React.FunctionComponent<{ ) : null}
-
+ ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx index 74b08f48df12d..6b0c68ccb7d3f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx @@ -17,8 +17,10 @@ import { EuiButtonIcon, EuiHorizontalRule, EuiSpacer, + EuiIconTip, } from '@elastic/eui'; import { DatasourceInput, DatasourceInputStream, RegistryInput } from '../../../../types'; +import { DatasourceInputValidationResults, validationHasErrors } from '../services'; import { DatasourceInputConfig } from './datasource_input_config'; import { DatasourceInputStreamConfig } from './datasource_input_stream_config'; @@ -32,10 +34,21 @@ export const DatasourceInputPanel: React.FunctionComponent<{ packageInput: RegistryInput; datasourceInput: DatasourceInput; updateDatasourceInput: (updatedInput: Partial) => void; -}> = ({ packageInput, datasourceInput, updateDatasourceInput }) => { + inputValidationResults: DatasourceInputValidationResults; + forceShowErrors?: boolean; +}> = ({ + packageInput, + datasourceInput, + updateDatasourceInput, + inputValidationResults, + forceShowErrors, +}) => { // Showing streams toggle state const [isShowingStreams, setIsShowingStreams] = useState(false); + // Errors state + const hasErrors = forceShowErrors && validationHasErrors(inputValidationResults); + return ( {/* Header / input-level toggle */} @@ -43,9 +56,32 @@ export const DatasourceInputPanel: React.FunctionComponent<{ -

{packageInput.title || packageInput.type}

-
+ + + +

+ + {packageInput.title || packageInput.type} + +

+
+
+ {hasErrors ? ( + + + } + position="right" + type="alert" + iconProps={{ color: 'danger' }} + /> + + ) : null} +
} checked={datasourceInput.enabled} onChange={e => { @@ -122,6 +158,8 @@ export const DatasourceInputPanel: React.FunctionComponent<{ packageInputVars={packageInput.vars} datasourceInput={datasourceInput} updateDatasourceInput={updateDatasourceInput} + inputVarsValidationResults={{ config: inputValidationResults.config }} + forceShowErrors={forceShowErrors} /> @@ -165,6 +203,10 @@ export const DatasourceInputPanel: React.FunctionComponent<{ updateDatasourceInput(updatedInput); }} + inputStreamValidationResults={ + inputValidationResults.streams![datasourceInputStream.id] + } + forceShowErrors={forceShowErrors} /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx index 3bf5b2bb4c0f0..43e8f5a2c060d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx @@ -7,26 +7,38 @@ import React, { useState, Fragment } from 'react'; import ReactMarkdown from 'react-markdown'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiSwitch, EuiText, EuiSpacer, EuiButtonEmpty, + EuiTextColor, + EuiIconTip, } from '@elastic/eui'; import { DatasourceInputStream, RegistryStream, RegistryVarsEntry } from '../../../../types'; -import { isAdvancedVar } from '../services'; +import { isAdvancedVar, DatasourceConfigValidationResults, validationHasErrors } from '../services'; import { DatasourceInputVarField } from './datasource_input_var_field'; export const DatasourceInputStreamConfig: React.FunctionComponent<{ packageInputStream: RegistryStream; datasourceInputStream: DatasourceInputStream; updateDatasourceInputStream: (updatedStream: Partial) => void; -}> = ({ packageInputStream, datasourceInputStream, updateDatasourceInputStream }) => { + inputStreamValidationResults: DatasourceConfigValidationResults; + forceShowErrors?: boolean; +}> = ({ + packageInputStream, + datasourceInputStream, + updateDatasourceInputStream, + inputStreamValidationResults, + forceShowErrors, +}) => { // Showing advanced options toggle state const [isShowingAdvanced, setIsShowingAdvanced] = useState(false); + // Errors state + const hasErrors = forceShowErrors && validationHasErrors(inputStreamValidationResults); + const requiredVars: RegistryVarsEntry[] = []; const advancedVars: RegistryVarsEntry[] = []; @@ -41,10 +53,33 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ } return ( - - + + + + + {packageInputStream.title || packageInputStream.dataset} + + + {hasErrors ? ( + + + } + position="right" + type="alert" + iconProps={{ color: 'danger' }} + /> + + ) : null} + + } checked={datasourceInputStream.enabled} onChange={e => { const enabled = e.target.checked; @@ -62,7 +97,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ ) : null} - + {requiredVars.map(varDef => { const { name: varName, type: varType } = varDef; @@ -83,6 +118,8 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ }, }); }} + errors={inputStreamValidationResults.config![varName]} + forceShowErrors={forceShowErrors} /> ); @@ -125,6 +162,8 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ }, }); }} + errors={inputStreamValidationResults.config![varName]} + forceShowErrors={forceShowErrors} /> ); @@ -134,6 +173,6 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ ) : null} - + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx index bcb99eed88ac0..846a807f9240d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx @@ -3,7 +3,7 @@ * 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 React, { useState } from 'react'; import ReactMarkdown from 'react-markdown'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFormRow, EuiFieldText, EuiComboBox, EuiText, EuiCodeEditor } from '@elastic/eui'; @@ -16,12 +16,20 @@ export const DatasourceInputVarField: React.FunctionComponent<{ varDef: RegistryVarsEntry; value: any; onChange: (newValue: any) => void; -}> = ({ varDef, value, onChange }) => { + errors?: string[] | null; + forceShowErrors?: boolean; +}> = ({ varDef, value, onChange, errors: varErrors, forceShowErrors }) => { + const [isDirty, setIsDirty] = useState(false); + const { multi, required, type, title, name, description } = varDef; + const isInvalid = (isDirty || forceShowErrors) && !!varErrors; + const errors = isInvalid ? varErrors : null; + const renderField = () => { - if (varDef.multi) { + if (multi) { return ( ({ label: val }))} onCreateOption={(newVal: any) => { onChange([...value, newVal]); @@ -29,10 +37,11 @@ export const DatasourceInputVarField: React.FunctionComponent<{ onChange={(newVals: any[]) => { onChange(newVals.map(val => val.label)); }} + onBlur={() => setIsDirty(true)} /> ); } - if (varDef.type === 'yaml') { + if (type === 'yaml') { return ( onChange(newVal)} + onBlur={() => setIsDirty(true)} /> ); } return ( onChange(e.target.value)} + onBlur={() => setIsDirty(true)} /> ); }; return ( ) : null } - helpText={} + helpText={} > {renderField()} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts index e5f18e1449d1b..3bfca75668911 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts @@ -5,3 +5,4 @@ */ export { CreateDatasourcePageLayout } from './layout'; export { DatasourceInputPanel } from './datasource_input_panel'; +export { DatasourceInputVarField } from './datasource_input_var_field'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 23d0f3317a667..7815ab9cd1d6e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -21,6 +21,7 @@ import { useLinks as useEPMLinks } from '../../epm/hooks'; import { CreateDatasourcePageLayout } from './components'; import { CreateDatasourceFrom, CreateDatasourceStep } from './types'; import { CREATE_DATASOURCE_STEP_PATHS } from './constants'; +import { DatasourceValidationResults, validateDatasource } from './services'; import { StepSelectPackage } from './step_select_package'; import { StepSelectConfig } from './step_select_config'; import { StepConfigureDatasource } from './step_configure_datasource'; @@ -51,6 +52,9 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { inputs: [], }); + // Datasource validation state + const [validationResults, setValidationResults] = useState(); + // Update package info method const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => { if (updatedPackageInfo) { @@ -84,9 +88,18 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { ...updatedFields, }; setDatasource(newDatasource); - // eslint-disable-next-line no-console console.debug('Datasource updated', newDatasource); + updateDatasourceValidation(newDatasource); + }; + + const updateDatasourceValidation = (newDatasource?: NewDatasource) => { + if (packageInfo) { + const newValidationResult = validateDatasource(newDatasource || datasource, packageInfo); + setValidationResults(newValidationResult); + // eslint-disable-next-line no-console + console.debug('Datasource validation results', newValidationResult); + } }; // Cancel url @@ -202,6 +215,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { packageInfo={packageInfo} datasource={datasource} updateDatasource={updateDatasource} + validationResults={validationResults!} backLink={ {from === 'config' ? ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts index 44e5bfa41cb9b..d99f0712db3c3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts @@ -4,3 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ export { isAdvancedVar } from './is_advanced_var'; +export { + DatasourceValidationResults, + DatasourceConfigValidationResults, + DatasourceInputValidationResults, + validateDatasource, + validationHasErrors, +} from './validate_datasource'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts new file mode 100644 index 0000000000000..a45fabeb5ed6a --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts @@ -0,0 +1,504 @@ +/* + * 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 { + PackageInfo, + InstallationStatus, + NewDatasource, + RegistryDatasource, +} from '../../../../types'; +import { validateDatasource, validationHasErrors } from './validate_datasource'; + +describe('Ingest Manager - validateDatasource()', () => { + const mockPackage = ({ + name: 'mock-package', + title: 'Mock package', + version: '0.0.0', + description: 'description', + type: 'mock', + categories: [], + requirement: { kibana: { versions: '' }, elasticsearch: { versions: '' } }, + format_version: '', + download: '', + path: '', + assets: { + kibana: { + dashboard: [], + visualization: [], + search: [], + 'index-pattern': [], + }, + }, + status: InstallationStatus.notInstalled, + datasources: [ + { + name: 'datasource1', + title: 'Datasource 1', + description: 'test datasource', + inputs: [ + { + type: 'foo', + title: 'Foo', + vars: [ + { default: 'foo-input-var-value', name: 'foo-input-var-name', type: 'text' }, + { + default: 'foo-input2-var-value', + name: 'foo-input2-var-name', + required: true, + type: 'text', + }, + { name: 'foo-input3-var-name', type: 'text', required: true, multi: true }, + ], + streams: [ + { + dataset: 'foo', + input: 'foo', + title: 'Foo', + vars: [{ name: 'var-name', type: 'yaml' }], + }, + ], + }, + { + type: 'bar', + title: 'Bar', + vars: [ + { + default: ['value1', 'value2'], + name: 'bar-input-var-name', + type: 'text', + multi: true, + }, + { name: 'bar-input2-var-name', required: true, type: 'text' }, + ], + streams: [ + { + dataset: 'bar', + input: 'bar', + title: 'Bar', + vars: [{ name: 'var-name', type: 'yaml', required: true }], + }, + { + dataset: 'bar2', + input: 'bar2', + title: 'Bar 2', + vars: [{ default: 'bar2-var-value', name: 'var-name', type: 'text' }], + }, + ], + }, + { + type: 'with-no-config-or-streams', + title: 'With no config or streams', + streams: [], + }, + { + type: 'with-disabled-streams', + title: 'With disabled streams', + streams: [ + { + dataset: 'disabled', + input: 'disabled', + title: 'Disabled', + enabled: false, + vars: [{ multi: true, required: true, name: 'var-name', type: 'text' }], + }, + { dataset: 'disabled2', input: 'disabled2', title: 'Disabled 2', enabled: false }, + ], + }, + ], + }, + ], + } as unknown) as PackageInfo; + + const validDatasource: NewDatasource = { + name: 'datasource1-1', + config_id: 'test-config', + enabled: true, + output_id: 'test-output', + inputs: [ + { + type: 'foo', + enabled: true, + config: { + 'foo-input-var-name': { value: 'foo-input-var-value', type: 'text' }, + 'foo-input2-var-name': { value: 'foo-input2-var-value', type: 'text' }, + 'foo-input3-var-name': { value: ['test'], type: 'text' }, + }, + streams: [ + { + id: 'foo-foo', + dataset: 'foo', + enabled: true, + config: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, + }, + ], + }, + { + type: 'bar', + enabled: true, + config: { + 'bar-input-var-name': { value: ['value1', 'value2'], type: 'text' }, + 'bar-input2-var-name': { value: 'test', type: 'text' }, + }, + streams: [ + { + id: 'bar-bar', + dataset: 'bar', + enabled: true, + config: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, + }, + { + id: 'bar-bar2', + dataset: 'bar2', + enabled: true, + config: { 'var-name': { value: undefined, type: 'text' } }, + }, + ], + }, + { + type: 'with-no-config-or-streams', + enabled: true, + streams: [], + }, + { + type: 'with-disabled-streams', + enabled: true, + streams: [ + { + id: 'with-disabled-streams-disabled', + dataset: 'disabled', + enabled: false, + config: { 'var-name': { value: undefined, type: 'text' } }, + }, + { + id: 'with-disabled-streams-disabled2', + dataset: 'disabled2', + enabled: false, + }, + ], + }, + ], + }; + + const invalidDatasource: NewDatasource = { + ...validDatasource, + name: '', + inputs: [ + { + type: 'foo', + enabled: true, + config: { + 'foo-input-var-name': { value: undefined, type: 'text' }, + 'foo-input2-var-name': { value: '', type: 'text' }, + 'foo-input3-var-name': { value: [], type: 'text' }, + }, + streams: [ + { + id: 'foo-foo', + dataset: 'foo', + enabled: true, + config: { 'var-name': { value: 'invalidyaml: test\n foo bar:', type: 'yaml' } }, + }, + ], + }, + { + type: 'bar', + enabled: true, + config: { + 'bar-input-var-name': { value: 'invalid value for multi', type: 'text' }, + 'bar-input2-var-name': { value: undefined, type: 'text' }, + }, + streams: [ + { + id: 'bar-bar', + dataset: 'bar', + enabled: true, + config: { 'var-name': { value: ' \n\n', type: 'yaml' } }, + }, + { + id: 'bar-bar2', + dataset: 'bar2', + enabled: true, + config: { 'var-name': { value: undefined, type: 'text' } }, + }, + ], + }, + { + type: 'with-no-config-or-streams', + enabled: true, + streams: [], + }, + { + type: 'with-disabled-streams', + enabled: true, + streams: [ + { + id: 'with-disabled-streams-disabled', + dataset: 'disabled', + enabled: false, + config: { + 'var-name': { + value: 'invalid value but not checked due to not enabled', + type: 'text', + }, + }, + }, + { + id: 'with-disabled-streams-disabled2', + dataset: 'disabled2', + enabled: false, + }, + ], + }, + ], + }; + + const noErrorsValidationResults = { + name: null, + description: null, + inputs: { + foo: { + config: { + 'foo-input-var-name': null, + 'foo-input2-var-name': null, + 'foo-input3-var-name': null, + }, + streams: { 'foo-foo': { config: { 'var-name': null } } }, + }, + bar: { + config: { 'bar-input-var-name': null, 'bar-input2-var-name': null }, + streams: { + 'bar-bar': { config: { 'var-name': null } }, + 'bar-bar2': { config: { 'var-name': null } }, + }, + }, + 'with-disabled-streams': { + streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } }, + }, + }, + }; + + it('returns no errors for valid datasource configuration', () => { + expect(validateDatasource(validDatasource, mockPackage)).toEqual(noErrorsValidationResults); + }); + + it('returns errors for invalid datasource configuration', () => { + expect(validateDatasource(invalidDatasource, mockPackage)).toEqual({ + name: ['Name is required'], + description: null, + inputs: { + foo: { + config: { + 'foo-input-var-name': null, + 'foo-input2-var-name': ['foo-input2-var-name is required'], + 'foo-input3-var-name': ['foo-input3-var-name is required'], + }, + streams: { 'foo-foo': { config: { 'var-name': ['Invalid YAML format'] } } }, + }, + bar: { + config: { + 'bar-input-var-name': ['Invalid format'], + 'bar-input2-var-name': ['bar-input2-var-name is required'], + }, + streams: { + 'bar-bar': { config: { 'var-name': ['var-name is required'] } }, + 'bar-bar2': { config: { 'var-name': null } }, + }, + }, + 'with-disabled-streams': { + streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } }, + }, + }, + }); + }); + + it('returns no errors for disabled inputs', () => { + const disabledInputs = invalidDatasource.inputs.map(input => ({ ...input, enabled: false })); + expect(validateDatasource({ ...validDatasource, inputs: disabledInputs }, mockPackage)).toEqual( + noErrorsValidationResults + ); + }); + + it('returns only datasource and input-level errors for disabled streams', () => { + const inputsWithDisabledStreams = invalidDatasource.inputs.map(input => + input.streams + ? { + ...input, + streams: input.streams.map(stream => ({ ...stream, enabled: false })), + } + : input + ); + expect( + validateDatasource({ ...invalidDatasource, inputs: inputsWithDisabledStreams }, mockPackage) + ).toEqual({ + name: ['Name is required'], + description: null, + inputs: { + foo: { + config: { + 'foo-input-var-name': null, + 'foo-input2-var-name': ['foo-input2-var-name is required'], + 'foo-input3-var-name': ['foo-input3-var-name is required'], + }, + streams: { 'foo-foo': { config: { 'var-name': null } } }, + }, + bar: { + config: { + 'bar-input-var-name': ['Invalid format'], + 'bar-input2-var-name': ['bar-input2-var-name is required'], + }, + streams: { + 'bar-bar': { config: { 'var-name': null } }, + 'bar-bar2': { config: { 'var-name': null } }, + }, + }, + 'with-disabled-streams': { + streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } }, + }, + }, + }); + }); + + it('returns no errors for packages with no datasources', () => { + expect( + validateDatasource(validDatasource, { + ...mockPackage, + datasources: undefined, + }) + ).toEqual({ + name: null, + description: null, + inputs: null, + }); + expect( + validateDatasource(validDatasource, { + ...mockPackage, + datasources: [], + }) + ).toEqual({ + name: null, + description: null, + inputs: null, + }); + }); + + it('returns no errors for packages with no inputs', () => { + expect( + validateDatasource(validDatasource, { + ...mockPackage, + datasources: [{} as RegistryDatasource], + }) + ).toEqual({ + name: null, + description: null, + inputs: null, + }); + expect( + validateDatasource(validDatasource, { + ...mockPackage, + datasources: [({ inputs: [] } as unknown) as RegistryDatasource], + }) + ).toEqual({ + name: null, + description: null, + inputs: null, + }); + }); +}); + +describe('Ingest Manager - validationHasErrors()', () => { + it('returns true for stream validation results with errors', () => { + expect( + validationHasErrors({ + config: { foo: ['foo error'], bar: null }, + }) + ).toBe(true); + }); + + it('returns false for stream validation results with no errors', () => { + expect( + validationHasErrors({ + config: { foo: null, bar: null }, + }) + ).toBe(false); + }); + + it('returns true for input validation results with errors', () => { + expect( + validationHasErrors({ + config: { foo: ['foo error'], bar: null }, + streams: { stream1: { config: { foo: null, bar: null } } }, + }) + ).toBe(true); + expect( + validationHasErrors({ + config: { foo: null, bar: null }, + streams: { stream1: { config: { foo: ['foo error'], bar: null } } }, + }) + ).toBe(true); + }); + + it('returns false for input validation results with no errors', () => { + expect( + validationHasErrors({ + config: { foo: null, bar: null }, + streams: { stream1: { config: { foo: null, bar: null } } }, + }) + ).toBe(false); + }); + + it('returns true for datasource validation results with errors', () => { + expect( + validationHasErrors({ + name: ['name error'], + description: null, + inputs: { + input1: { + config: { foo: null, bar: null }, + streams: { stream1: { config: { foo: null, bar: null } } }, + }, + }, + }) + ).toBe(true); + expect( + validationHasErrors({ + name: null, + description: null, + inputs: { + input1: { + config: { foo: ['foo error'], bar: null }, + streams: { stream1: { config: { foo: null, bar: null } } }, + }, + }, + }) + ).toBe(true); + expect( + validationHasErrors({ + name: null, + description: null, + inputs: { + input1: { + config: { foo: null, bar: null }, + streams: { stream1: { config: { foo: ['foo error'], bar: null } } }, + }, + }, + }) + ).toBe(true); + }); + + it('returns false for datasource validation results with no errors', () => { + expect( + validationHasErrors({ + name: null, + description: null, + inputs: { + input1: { + config: { foo: null, bar: null }, + streams: { stream1: { config: { foo: null, bar: null } } }, + }, + }, + }) + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts new file mode 100644 index 0000000000000..518e2bfc1af07 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts @@ -0,0 +1,232 @@ +/* + * 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 { safeLoad } from 'js-yaml'; +import { getFlattenedObject } from '../../../../services'; +import { + NewDatasource, + DatasourceInput, + DatasourceInputStream, + DatasourceConfigRecordEntry, + PackageInfo, + RegistryInput, + RegistryVarsEntry, +} from '../../../../types'; + +type Errors = string[] | null; + +type ValidationEntry = Record; + +export interface DatasourceConfigValidationResults { + config?: ValidationEntry; +} + +export type DatasourceInputValidationResults = DatasourceConfigValidationResults & { + streams?: Record; +}; + +export interface DatasourceValidationResults { + name: Errors; + description: Errors; + inputs: Record | null; +} + +/* + * Returns validation information for a given datasource configuration and package info + * Note: this method assumes that `datasource` is correctly structured for the given package + */ +export const validateDatasource = ( + datasource: NewDatasource, + packageInfo: PackageInfo +): DatasourceValidationResults => { + const validationResults: DatasourceValidationResults = { + name: null, + description: null, + inputs: {}, + }; + + if (!datasource.name.trim()) { + validationResults.name = [ + i18n.translate('xpack.ingestManager.datasourceValidation.nameRequiredErrorMessage', { + defaultMessage: 'Name is required', + }), + ]; + } + + if ( + !packageInfo.datasources || + packageInfo.datasources.length === 0 || + !packageInfo.datasources[0] || + !packageInfo.datasources[0].inputs || + packageInfo.datasources[0].inputs.length === 0 + ) { + validationResults.inputs = null; + return validationResults; + } + + const registryInputsByType: Record< + string, + RegistryInput + > = packageInfo.datasources[0].inputs.reduce((inputs, registryInput) => { + inputs[registryInput.type] = registryInput; + return inputs; + }, {} as Record); + + // Validate each datasource input with either its own config fields or streams + datasource.inputs.forEach(input => { + if (!input.config && !input.streams) { + return; + } + + const inputValidationResults: DatasourceInputValidationResults = { + config: undefined, + streams: {}, + }; + + const inputVarsByName = (registryInputsByType[input.type].vars || []).reduce( + (vars, registryVar) => { + vars[registryVar.name] = registryVar; + return vars; + }, + {} as Record + ); + + // Validate input-level config fields + const inputConfigs = Object.entries(input.config || {}); + if (inputConfigs.length) { + inputValidationResults.config = inputConfigs.reduce((results, [name, configEntry]) => { + results[name] = input.enabled + ? validateDatasourceConfig(configEntry, inputVarsByName[name]) + : null; + return results; + }, {} as ValidationEntry); + } else { + delete inputValidationResults.config; + } + + // Validate each input stream with config fields + if (input.streams.length) { + input.streams.forEach(stream => { + if (!stream.config) { + return; + } + + const streamValidationResults: DatasourceConfigValidationResults = { + config: undefined, + }; + + const streamVarsByName = ( + ( + registryInputsByType[input.type].streams.find( + registryStream => registryStream.dataset === stream.dataset + ) || {} + ).vars || [] + ).reduce((vars, registryVar) => { + vars[registryVar.name] = registryVar; + return vars; + }, {} as Record); + + // Validate stream-level config fields + streamValidationResults.config = Object.entries(stream.config).reduce( + (results, [name, configEntry]) => { + results[name] = + input.enabled && stream.enabled + ? validateDatasourceConfig(configEntry, streamVarsByName[name]) + : null; + return results; + }, + {} as ValidationEntry + ); + + inputValidationResults.streams![stream.id] = streamValidationResults; + }); + } else { + delete inputValidationResults.streams; + } + + if (inputValidationResults.config || inputValidationResults.streams) { + validationResults.inputs![input.type] = inputValidationResults; + } + }); + + if (Object.entries(validationResults.inputs!).length === 0) { + validationResults.inputs = null; + } + return validationResults; +}; + +const validateDatasourceConfig = ( + configEntry: DatasourceConfigRecordEntry, + varDef: RegistryVarsEntry +): string[] | null => { + const errors = []; + const { value } = configEntry; + let parsedValue: any = value; + + if (typeof value === 'string') { + parsedValue = value.trim(); + } + + if (varDef.required) { + if (parsedValue === undefined || (typeof parsedValue === 'string' && !parsedValue)) { + errors.push( + i18n.translate('xpack.ingestManager.datasourceValidation.requiredErrorMessage', { + defaultMessage: '{fieldName} is required', + values: { + fieldName: varDef.title || varDef.name, + }, + }) + ); + } + } + + if (varDef.type === 'yaml') { + try { + parsedValue = safeLoad(value); + } catch (e) { + errors.push( + i18n.translate('xpack.ingestManager.datasourceValidation.invalidYamlFormatErrorMessage', { + defaultMessage: 'Invalid YAML format', + }) + ); + } + } + + if (varDef.multi) { + if (parsedValue && !Array.isArray(parsedValue)) { + errors.push( + i18n.translate('xpack.ingestManager.datasourceValidation.invalidArrayErrorMessage', { + defaultMessage: 'Invalid format', + }) + ); + } + if ( + varDef.required && + (!parsedValue || (Array.isArray(parsedValue) && parsedValue.length === 0)) + ) { + errors.push( + i18n.translate('xpack.ingestManager.datasourceValidation.requiredErrorMessage', { + defaultMessage: '{fieldName} is required', + values: { + fieldName: varDef.title || varDef.name, + }, + }) + ); + } + } + + return errors.length ? errors : null; +}; + +export const validationHasErrors = ( + validationResults: + | DatasourceValidationResults + | DatasourceInputValidationResults + | DatasourceConfigValidationResults +) => { + const flattenedValidation = getFlattenedObject(validationResults); + return !!Object.entries(flattenedValidation).find(([, value]) => !!value); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx index b45beef4a8b5e..105d6c66a5704 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx @@ -9,17 +9,16 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSteps, EuiPanel, - EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiFormRow, - EuiFieldText, EuiButtonEmpty, EuiSpacer, EuiEmptyPrompt, EuiText, EuiButton, EuiComboBox, + EuiCallOut, } from '@elastic/eui'; import { AgentConfig, @@ -28,21 +27,37 @@ import { NewDatasource, DatasourceInput, } from '../../../types'; +import { Loading } from '../../../components'; import { packageToConfigDatasourceInputs } from '../../../services'; -import { DatasourceInputPanel } from './components'; +import { DatasourceValidationResults, validationHasErrors } from './services'; +import { DatasourceInputPanel, DatasourceInputVarField } from './components'; export const StepConfigureDatasource: React.FunctionComponent<{ agentConfig: AgentConfig; packageInfo: PackageInfo; datasource: NewDatasource; updateDatasource: (fields: Partial) => void; + validationResults: DatasourceValidationResults; backLink: JSX.Element; cancelUrl: string; onNext: () => void; -}> = ({ agentConfig, packageInfo, datasource, updateDatasource, backLink, cancelUrl, onNext }) => { +}> = ({ + agentConfig, + packageInfo, + datasource, + updateDatasource, + validationResults, + backLink, + cancelUrl, + onNext, +}) => { // Form show/hide states const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState(false); + // Form submit state + const [submitAttempted, setSubmitAttempted] = useState(false); + const hasErrors = validationResults ? validationHasErrors(validationResults) : false; + // Update datasource's package and config info useEffect(() => { const dsPackage = datasource.package; @@ -81,56 +96,56 @@ export const StepConfigureDatasource: React.FunctionComponent<{ }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); // Step A, define datasource - const DefineDatasource = ( + const renderDefineDatasource = () => ( - - - - } - > - - updateDatasource({ - name: e.target.value, - }) - } - /> - + + + { + updateDatasource({ + name: newValue, + }); + }} + errors={validationResults!.name} + forceShowErrors={submitAttempted} + /> - - - } - labelAppend={ - - - - } - > - - updateDatasource({ - description: e.target.value, - }) - } - /> - + + { + updateDatasource({ + description: newValue, + }); + }} + errors={validationResults!.description} + forceShowErrors={submitAttempted} + /> - + - - + + - + + ) : null} @@ -182,7 +198,7 @@ export const StepConfigureDatasource: React.FunctionComponent<{ // Step B, configure inputs (and their streams) // Assume packages only export one datasource for now - const ConfigureInputs = + const renderConfigureInputs = () => packageInfo.datasources && packageInfo.datasources[0] && packageInfo.datasources[0].inputs && @@ -208,6 +224,8 @@ export const StepConfigureDatasource: React.FunctionComponent<{ inputs: newInputs, }); }} + inputValidationResults={validationResults!.inputs![datasourceInput.type]} + forceShowErrors={submitAttempted} /> ) : null; @@ -232,7 +250,7 @@ export const StepConfigureDatasource: React.FunctionComponent<{ ); - return ( + return validationResults ? ( @@ -251,7 +269,7 @@ export const StepConfigureDatasource: React.FunctionComponent<{ defaultMessage: 'Define your datasource', } ), - children: DefineDatasource, + children: renderDefineDatasource(), }, { title: i18n.translate( @@ -260,13 +278,34 @@ export const StepConfigureDatasource: React.FunctionComponent<{ defaultMessage: 'Choose the data you want to collect', } ), - children: ConfigureInputs, + children: renderConfigureInputs(), }, ]} /> + {hasErrors && submitAttempted ? ( + + +

+ +

+
+ +
+ ) : null} @@ -278,7 +317,17 @@ export const StepConfigureDatasource: React.FunctionComponent<{
- onNext()}> + { + setSubmitAttempted(true); + if (!hasErrors) { + onNext(); + } + }} + > + ) : ( + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts index 0aa08602e4d4d..5ebd1300baf65 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export { getFlattenedObject } from '../../../../../../../src/core/utils'; + export { agentConfigRouteService, datasourceRouteService, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 333a9b049fa85..32615278b67d7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -16,6 +16,7 @@ export { NewDatasource, DatasourceInput, DatasourceInputStream, + DatasourceConfigRecordEntry, // API schemas - Agent Config GetAgentConfigsResponse, GetAgentConfigsResponseItem, @@ -56,6 +57,7 @@ export { RegistryVarsEntry, RegistryInput, RegistryStream, + RegistryDatasource, PackageList, PackageListItem, PackagesGroupedByStatus, @@ -70,4 +72,5 @@ export { DeletePackageResponse, DetailViewPanelName, InstallStatus, + InstallationStatus, } from '../../../../common'; From 578e443bdd79ea6436289ee39703816fe7419ebd Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Thu, 9 Apr 2020 00:08:21 +0300 Subject: [PATCH 38/81] FTR: add chromium-based Edge browser support (#61684) * bump dependency, add edge support in ftr services * add config files * fix browser version for msedge * use npm ms-chromium-edge-driver * download edge driver aside from session creation * move dependency to dev * update dist/index file * bump edge-driver version * change type to msedge to match w3c spec * fix discover tests for Edge * Revert "fix discover tests for Edge" This reverts commit 87e7fdd256433ef1ad392147d6bd63b925552b37. * bump driver version up Co-authored-by: Elastic Machine --- package.json | 5 +- packages/kbn-pm/dist/index.js | 1105 +++-------------- .../lib/config/schema.ts | 2 +- test/functional/config.edge.js | 34 + test/functional/services/browser.ts | 4 +- .../web_element_wrapper.ts | 9 +- test/functional/services/remote/browsers.ts | 1 + test/functional/services/remote/remote.ts | 17 +- test/functional/services/remote/webdriver.ts | 50 +- x-pack/test/functional/config.edge.js | 21 + yarn.lock | 320 ++++- 11 files changed, 556 insertions(+), 1012 deletions(-) create mode 100644 test/functional/config.edge.js create mode 100644 x-pack/test/functional/config.edge.js diff --git a/package.json b/package.json index 4c5db5321c282..bd0fec3a5c116 100644 --- a/package.json +++ b/package.json @@ -376,7 +376,7 @@ "@types/recompose": "^0.30.6", "@types/redux-actions": "^2.6.1", "@types/request": "^2.48.2", - "@types/selenium-webdriver": "^4.0.5", + "@types/selenium-webdriver": "4.0.9", "@types/semver": "^5.5.0", "@types/sinon": "^7.0.13", "@types/strip-ansi": "^3.0.0", @@ -462,6 +462,7 @@ "load-grunt-config": "^3.0.1", "mocha": "^7.1.1", "mock-http-server": "1.3.0", + "ms-chromium-edge-driver": "^0.2.0", "multistream": "^2.1.1", "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", @@ -480,7 +481,7 @@ "react-textarea-autosize": "^7.1.2", "regenerate": "^1.4.0", "sass-lint": "^1.12.1", - "selenium-webdriver": "^4.0.0-alpha.5", + "selenium-webdriver": "^4.0.0-alpha.7", "simple-git": "1.116.0", "simplebar-react": "^2.1.0", "sinon": "^7.4.2", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 0cc1ad6326671..7a858deff41d3 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -79260,7 +79260,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(705); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(928); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(923); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -79445,9 +79445,9 @@ const pAll = __webpack_require__(707); const arrify = __webpack_require__(709); const globby = __webpack_require__(710); const isGlob = __webpack_require__(604); -const cpFile = __webpack_require__(913); -const junk = __webpack_require__(925); -const CpyError = __webpack_require__(926); +const cpFile = __webpack_require__(908); +const junk = __webpack_require__(920); +const CpyError = __webpack_require__(921); const defaultOptions = { ignoreJunk: true @@ -79697,8 +79697,8 @@ const fs = __webpack_require__(23); const arrayUnion = __webpack_require__(711); const glob = __webpack_require__(713); const fastGlob = __webpack_require__(718); -const dirGlob = __webpack_require__(906); -const gitignore = __webpack_require__(909); +const dirGlob = __webpack_require__(901); +const gitignore = __webpack_require__(904); const DEFAULT_FILTER = () => false; @@ -81531,11 +81531,11 @@ module.exports.generateTasks = pkg.generateTasks; Object.defineProperty(exports, "__esModule", { value: true }); var optionsManager = __webpack_require__(720); var taskManager = __webpack_require__(721); -var reader_async_1 = __webpack_require__(877); -var reader_stream_1 = __webpack_require__(901); -var reader_sync_1 = __webpack_require__(902); -var arrayUtils = __webpack_require__(904); -var streamUtils = __webpack_require__(905); +var reader_async_1 = __webpack_require__(872); +var reader_stream_1 = __webpack_require__(896); +var reader_sync_1 = __webpack_require__(897); +var arrayUtils = __webpack_require__(899); +var streamUtils = __webpack_require__(900); /** * Synchronous API. */ @@ -82175,9 +82175,9 @@ var extend = __webpack_require__(838); */ var compilers = __webpack_require__(841); -var parsers = __webpack_require__(873); -var cache = __webpack_require__(874); -var utils = __webpack_require__(875); +var parsers = __webpack_require__(868); +var cache = __webpack_require__(869); +var utils = __webpack_require__(870); var MAX_LENGTH = 1024 * 64; /** @@ -100710,9 +100710,9 @@ var toRegex = __webpack_require__(729); */ var compilers = __webpack_require__(858); -var parsers = __webpack_require__(869); -var Extglob = __webpack_require__(872); -var utils = __webpack_require__(871); +var parsers = __webpack_require__(864); +var Extglob = __webpack_require__(867); +var utils = __webpack_require__(866); var MAX_LENGTH = 1024 * 64; /** @@ -101222,7 +101222,7 @@ var parsers = __webpack_require__(862); * Module dependencies */ -var debug = __webpack_require__(864)('expand-brackets'); +var debug = __webpack_require__(801)('expand-brackets'); var extend = __webpack_require__(738); var Snapdragon = __webpack_require__(768); var toRegex = __webpack_require__(729); @@ -101816,839 +101816,12 @@ exports.createRegex = function(pattern, include) { /* 864 */ /***/ (function(module, exports, __webpack_require__) { -/** - * Detect Electron renderer process, which is node, but we should - * treat as a browser. - */ - -if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(865); -} else { - module.exports = __webpack_require__(868); -} - - -/***/ }), -/* 865 */ -/***/ (function(module, exports, __webpack_require__) { - -/** - * This is the web browser implementation of `debug()`. - * - * Expose `debug()` as the module. - */ - -exports = module.exports = __webpack_require__(866); -exports.log = log; -exports.formatArgs = formatArgs; -exports.save = save; -exports.load = load; -exports.useColors = useColors; -exports.storage = 'undefined' != typeof chrome - && 'undefined' != typeof chrome.storage - ? chrome.storage.local - : localstorage(); - -/** - * Colors. - */ - -exports.colors = [ - 'lightseagreen', - 'forestgreen', - 'goldenrod', - 'dodgerblue', - 'darkorchid', - 'crimson' -]; - -/** - * Currently only WebKit-based Web Inspectors, Firefox >= v31, - * and the Firebug extension (any Firefox version) are known - * to support "%c" CSS customizations. - * - * TODO: add a `localStorage` variable to explicitly enable/disable colors - */ - -function useColors() { - // NB: In an Electron preload script, document will be defined but not fully - // initialized. Since we know we're in Chrome, we'll just detect this case - // explicitly - if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') { - return true; - } - - // is webkit? http://stackoverflow.com/a/16459606/376773 - // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 - return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || - // is firebug? http://stackoverflow.com/a/398120/376773 - (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || - // is firefox >= v31? - // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages - (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || - // double check webkit in userAgent just in case we are in a worker - (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); -} - -/** - * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. - */ - -exports.formatters.j = function(v) { - try { - return JSON.stringify(v); - } catch (err) { - return '[UnexpectedJSONParseError]: ' + err.message; - } -}; - - -/** - * Colorize log arguments if enabled. - * - * @api public - */ - -function formatArgs(args) { - var useColors = this.useColors; - - args[0] = (useColors ? '%c' : '') - + this.namespace - + (useColors ? ' %c' : ' ') - + args[0] - + (useColors ? '%c ' : ' ') - + '+' + exports.humanize(this.diff); - - if (!useColors) return; - - var c = 'color: ' + this.color; - args.splice(1, 0, c, 'color: inherit') - - // the final "%c" is somewhat tricky, because there could be other - // arguments passed either before or after the %c, so we need to - // figure out the correct index to insert the CSS into - var index = 0; - var lastC = 0; - args[0].replace(/%[a-zA-Z%]/g, function(match) { - if ('%%' === match) return; - index++; - if ('%c' === match) { - // we only are interested in the *last* %c - // (the user may have provided their own) - lastC = index; - } - }); - - args.splice(lastC, 0, c); -} - -/** - * Invokes `console.log()` when available. - * No-op when `console.log` is not a "function". - * - * @api public - */ - -function log() { - // this hackery is required for IE8/9, where - // the `console.log` function doesn't have 'apply' - return 'object' === typeof console - && console.log - && Function.prototype.apply.call(console.log, console, arguments); -} - -/** - * Save `namespaces`. - * - * @param {String} namespaces - * @api private - */ - -function save(namespaces) { - try { - if (null == namespaces) { - exports.storage.removeItem('debug'); - } else { - exports.storage.debug = namespaces; - } - } catch(e) {} -} - -/** - * Load `namespaces`. - * - * @return {String} returns the previously persisted debug modes - * @api private - */ - -function load() { - var r; - try { - r = exports.storage.debug; - } catch(e) {} - - // If debug isn't set in LS, and we're in Electron, try to load $DEBUG - if (!r && typeof process !== 'undefined' && 'env' in process) { - r = process.env.DEBUG; - } - - return r; -} - -/** - * Enable namespaces listed in `localStorage.debug` initially. - */ - -exports.enable(load()); - -/** - * Localstorage attempts to return the localstorage. - * - * This is necessary because safari throws - * when a user disables cookies/localstorage - * and you attempt to access it. - * - * @return {LocalStorage} - * @api private - */ - -function localstorage() { - try { - return window.localStorage; - } catch (e) {} -} - - -/***/ }), -/* 866 */ -/***/ (function(module, exports, __webpack_require__) { - - -/** - * This is the common logic for both the Node.js and web browser - * implementations of `debug()`. - * - * Expose `debug()` as the module. - */ - -exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; -exports.coerce = coerce; -exports.disable = disable; -exports.enable = enable; -exports.enabled = enabled; -exports.humanize = __webpack_require__(867); - -/** - * The currently active debug mode names, and names to skip. - */ - -exports.names = []; -exports.skips = []; - -/** - * Map of special "%n" handling functions, for the debug "format" argument. - * - * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". - */ - -exports.formatters = {}; - -/** - * Previous log timestamp. - */ - -var prevTime; - -/** - * Select a color. - * @param {String} namespace - * @return {Number} - * @api private - */ - -function selectColor(namespace) { - var hash = 0, i; - - for (i in namespace) { - hash = ((hash << 5) - hash) + namespace.charCodeAt(i); - hash |= 0; // Convert to 32bit integer - } - - return exports.colors[Math.abs(hash) % exports.colors.length]; -} - -/** - * Create a debugger with the given `namespace`. - * - * @param {String} namespace - * @return {Function} - * @api public - */ - -function createDebug(namespace) { - - function debug() { - // disabled? - if (!debug.enabled) return; - - var self = debug; - - // set `diff` timestamp - var curr = +new Date(); - var ms = curr - (prevTime || curr); - self.diff = ms; - self.prev = prevTime; - self.curr = curr; - prevTime = curr; - - // turn the `arguments` into a proper Array - var args = new Array(arguments.length); - for (var i = 0; i < args.length; i++) { - args[i] = arguments[i]; - } - - args[0] = exports.coerce(args[0]); - - if ('string' !== typeof args[0]) { - // anything else let's inspect with %O - args.unshift('%O'); - } - - // apply any `formatters` transformations - var index = 0; - args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { - // if we encounter an escaped % then don't increase the array index - if (match === '%%') return match; - index++; - var formatter = exports.formatters[format]; - if ('function' === typeof formatter) { - var val = args[index]; - match = formatter.call(self, val); - - // now we need to remove `args[index]` since it's inlined in the `format` - args.splice(index, 1); - index--; - } - return match; - }); - - // apply env-specific formatting (colors, etc.) - exports.formatArgs.call(self, args); - - var logFn = debug.log || exports.log || console.log.bind(console); - logFn.apply(self, args); - } - - debug.namespace = namespace; - debug.enabled = exports.enabled(namespace); - debug.useColors = exports.useColors(); - debug.color = selectColor(namespace); - - // env-specific initialization logic for debug instances - if ('function' === typeof exports.init) { - exports.init(debug); - } - - return debug; -} - -/** - * Enables a debug mode by namespaces. This can include modes - * separated by a colon and wildcards. - * - * @param {String} namespaces - * @api public - */ - -function enable(namespaces) { - exports.save(namespaces); - - exports.names = []; - exports.skips = []; - - var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); - var len = split.length; - - for (var i = 0; i < len; i++) { - if (!split[i]) continue; // ignore empty strings - namespaces = split[i].replace(/\*/g, '.*?'); - if (namespaces[0] === '-') { - exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); - } else { - exports.names.push(new RegExp('^' + namespaces + '$')); - } - } -} - -/** - * Disable debug output. - * - * @api public - */ - -function disable() { - exports.enable(''); -} - -/** - * Returns true if the given mode name is enabled, false otherwise. - * - * @param {String} name - * @return {Boolean} - * @api public - */ - -function enabled(name) { - var i, len; - for (i = 0, len = exports.skips.length; i < len; i++) { - if (exports.skips[i].test(name)) { - return false; - } - } - for (i = 0, len = exports.names.length; i < len; i++) { - if (exports.names[i].test(name)) { - return true; - } - } - return false; -} - -/** - * Coerce `val`. - * - * @param {Mixed} val - * @return {Mixed} - * @api private - */ - -function coerce(val) { - if (val instanceof Error) return val.stack || val.message; - return val; -} - - -/***/ }), -/* 867 */ -/***/ (function(module, exports) { - -/** - * Helpers. - */ - -var s = 1000; -var m = s * 60; -var h = m * 60; -var d = h * 24; -var y = d * 365.25; - -/** - * Parse or format the given `val`. - * - * Options: - * - * - `long` verbose formatting [false] - * - * @param {String|Number} val - * @param {Object} [options] - * @throws {Error} throw an error if val is not a non-empty string or a number - * @return {String|Number} - * @api public - */ - -module.exports = function(val, options) { - options = options || {}; - var type = typeof val; - if (type === 'string' && val.length > 0) { - return parse(val); - } else if (type === 'number' && isNaN(val) === false) { - return options.long ? fmtLong(val) : fmtShort(val); - } - throw new Error( - 'val is not a non-empty string or a valid number. val=' + - JSON.stringify(val) - ); -}; - -/** - * Parse the given `str` and return milliseconds. - * - * @param {String} str - * @return {Number} - * @api private - */ - -function parse(str) { - str = String(str); - if (str.length > 100) { - return; - } - var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec( - str - ); - if (!match) { - return; - } - var n = parseFloat(match[1]); - var type = (match[2] || 'ms').toLowerCase(); - switch (type) { - case 'years': - case 'year': - case 'yrs': - case 'yr': - case 'y': - return n * y; - case 'days': - case 'day': - case 'd': - return n * d; - case 'hours': - case 'hour': - case 'hrs': - case 'hr': - case 'h': - return n * h; - case 'minutes': - case 'minute': - case 'mins': - case 'min': - case 'm': - return n * m; - case 'seconds': - case 'second': - case 'secs': - case 'sec': - case 's': - return n * s; - case 'milliseconds': - case 'millisecond': - case 'msecs': - case 'msec': - case 'ms': - return n; - default: - return undefined; - } -} - -/** - * Short format for `ms`. - * - * @param {Number} ms - * @return {String} - * @api private - */ - -function fmtShort(ms) { - if (ms >= d) { - return Math.round(ms / d) + 'd'; - } - if (ms >= h) { - return Math.round(ms / h) + 'h'; - } - if (ms >= m) { - return Math.round(ms / m) + 'm'; - } - if (ms >= s) { - return Math.round(ms / s) + 's'; - } - return ms + 'ms'; -} - -/** - * Long format for `ms`. - * - * @param {Number} ms - * @return {String} - * @api private - */ - -function fmtLong(ms) { - return plural(ms, d, 'day') || - plural(ms, h, 'hour') || - plural(ms, m, 'minute') || - plural(ms, s, 'second') || - ms + ' ms'; -} - -/** - * Pluralization helper. - */ - -function plural(ms, n, name) { - if (ms < n) { - return; - } - if (ms < n * 1.5) { - return Math.floor(ms / n) + ' ' + name; - } - return Math.ceil(ms / n) + ' ' + name + 's'; -} - - -/***/ }), -/* 868 */ -/***/ (function(module, exports, __webpack_require__) { - -/** - * Module dependencies. - */ - -var tty = __webpack_require__(478); -var util = __webpack_require__(29); - -/** - * This is the Node.js implementation of `debug()`. - * - * Expose `debug()` as the module. - */ - -exports = module.exports = __webpack_require__(866); -exports.init = init; -exports.log = log; -exports.formatArgs = formatArgs; -exports.save = save; -exports.load = load; -exports.useColors = useColors; - -/** - * Colors. - */ - -exports.colors = [6, 2, 3, 4, 5, 1]; - -/** - * Build up the default `inspectOpts` object from the environment variables. - * - * $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js - */ - -exports.inspectOpts = Object.keys(process.env).filter(function (key) { - return /^debug_/i.test(key); -}).reduce(function (obj, key) { - // camel-case - var prop = key - .substring(6) - .toLowerCase() - .replace(/_([a-z])/g, function (_, k) { return k.toUpperCase() }); - - // coerce string value into JS value - var val = process.env[key]; - if (/^(yes|on|true|enabled)$/i.test(val)) val = true; - else if (/^(no|off|false|disabled)$/i.test(val)) val = false; - else if (val === 'null') val = null; - else val = Number(val); - - obj[prop] = val; - return obj; -}, {}); - -/** - * The file descriptor to write the `debug()` calls to. - * Set the `DEBUG_FD` env variable to override with another value. i.e.: - * - * $ DEBUG_FD=3 node script.js 3>debug.log - */ - -var fd = parseInt(process.env.DEBUG_FD, 10) || 2; - -if (1 !== fd && 2 !== fd) { - util.deprecate(function(){}, 'except for stderr(2) and stdout(1), any other usage of DEBUG_FD is deprecated. Override debug.log if you want to use a different log function (https://git.io/debug_fd)')() -} - -var stream = 1 === fd ? process.stdout : - 2 === fd ? process.stderr : - createWritableStdioStream(fd); - -/** - * Is stdout a TTY? Colored output is enabled when `true`. - */ - -function useColors() { - return 'colors' in exports.inspectOpts - ? Boolean(exports.inspectOpts.colors) - : tty.isatty(fd); -} - -/** - * Map %o to `util.inspect()`, all on a single line. - */ - -exports.formatters.o = function(v) { - this.inspectOpts.colors = this.useColors; - return util.inspect(v, this.inspectOpts) - .split('\n').map(function(str) { - return str.trim() - }).join(' '); -}; - -/** - * Map %o to `util.inspect()`, allowing multiple lines if needed. - */ - -exports.formatters.O = function(v) { - this.inspectOpts.colors = this.useColors; - return util.inspect(v, this.inspectOpts); -}; - -/** - * Adds ANSI color escape codes if enabled. - * - * @api public - */ - -function formatArgs(args) { - var name = this.namespace; - var useColors = this.useColors; - - if (useColors) { - var c = this.color; - var prefix = ' \u001b[3' + c + ';1m' + name + ' ' + '\u001b[0m'; - - args[0] = prefix + args[0].split('\n').join('\n' + prefix); - args.push('\u001b[3' + c + 'm+' + exports.humanize(this.diff) + '\u001b[0m'); - } else { - args[0] = new Date().toUTCString() - + ' ' + name + ' ' + args[0]; - } -} - -/** - * Invokes `util.format()` with the specified arguments and writes to `stream`. - */ - -function log() { - return stream.write(util.format.apply(util, arguments) + '\n'); -} - -/** - * Save `namespaces`. - * - * @param {String} namespaces - * @api private - */ - -function save(namespaces) { - if (null == namespaces) { - // If you set a process.env field to null or undefined, it gets cast to the - // string 'null' or 'undefined'. Just delete instead. - delete process.env.DEBUG; - } else { - process.env.DEBUG = namespaces; - } -} - -/** - * Load `namespaces`. - * - * @return {String} returns the previously persisted debug modes - * @api private - */ - -function load() { - return process.env.DEBUG; -} - -/** - * Copied from `node/src/node.js`. - * - * XXX: It's lame that node doesn't expose this API out-of-the-box. It also - * relies on the undocumented `tty_wrap.guessHandleType()` which is also lame. - */ - -function createWritableStdioStream (fd) { - var stream; - var tty_wrap = process.binding('tty_wrap'); - - // Note stream._type is used for test-module-load-list.js - - switch (tty_wrap.guessHandleType(fd)) { - case 'TTY': - stream = new tty.WriteStream(fd); - stream._type = 'tty'; - - // Hack to have stream not keep the event loop alive. - // See https://github.com/joyent/node/issues/1726 - if (stream._handle && stream._handle.unref) { - stream._handle.unref(); - } - break; - - case 'FILE': - var fs = __webpack_require__(23); - stream = new fs.SyncWriteStream(fd, { autoClose: false }); - stream._type = 'fs'; - break; - - case 'PIPE': - case 'TCP': - var net = __webpack_require__(806); - stream = new net.Socket({ - fd: fd, - readable: false, - writable: true - }); - - // FIXME Should probably have an option in net.Socket to create a - // stream from an existing fd which is writable only. But for now - // we'll just add this hack and set the `readable` member to false. - // Test: ./node test/fixtures/echo.js < /etc/passwd - stream.readable = false; - stream.read = null; - stream._type = 'pipe'; - - // FIXME Hack to have stream not keep the event loop alive. - // See https://github.com/joyent/node/issues/1726 - if (stream._handle && stream._handle.unref) { - stream._handle.unref(); - } - break; - - default: - // Probably an error on in uv_guess_handle() - throw new Error('Implement me. Unknown stream file type!'); - } - - // For supporting legacy API we put the FD here. - stream.fd = fd; - - stream._isStdio = true; - - return stream; -} - -/** - * Init logic for `debug` instances. - * - * Create a new `inspectOpts` object in case `useColors` is set - * differently for a particular `debug` instance. - */ - -function init (debug) { - debug.inspectOpts = {}; - - var keys = Object.keys(exports.inspectOpts); - for (var i = 0; i < keys.length; i++) { - debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; - } -} - -/** - * Enable namespaces listed in `process.env.DEBUG` initially. - */ - -exports.enable(load()); - - -/***/ }), -/* 869 */ -/***/ (function(module, exports, __webpack_require__) { - "use strict"; var brackets = __webpack_require__(859); -var define = __webpack_require__(870); -var utils = __webpack_require__(871); +var define = __webpack_require__(865); +var utils = __webpack_require__(866); /** * Characters to use in text regex (we want to "not" match @@ -102803,7 +101976,7 @@ module.exports = parsers; /***/ }), -/* 870 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102841,7 +102014,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 871 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102917,7 +102090,7 @@ utils.createRegex = function(str) { /***/ }), -/* 872 */ +/* 867 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102928,7 +102101,7 @@ utils.createRegex = function(str) { */ var Snapdragon = __webpack_require__(768); -var define = __webpack_require__(870); +var define = __webpack_require__(865); var extend = __webpack_require__(738); /** @@ -102936,7 +102109,7 @@ var extend = __webpack_require__(738); */ var compilers = __webpack_require__(858); -var parsers = __webpack_require__(869); +var parsers = __webpack_require__(864); /** * Customize Snapdragon parser and renderer @@ -103002,7 +102175,7 @@ module.exports = Extglob; /***/ }), -/* 873 */ +/* 868 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103092,14 +102265,14 @@ function textRegex(pattern) { /***/ }), -/* 874 */ +/* 869 */ /***/ (function(module, exports, __webpack_require__) { module.exports = new (__webpack_require__(850))(); /***/ }), -/* 875 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103117,7 +102290,7 @@ utils.define = __webpack_require__(837); utils.diff = __webpack_require__(854); utils.extend = __webpack_require__(838); utils.pick = __webpack_require__(855); -utils.typeOf = __webpack_require__(876); +utils.typeOf = __webpack_require__(871); utils.unique = __webpack_require__(741); /** @@ -103415,7 +102588,7 @@ utils.unixify = function(options) { /***/ }), -/* 876 */ +/* 871 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -103550,7 +102723,7 @@ function isBuffer(val) { /***/ }), -/* 877 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103569,9 +102742,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_stream_1 = __webpack_require__(895); +var readdir = __webpack_require__(873); +var reader_1 = __webpack_require__(886); +var fs_stream_1 = __webpack_require__(890); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -103632,15 +102805,15 @@ exports.default = ReaderAsync; /***/ }), -/* 878 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(879); -const readdirAsync = __webpack_require__(887); -const readdirStream = __webpack_require__(890); +const readdirSync = __webpack_require__(874); +const readdirAsync = __webpack_require__(882); +const readdirStream = __webpack_require__(885); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -103724,7 +102897,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 879 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103732,11 +102905,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(880); +const DirectoryReader = __webpack_require__(875); let syncFacade = { - fs: __webpack_require__(885), - forEach: __webpack_require__(886), + fs: __webpack_require__(880), + forEach: __webpack_require__(881), sync: true }; @@ -103765,7 +102938,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 880 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103774,9 +102947,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; const EventEmitter = __webpack_require__(379).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(881); -const stat = __webpack_require__(883); -const call = __webpack_require__(884); +const normalizeOptions = __webpack_require__(876); +const stat = __webpack_require__(878); +const call = __webpack_require__(879); /** * Asynchronously reads the contents of a directory and streams the results @@ -104152,14 +103325,14 @@ module.exports = DirectoryReader; /***/ }), -/* 881 */ +/* 876 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(882); +const globToRegExp = __webpack_require__(877); module.exports = normalizeOptions; @@ -104336,7 +103509,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 882 */ +/* 877 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -104473,13 +103646,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 883 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(884); +const call = __webpack_require__(879); module.exports = stat; @@ -104554,7 +103727,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 884 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104615,14 +103788,14 @@ function callOnce (fn) { /***/ }), -/* 885 */ +/* 880 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(884); +const call = __webpack_require__(879); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -104686,7 +103859,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 886 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104715,7 +103888,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 887 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104723,12 +103896,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(888); -const DirectoryReader = __webpack_require__(880); +const maybe = __webpack_require__(883); +const DirectoryReader = __webpack_require__(875); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(889), + forEach: __webpack_require__(884), async: true }; @@ -104770,7 +103943,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 888 */ +/* 883 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104797,7 +103970,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 889 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104833,7 +104006,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 890 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104841,11 +104014,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(880); +const DirectoryReader = __webpack_require__(875); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(889), + forEach: __webpack_require__(884), async: true }; @@ -104865,16 +104038,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 891 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(892); -var entry_1 = __webpack_require__(894); -var pathUtil = __webpack_require__(893); +var deep_1 = __webpack_require__(887); +var entry_1 = __webpack_require__(889); +var pathUtil = __webpack_require__(888); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -104940,13 +104113,13 @@ exports.default = Reader; /***/ }), -/* 892 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(893); +var pathUtils = __webpack_require__(888); var patternUtils = __webpack_require__(722); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { @@ -105030,7 +104203,7 @@ exports.default = DeepFilter; /***/ }), -/* 893 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105061,13 +104234,13 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 894 */ +/* 889 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(893); +var pathUtils = __webpack_require__(888); var patternUtils = __webpack_require__(722); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { @@ -105153,7 +104326,7 @@ exports.default = EntryFilter; /***/ }), -/* 895 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105173,8 +104346,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(896); -var fs_1 = __webpack_require__(900); +var fsStat = __webpack_require__(891); +var fs_1 = __webpack_require__(895); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -105224,14 +104397,14 @@ exports.default = FileSystemStream; /***/ }), -/* 896 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(897); -const statProvider = __webpack_require__(899); +const optionsManager = __webpack_require__(892); +const statProvider = __webpack_require__(894); /** * Asynchronous API. */ @@ -105262,13 +104435,13 @@ exports.statSync = statSync; /***/ }), -/* 897 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(898); +const fsAdapter = __webpack_require__(893); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -105281,7 +104454,7 @@ exports.prepare = prepare; /***/ }), -/* 898 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105304,7 +104477,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 899 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105356,7 +104529,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 900 */ +/* 895 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105387,7 +104560,7 @@ exports.default = FileSystem; /***/ }), -/* 901 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105407,9 +104580,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_stream_1 = __webpack_require__(895); +var readdir = __webpack_require__(873); +var reader_1 = __webpack_require__(886); +var fs_stream_1 = __webpack_require__(890); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -105477,7 +104650,7 @@ exports.default = ReaderStream; /***/ }), -/* 902 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105496,9 +104669,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_sync_1 = __webpack_require__(903); +var readdir = __webpack_require__(873); +var reader_1 = __webpack_require__(886); +var fs_sync_1 = __webpack_require__(898); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -105558,7 +104731,7 @@ exports.default = ReaderSync; /***/ }), -/* 903 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105577,8 +104750,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(896); -var fs_1 = __webpack_require__(900); +var fsStat = __webpack_require__(891); +var fs_1 = __webpack_require__(895); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -105624,7 +104797,7 @@ exports.default = FileSystemSync; /***/ }), -/* 904 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105640,7 +104813,7 @@ exports.flatten = flatten; /***/ }), -/* 905 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105661,13 +104834,13 @@ exports.merge = merge; /***/ }), -/* 906 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(907); +const pathType = __webpack_require__(902); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -105733,13 +104906,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 907 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(908); +const pify = __webpack_require__(903); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -105782,7 +104955,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 908 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105873,7 +105046,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 909 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105881,9 +105054,9 @@ module.exports = (obj, opts) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const fastGlob = __webpack_require__(718); -const gitIgnore = __webpack_require__(910); -const pify = __webpack_require__(911); -const slash = __webpack_require__(912); +const gitIgnore = __webpack_require__(905); +const pify = __webpack_require__(906); +const slash = __webpack_require__(907); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -105981,7 +105154,7 @@ module.exports.sync = options => { /***/ }), -/* 910 */ +/* 905 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -106450,7 +105623,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 911 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106525,7 +105698,7 @@ module.exports = (input, options) => { /***/ }), -/* 912 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106543,17 +105716,17 @@ module.exports = input => { /***/ }), -/* 913 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const pEvent = __webpack_require__(914); -const CpFileError = __webpack_require__(917); -const fs = __webpack_require__(921); -const ProgressEmitter = __webpack_require__(924); +const pEvent = __webpack_require__(909); +const CpFileError = __webpack_require__(912); +const fs = __webpack_require__(916); +const ProgressEmitter = __webpack_require__(919); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -106667,12 +105840,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 914 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(915); +const pTimeout = __webpack_require__(910); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -106963,12 +106136,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 915 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(916); +const pFinally = __webpack_require__(911); class TimeoutError extends Error { constructor(message) { @@ -107014,7 +106187,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 916 */ +/* 911 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107036,12 +106209,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 917 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(918); +const NestedError = __webpack_require__(913); class CpFileError extends NestedError { constructor(message, nested) { @@ -107055,10 +106228,10 @@ module.exports = CpFileError; /***/ }), -/* 918 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(919); +var inherits = __webpack_require__(914); var NestedError = function (message, nested) { this.nested = nested; @@ -107109,7 +106282,7 @@ module.exports = NestedError; /***/ }), -/* 919 */ +/* 914 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -107117,12 +106290,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(920); + module.exports = __webpack_require__(915); } /***/ }), -/* 920 */ +/* 915 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -107151,16 +106324,16 @@ if (typeof Object.create === 'function') { /***/ }), -/* 921 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(922); -const pEvent = __webpack_require__(914); -const CpFileError = __webpack_require__(917); +const makeDir = __webpack_require__(917); +const pEvent = __webpack_require__(909); +const CpFileError = __webpack_require__(912); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -107257,7 +106430,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 922 */ +/* 917 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107265,7 +106438,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const {promisify} = __webpack_require__(29); -const semver = __webpack_require__(923); +const semver = __webpack_require__(918); const defaults = { mode: 0o777 & (~process.umask()), @@ -107414,7 +106587,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 923 */ +/* 918 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -109016,7 +108189,7 @@ function coerce (version, options) { /***/ }), -/* 924 */ +/* 919 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109057,7 +108230,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 925 */ +/* 920 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109103,12 +108276,12 @@ exports.default = module.exports; /***/ }), -/* 926 */ +/* 921 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(927); +const NestedError = __webpack_require__(922); class CpyError extends NestedError { constructor(message, nested) { @@ -109122,7 +108295,7 @@ module.exports = CpyError; /***/ }), -/* 927 */ +/* 922 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(29).inherits; @@ -109178,7 +108351,7 @@ module.exports = NestedError; /***/ }), -/* 928 */ +/* 923 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 66f17ab579ec3..f4b91d154cbb8 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -136,7 +136,7 @@ export const schema = Joi.object() browser: Joi.object() .keys({ type: Joi.string() - .valid('chrome', 'firefox', 'ie') + .valid('chrome', 'firefox', 'ie', 'msedge') .default('chrome'), logPollingMs: Joi.number().default(100), diff --git a/test/functional/config.edge.js b/test/functional/config.edge.js new file mode 100644 index 0000000000000..ed68b41e8c89a --- /dev/null +++ b/test/functional/config.edge.js @@ -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. + */ + +export default async function({ readConfigFile }) { + const defaultConfig = await readConfigFile(require.resolve('./config')); + + return { + ...defaultConfig.getAll(), + + browser: { + type: 'msedge', + }, + + junit: { + reportName: 'MS Chromium Edge UI Functional Tests', + }, + }; +} diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index 5017947e95d03..13d2365c07191 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -47,7 +47,9 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { */ public readonly browserType: string = browserType; - public readonly isChrome: boolean = browserType === Browsers.Chrome; + public readonly isChromium: boolean = [Browsers.Chrome, Browsers.ChromiumEdge].includes( + browserType + ); public readonly isFirefox: boolean = browserType === Browsers.Firefox; diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index 157918df874c8..8b57ecd3c8235 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -55,6 +55,7 @@ export class WebElementWrapper { private driver: WebDriver = this.webDriver.driver; private Keys = Key; public isW3CEnabled: boolean = (this.webDriver.driver as any).executor_.w3c === true; + public isChromium: boolean = [Browsers.Chrome, Browsers.ChromiumEdge].includes(this.browserType); public static create( webElement: WebElement | WebElementWrapper, @@ -63,7 +64,7 @@ export class WebElementWrapper { timeout: number, fixedHeaderHeight: number, logger: ToolingLog, - browserType: string + browserType: Browsers ): WebElementWrapper { if (webElement instanceof WebElementWrapper) { return webElement; @@ -87,7 +88,7 @@ export class WebElementWrapper { private timeout: number, private fixedHeaderHeight: number, private logger: ToolingLog, - private browserType: string + private browserType: Browsers ) {} private async _findWithCustomTimeout( @@ -243,7 +244,7 @@ export class WebElementWrapper { return this.clearValueWithKeyboard(); } await this.retryCall(async function clearValue(wrapper) { - if (wrapper.browserType === Browsers.Chrome || options.withJS) { + if (wrapper.isChromium || options.withJS) { // https://bugs.chromium.org/p/chromedriver/issues/detail?id=2702 await wrapper.driver.executeScript(`arguments[0].value=''`, wrapper._webElement); } else { @@ -275,7 +276,7 @@ export class WebElementWrapper { await delay(100); } } else { - if (this.browserType === Browsers.Chrome) { + if (this.isChromium) { // https://bugs.chromium.org/p/chromedriver/issues/detail?id=30 await this.retryCall(async function clearValueWithKeyboard(wrapper) { await wrapper.driver.executeScript(`arguments[0].select();`, wrapper._webElement); diff --git a/test/functional/services/remote/browsers.ts b/test/functional/services/remote/browsers.ts index 46d81f1737a55..aa6e364d0a09d 100644 --- a/test/functional/services/remote/browsers.ts +++ b/test/functional/services/remote/browsers.ts @@ -21,4 +21,5 @@ export enum Browsers { Chrome = 'chrome', Firefox = 'firefox', InternetExplorer = 'ie', + ChromiumEdge = 'msedge', } diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts index e571a1a7e5551..b0724488cb5db 100644 --- a/test/functional/services/remote/remote.ts +++ b/test/functional/services/remote/remote.ts @@ -64,18 +64,23 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { lifecycle, config.get('browser.logPollingMs') ); + const isW3CEnabled = (driver as any).executor_.w3c; const caps = await driver.getCapabilities(); - const browserVersion = caps.get(isW3CEnabled ? 'browserVersion' : 'version'); + const browserVersion = caps.get( + isW3CEnabled || browserType === Browsers.ChromiumEdge ? 'browserVersion' : 'version' + ); - log.info(`Remote initialized: ${caps.get('browserName')} ${browserVersion}`); + log.info( + `Remote initialized: ${caps.get( + 'browserName' + )} ${browserVersion}, w3c compliance=${isW3CEnabled}, collectingCoverage=${collectCoverage}` + ); - if (browserType === Browsers.Chrome) { + if ([Browsers.Chrome, Browsers.ChromiumEdge].includes(browserType)) { log.info( - `Chromedriver version: ${ - caps.get('chrome').chromedriverVersion - }, w3c=${isW3CEnabled}, codeCoverage=${collectCoverage}` + `${browserType}driver version: ${caps.get(browserType)[`${browserType}driverVersion`]}` ); } diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 3bf5b865aa7ba..fc0b5bbb787c8 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -31,10 +31,12 @@ import { Builder, Capabilities, By, logging, until } from 'selenium-webdriver'; import chrome from 'selenium-webdriver/chrome'; import firefox from 'selenium-webdriver/firefox'; // @ts-ignore internal modules are not typed +import edge from 'selenium-webdriver/edge'; +import { installDriver } from 'ms-chromium-edge-driver'; +// @ts-ignore internal modules are not typed import { Executor } from 'selenium-webdriver/lib/http'; // @ts-ignore internal modules are not typed import { getLogger } from 'selenium-webdriver/lib/logging'; - import { pollForLogEntry$ } from './poll_for_log_entry'; import { createStdoutSocket } from './create_stdout_stream'; import { preventParallelCalls } from './prevent_parallel_calls'; @@ -63,6 +65,7 @@ Executor.prototype.execute = preventParallelCalls( ); let attemptCounter = 0; +let edgePaths: { driverPath: string | undefined; browserPath: string | undefined }; async function attemptToCreateCommand( log: ToolingLog, browserType: Browsers, @@ -74,6 +77,46 @@ async function attemptToCreateCommand( const buildDriverInstance = async () => { switch (browserType) { + case 'msedge': { + if (edgePaths && edgePaths.browserPath && edgePaths.driverPath) { + const edgeOptions = new edge.Options(); + if (headlessBrowser === '1') { + // @ts-ignore internal modules are not typed + edgeOptions.headless(); + } + // @ts-ignore internal modules are not typed + edgeOptions.setEdgeChromium(true); + // @ts-ignore internal modules are not typed + edgeOptions.setBinaryPath(edgePaths.browserPath); + const session = await new Builder() + .forBrowser('MicrosoftEdge') + .setEdgeOptions(edgeOptions) + .setEdgeService(new edge.ServiceBuilder(edgePaths.driverPath)) + .build(); + return { + session, + consoleLog$: pollForLogEntry$( + session, + logging.Type.BROWSER, + logPollingMs, + lifecycle.cleanup.after$ + ).pipe( + takeUntil(lifecycle.cleanup.after$), + map(({ message, level: { name: level } }) => ({ + message: message.replace(/\\n/g, '\n'), + level, + })) + ), + }; + } else { + throw new Error( + `Chromium Edge session requires browser or driver path to be defined: ${JSON.stringify( + edgePaths + )}` + ); + } + } + case 'chrome': { const chromeCapabilities = Capabilities.chrome(); const chromeOptions = [ @@ -265,6 +308,11 @@ export async function initWebDriver( log.verbose(entry.message); }); + // download Edge driver only in case of usage + if (browserType === Browsers.ChromiumEdge) { + edgePaths = await installDriver(); + } + return await Promise.race([ (async () => { await delay(2 * MINUTE); diff --git a/x-pack/test/functional/config.edge.js b/x-pack/test/functional/config.edge.js new file mode 100644 index 0000000000000..882fb6fea3686 --- /dev/null +++ b/x-pack/test/functional/config.edge.js @@ -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. + */ + +export default async function({ readConfigFile }) { + const chromeConfig = await readConfigFile(require.resolve('./config')); + + return { + ...chromeConfig.getAll(), + + browser: { + type: 'msedge', + }, + + junit: { + reportName: 'MS Chromium Edge XPack UI Functional Tests', + }, + }; +} diff --git a/yarn.lock b/yarn.lock index 77ab69c715573..3f04b2d26a013 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2499,6 +2499,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@sindresorhus/is@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-2.1.0.tgz#6ad4ca610f696098e92954ab431ff83bea0ce13f" + integrity sha512-lXKXfypKo644k4Da4yXkPCrwcvn6SlUW2X2zFbuflKHNjf0w9htru01bo26uMhleMXsDmnZ12eJLdrAZa9MANg== + "@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.6.0.tgz#ec7670432ae9c8eb710400d112c201a362d83393" @@ -3398,6 +3403,13 @@ "@svgr/plugin-svgo" "^4.2.0" loader-utils "^1.2.3" +"@szmarczak/http-timer@^4.0.0": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" + integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== + dependencies: + defer-to-connect "^2.0.0" + "@testim/chrome-version@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.0.7.tgz#0cd915785ec4190f08a3a6acc9b61fc38fb5f1a9" @@ -3646,6 +3658,16 @@ resolved "https://registry.yarnpkg.com/@types/browserslist-useragent/-/browserslist-useragent-3.0.0.tgz#d425c9818182ce71ce53866798cee9c7d41d6e53" integrity sha512-ZBvKzg3yyWNYEkwxAzdmUzp27sFvw+1m080/+2lwrt+eltNefn1f4fnpMyrjOla31p8zLleCYqQXw+3EETfn0w== +"@types/cacheable-request@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" + integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "*" + "@types/node" "*" + "@types/responselike" "*" + "@types/caseless@*": version "0.12.2" resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" @@ -4015,6 +4037,11 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" +"@types/http-cache-semantics@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" + integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== + "@types/indent-string@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/indent-string/-/indent-string-3.0.0.tgz#9ebb391ceda548926f5819ad16405349641b999f" @@ -4146,6 +4173,13 @@ dependencies: "@types/node" "*" +"@types/keyv@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" + integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== + dependencies: + "@types/node" "*" + "@types/license-checker@15.0.0": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/license-checker/-/license-checker-15.0.0.tgz#685d69e2cf61ffd862320434601f51c85e28bba1" @@ -4617,6 +4651,13 @@ "@types/tough-cookie" "*" form-data "^2.5.0" +"@types/responselike@*": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + "@types/retry@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" @@ -4632,10 +4673,10 @@ resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f" integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA== -"@types/selenium-webdriver@^4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.5.tgz#23041a4948c82daf2df9836e4d2358fec10d3e24" - integrity sha512-ma1aL1znI3ptEbSQgbywgadrRCJouPIACSfOl/bPwu/TPNSyyE/+o9jZ6+bpDVTtIdksZuVKpq4SR1ip3DRduw== +"@types/selenium-webdriver@4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.9.tgz#12621e55b2ef8f6c98bd17fe23fa720c6cba16bd" + integrity sha512-HopIwBE7GUXsscmt/J0DhnFXLSmO04AfxT6b8HAprknwka7pqEWquWDMXxCjd+NUHK9MkCe1SDKKsMiNmCItbQ== "@types/semver@^5.5.0": version "5.5.0" @@ -7358,13 +7399,18 @@ binaryextensions@2: resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935" integrity sha512-XBaoWE9RW8pPdPQNibZsW2zh8TW6gcarXp1FZPwT8Uop8ScSNldJEWf2k9l3HeTqdrEwsOsFcq74RiJECW34yA== -bindings@^1.5.0: +bindings@1, bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== dependencies: file-uri-to-path "1.0.0" +bindings@~1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" + integrity sha1-FK1hE4EtLTfXLme0ystLtyZQXxE= + bit-twiddle@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bit-twiddle/-/bit-twiddle-1.0.2.tgz#0c6c1fabe2b23d17173d9a61b7b7093eb9e1769e" @@ -7898,7 +7944,7 @@ buffer@^5.1.0, buffer@^5.2.0: base64-js "^1.0.2" ieee754 "^1.1.4" -builtin-modules@^1.0.0: +builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= @@ -8045,6 +8091,13 @@ cache-loader@^4.1.0: neo-async "^2.6.1" schema-utils "^2.0.0" +cacheable-lookup@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-2.0.0.tgz#33b1e56f17507f5cf9bb46075112d65473fb7713" + integrity sha512-s2piO6LvA7xnL1AR03wuEdSx3BZT3tIJpZ56/lcJwzO/6DTJZlTs7X3lrvPxk6d1PlDe6PrVe2TjlUIZNFglAQ== + dependencies: + keyv "^4.0.0" + cacheable-request@^2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" @@ -8058,6 +8111,19 @@ cacheable-request@^2.1.1: normalize-url "2.0.1" responselike "1.0.2" +cacheable-request@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" + integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^2.0.0" + cachedir@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" @@ -8886,7 +8952,7 @@ clone-regexp@^1.0.0: is-regexp "^1.0.0" is-supported-regexp-flag "^1.0.0" -clone-response@1.0.2: +clone-response@1.0.2, clone-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= @@ -9150,16 +9216,16 @@ commander@4.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== +commander@^2.12.1, commander@^2.20.0, commander@^2.7.1: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== -commander@^2.20.0, commander@^2.7.1: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commander@^2.8.1: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" @@ -10575,7 +10641,7 @@ debug-fabulous@1.X: memoizee "0.4.X" object-assign "4.X" -debug@2.6.9, debug@^2.0.0, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: +debug@2, debug@2.6.9, debug@^2.0.0, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -10647,6 +10713,13 @@ decompress-response@^4.2.0: dependencies: mimic-response "^2.0.0" +decompress-response@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-5.0.0.tgz#7849396e80e3d1eba8cb2f75ef4930f76461cb0f" + integrity sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw== + dependencies: + mimic-response "^2.0.0" + decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" @@ -10798,6 +10871,11 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +defer-to-connect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1" + integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg== + define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -13239,6 +13317,17 @@ fetch-mock@^7.3.9: path-to-regexp "^2.2.1" whatwg-url "^6.5.0" +ffi@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/ffi/-/ffi-2.3.0.tgz#fa1a7b3d85c0fa8c83d96947a64b5192bc47f858" + integrity sha512-vkPA9Hf9CVuQ5HeMZykYvrZF2QNJ/iKGLkyDkisBnoOOFeFXZQhUPxBARPBIZMJVulvBI2R+jgofW03gyPpJcQ== + dependencies: + bindings "~1.2.0" + debug "2" + nan "2" + ref "1" + ref-struct "1" + figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -14716,6 +14805,27 @@ got@5.6.0: unzip-response "^1.0.0" url-parse-lax "^1.0.0" +got@^10.6.0: + version "10.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-10.6.0.tgz#ac3876261a4d8e5fc4f81186f79955ce7b0501dc" + integrity sha512-3LIdJNTdCFbbJc+h/EH0V5lpNpbJ6Bfwykk21lcQvQsEcrzdi/ltCyQehFHLzJ/ka0UMH4Slg0hkYvAZN9qUDg== + dependencies: + "@sindresorhus/is" "^2.0.0" + "@szmarczak/http-timer" "^4.0.0" + "@types/cacheable-request" "^6.0.1" + cacheable-lookup "^2.0.0" + cacheable-request "^7.0.1" + decompress-response "^5.0.0" + duplexer3 "^0.1.4" + get-stream "^5.0.0" + lowercase-keys "^2.0.0" + mimic-response "^2.1.0" + p-cancelable "^2.0.0" + p-event "^4.0.0" + responselike "^2.0.0" + to-readable-stream "^2.0.0" + type-fest "^0.10.0" + got@^3.2.0: version "3.3.1" resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca" @@ -15816,6 +15926,11 @@ http-cache-semantics@3.8.1: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -16854,6 +16969,11 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.0.0.tgz#038c31b774709641bda678b1f06a4e3227c10b3e" integrity sha512-elzyIdM7iKoFHzcrndIqjYomImhxrFRnGP3galODoII4TB9gI7mZ+FnlLQmmjf27SxHS2gKEeyhX5/+YRS6H9g== +is-generator-function@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" + integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw== + is-glob@4.0.0, is-glob@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" @@ -18124,6 +18244,11 @@ json-buffer@3.0.0: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-better-errors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz#50183cd1b2d25275de069e9e71b467ac9eab973a" @@ -18359,7 +18484,7 @@ jsx-to-string@^1.4.0: json-stringify-pretty-compact "^1.0.1" react "^0.14.0" -jszip@^3.1.5: +jszip@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.2.tgz#b143816df7e106a9597a94c77493385adca5bd1d" integrity sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA== @@ -18535,6 +18660,13 @@ keyv@3.0.0: dependencies: json-buffer "3.0.0" +keyv@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.0.tgz#2d1dab694926b2d427e4c74804a10850be44c12f" + integrity sha512-U7ioE8AimvRVLfw4LffyOIRhL2xVgmE8T22L6i0BucSnBUyv4w+I7VN/zVZwRKHOI6ZRUcdMdWHQ8KSUvGpEog== + dependencies: + json-buffer "3.0.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -19503,6 +19635,11 @@ lowercase-keys@^1.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + lowlight@~1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.9.1.tgz#ed7c3dffc36f8c1f263735c0fe0c907847c11250" @@ -20163,6 +20300,11 @@ mimic-response@^2.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.0.0.tgz#996a51c60adf12cb8a87d7fb8ef24c2f3d5ebb46" integrity sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ== +mimic-response@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + mimos@4.x.x: version "4.0.0" resolved "https://registry.yarnpkg.com/mimos/-/mimos-4.0.0.tgz#76e3d27128431cb6482fd15b20475719ad626a5a" @@ -20591,6 +20733,19 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" +ms-chromium-edge-driver@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ms-chromium-edge-driver/-/ms-chromium-edge-driver-0.2.0.tgz#0e0c6fd9fd1d1d36db97b2b3d7e9d4ba4d2de456" + integrity sha512-RkDsBPnMLjRna7q4LlvtLb+CHPei9gZapnlxm3ayWKk3Ab6HmDsz/17xG2eyqkKX5UcKeo04YlLZ345tO7OolA== + dependencies: + extract-zip "^1.6.7" + got "^10.6.0" + lodash "4.17.15" + tslint "^6.1.0" + tslint-config-prettier "^1.18.0" + util "^0.12.2" + windows-registry "^0.1.5" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -20703,7 +20858,7 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.12.1, nan@^2.13.2: +nan@2, nan@^2.12.1, nan@^2.13.2: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== @@ -21206,6 +21361,11 @@ normalize-url@^3.3.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== +normalize-url@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" + integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + now-and-later@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.0.tgz#bc61cbb456d79cb32207ce47ca05136ff2e7d6ee" @@ -21891,6 +22051,11 @@ p-cancelable@^0.4.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== +p-cancelable@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" + integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -21903,7 +22068,7 @@ p-each-series@^1.0.0: dependencies: p-reduce "^1.0.0" -p-event@^4.1.0: +p-event@^4.0.0, p-event@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.1.0.tgz#e92bb866d7e8e5b732293b1c8269d38e9982bf8e" integrity sha512-4vAd06GCsgflX4wHN1JqrMzBh/8QZ4j+rzp0cd2scXRwuBEv+QR3wrVA5aLhWDLw4y2WgDKvzWF3CCLmVM1UgA== @@ -24952,6 +25117,31 @@ redux@^4.0.5: loose-envify "^1.4.0" symbol-observable "^1.2.0" +ref-struct@1, ref-struct@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ref-struct/-/ref-struct-1.1.0.tgz#5d5ee65ad41cefc3a5c5feb40587261e479edc13" + integrity sha1-XV7mWtQc78Olxf60BYcmHkee3BM= + dependencies: + debug "2" + ref "1" + +ref-union@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ref-union/-/ref-union-1.0.1.tgz#3a2397f862f1e75171d687268f43b3f17729f120" + integrity sha1-OiOX+GLx51Fx1ocmj0Oz8Xcp8SA= + dependencies: + debug "2" + ref "1" + +ref@1, ref@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ref/-/ref-1.3.5.tgz#0e33f080cdb94a3d95312b2b3b1fd0f82044ca0f" + integrity sha512-2cBCniTtxcGUjDpvFfVpw323a83/0RLSGJJY5l5lcomZWhYpU2cuLdsvYqMixvsdLJ9+sTdzEkju8J8ZHDM2nA== + dependencies: + bindings "1" + debug "2" + nan "2" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -25691,6 +25881,13 @@ responselike@1.0.2: dependencies: lowercase-keys "^1.0.0" +responselike@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" + integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== + dependencies: + lowercase-keys "^2.0.0" + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" @@ -26249,15 +26446,14 @@ select@^1.1.2: resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= -selenium-webdriver@^4.0.0-alpha.5: - version "4.0.0-alpha.5" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.5.tgz#e4683b3dbf827d70df09a7e43bf02ebad20fa7c1" - integrity sha512-hktl3DSrhzM59yLhWzDGHIX9o56DvA+cVK7Dw6FcJR6qQ4CGzkaHeXQPcdrslkWMTeq0Ci9AmCxq0EMOvm2Rkg== +selenium-webdriver@^4.0.0-alpha.7: + version "4.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.7.tgz#e3879d8457fd7ad8e4424094b7dc0540d99e6797" + integrity sha512-D4qnTsyTr91jT8f7MfN+OwY0IlU5+5FmlO5xlgRUV6hDEV8JyYx2NerdTEqDDkNq7RZDYc4VoPALk8l578RBHw== dependencies: - jszip "^3.1.5" - rimraf "^2.6.3" + jszip "^3.2.2" + rimraf "^2.7.1" tmp "0.0.30" - xml2js "^0.4.19" selfsigned@^1.10.7: version "1.10.7" @@ -28648,6 +28844,11 @@ to-object-path@^0.3.0: dependencies: kind-of "^3.0.2" +to-readable-stream@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-2.1.0.tgz#82880316121bea662cdc226adb30addb50cb06e8" + integrity sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w== + to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" @@ -28927,6 +29128,37 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.2, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== +tslint-config-prettier@^1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37" + integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg== + +tslint@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.0.tgz#c6c611b8ba0eed1549bf5a59ba05a7732133d851" + integrity sha512-fXjYd/61vU6da04E505OZQGb2VCN2Mq3doeWcOIryuG+eqdmFUXTYVwdhnbEu2k46LNLgUYt9bI5icQze/j0bQ== + dependencies: + "@babel/code-frame" "^7.0.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^4.0.1" + glob "^7.1.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + mkdirp "^0.5.1" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.10.0" + tsutils "^2.29.0" + +tsutils@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== + dependencies: + tslib "^1.8.1" + tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -29431,6 +29663,11 @@ type-detect@^1.0.0: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" integrity sha1-diIXzAbbJY7EiQihKY6LlRIejqI= +type-fest@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.10.0.tgz#7f06b2b9fbfc581068d1341ffabd0349ceafc642" + integrity sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw== + type-fest@^0.3.0, type-fest@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" @@ -30121,6 +30358,16 @@ util@^0.11.0: dependencies: inherits "2.0.3" +util@^0.12.2: + version "0.12.2" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.2.tgz#54adb634c9e7c748707af2bf5a8c7ab640cbba2b" + integrity sha512-XE+MkWQvglYa+IOfBt5UFG93EmncEMP23UqpgDvVZVFBPxwmkK10QRp6pgU4xICPnWRf/t0zPv4noYSUq9gqUQ== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + safe-buffer "^5.1.2" + utila@^0.4.0, utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" @@ -31277,6 +31524,17 @@ window-size@^0.2.0: resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" integrity sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU= +windows-registry@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/windows-registry/-/windows-registry-0.1.5.tgz#92c25c960884b0d215e69395f52d8dfaa0ba4ad0" + integrity sha512-gMN3ets1fbdP+TApEbbX2TIfBK3MIH5+p9GMvIFS3CNLr7U0Khe5mRj/T5zvwo/pKdhJgDrCLYyaNSs7HYiBCw== + dependencies: + debug "^2.2.0" + ffi "^2.0.0" + ref "^1.2.0" + ref-struct "^1.0.2" + ref-union "^1.0.0" + windows-release@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f" @@ -31575,14 +31833,6 @@ xml-parse-from-string@^1.0.0: resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28" integrity sha1-qQKekp09vN7RafPG4oI42VpdWig= -xml2js@^0.4.19, xml2js@^0.4.5: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== - dependencies: - sax ">=0.6.0" - xmlbuilder "~9.0.1" - xml2js@^0.4.22: version "0.4.22" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.22.tgz#4fa2d846ec803237de86f30aa9b5f70b6600de02" @@ -31592,6 +31842,14 @@ xml2js@^0.4.22: util.promisify "~1.0.0" xmlbuilder "~11.0.0" +xml2js@^0.4.5: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + xml@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" From e3bd04fcb078a8fd01d315bbde68781bdd8a3cfd Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 8 Apr 2020 22:36:33 +0100 Subject: [PATCH 39/81] [Alerting] Displays warning when a permanent encryption key is missing and hides alerting UI appropriately (#62772) Removes the Security flyout and instead replaces the Alerting List, Connectors List and Alert Flyout with suitable messaging. Verifies that a permanent Encryption Key has been configured and if it hasn't displays a suitable warning in place, or along side the TLS warning, as needed. --- x-pack/plugins/alerting/common/index.ts | 1 + x-pack/plugins/alerting/server/plugin.ts | 2 +- .../alerting/server/routes/health.test.ts | 58 +++++- .../plugins/alerting/server/routes/health.ts | 8 +- .../translations/translations/ja-JP.json | 17 +- .../translations/translations/zh-CN.json | 17 +- .../alert_action_security_call_out.test.tsx | 78 ------- .../alert_action_security_call_out.tsx | 78 ------- .../application/components/health_check.scss | 13 ++ .../components/health_check.test.tsx | 131 ++++++++++++ .../application/components/health_check.tsx | 197 ++++++++++++++++++ .../prompts/empty_connectors_prompt.scss | 3 + .../prompts/empty_connectors_prompt.tsx | 55 +++++ .../components/prompts/empty_prompt.tsx | 47 +++++ .../components/security_call_out.test.tsx | 72 ------- .../components/security_call_out.tsx | 75 ------- .../public/application/home.tsx | 25 ++- .../components/actions_connectors_list.scss | 4 - .../components/actions_connectors_list.tsx | 53 +---- .../sections/alert_form/alert_add.test.tsx | 5 +- .../sections/alert_form/alert_add.tsx | 104 +++++---- .../sections/alert_form/alert_edit.test.tsx | 5 +- .../sections/alert_form/alert_edit.tsx | 134 ++++++------ .../alerts_list/components/alerts_list.tsx | 42 +--- 24 files changed, 662 insertions(+), 562 deletions(-) delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.test.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/health_check.scss create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.scss create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.test.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.tsx diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index 9d4ea69a63609..2574e73dd4f9a 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -17,6 +17,7 @@ export interface ActionGroup { export interface AlertingFrameworkHealth { isSufficientlySecure: boolean; + hasPermanentEncryptionKey: boolean; } export const BASE_ALERT_API_PATH = '/api/alert'; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 172a106226345..fdca6c0a9b503 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -190,7 +190,7 @@ export class AlertingPlugin { unmuteAllAlertRoute(router, this.licenseState); muteAlertInstanceRoute(router, this.licenseState); unmuteAlertInstanceRoute(router, this.licenseState); - healthRoute(router, this.licenseState); + healthRoute(router, this.licenseState, plugins.encryptedSavedObjects); return { registerType: alertTypeRegistry.register.bind(alertTypeRegistry), diff --git a/x-pack/plugins/alerting/server/routes/health.test.ts b/x-pack/plugins/alerting/server/routes/health.test.ts index 9efe020bc10c4..42c83a7c04deb 100644 --- a/x-pack/plugins/alerting/server/routes/health.test.ts +++ b/x-pack/plugins/alerting/server/routes/health.test.ts @@ -10,6 +10,7 @@ import { mockHandlerArguments } from './_mock_handler_arguments'; import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockLicenseState } from '../lib/license_state.mock'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; jest.mock('../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -24,7 +25,9 @@ describe('healthRoute', () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [config] = router.get.mock.calls[0]; @@ -35,7 +38,9 @@ describe('healthRoute', () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; const elasticsearch = elasticsearchServiceMock.createSetup(); @@ -58,11 +63,37 @@ describe('healthRoute', () => { `); }); + it('evaluates whether Encrypted Saved Objects is using an ephemeral encryption key', async () => { + const router: RouterMock = mockRouter.create(); + + const licenseState = mockLicenseState(); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = true; + healthRoute(router, licenseState, encryptedSavedObjects); + const [, handler] = router.get.mock.calls[0]; + + const elasticsearch = elasticsearchServiceMock.createSetup(); + elasticsearch.adminClient.callAsInternalUser.mockReturnValue(Promise.resolve({})); + + const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "hasPermanentEncryptionKey": false, + "isSufficientlySecure": true, + }, + } + `); + }); + it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; const elasticsearch = elasticsearchServiceMock.createSetup(); @@ -73,6 +104,7 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { "body": Object { + "hasPermanentEncryptionKey": true, "isSufficientlySecure": true, }, } @@ -83,7 +115,9 @@ describe('healthRoute', () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; const elasticsearch = elasticsearchServiceMock.createSetup(); @@ -94,6 +128,7 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { "body": Object { + "hasPermanentEncryptionKey": true, "isSufficientlySecure": true, }, } @@ -104,7 +139,9 @@ describe('healthRoute', () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; const elasticsearch = elasticsearchServiceMock.createSetup(); @@ -117,6 +154,7 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { "body": Object { + "hasPermanentEncryptionKey": true, "isSufficientlySecure": false, }, } @@ -127,7 +165,9 @@ describe('healthRoute', () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; const elasticsearch = elasticsearchServiceMock.createSetup(); @@ -140,6 +180,7 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { "body": Object { + "hasPermanentEncryptionKey": true, "isSufficientlySecure": false, }, } @@ -150,7 +191,9 @@ describe('healthRoute', () => { const router: RouterMock = mockRouter.create(); const licenseState = mockLicenseState(); - healthRoute(router, licenseState); + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); + encryptedSavedObjects.usingEphemeralEncryptionKey = false; + healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; const elasticsearch = elasticsearchServiceMock.createSetup(); @@ -163,6 +206,7 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { "body": Object { + "hasPermanentEncryptionKey": true, "isSufficientlySecure": true, }, } diff --git a/x-pack/plugins/alerting/server/routes/health.ts b/x-pack/plugins/alerting/server/routes/health.ts index 29c2f3c5730f4..fa2358a1f181c 100644 --- a/x-pack/plugins/alerting/server/routes/health.ts +++ b/x-pack/plugins/alerting/server/routes/health.ts @@ -14,6 +14,7 @@ import { import { LicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { AlertingFrameworkHealth } from '../types'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; interface XPackUsageSecurity { security?: { @@ -26,7 +27,11 @@ interface XPackUsageSecurity { }; } -export function healthRoute(router: IRouter, licenseState: LicenseState) { +export function healthRoute( + router: IRouter, + licenseState: LicenseState, + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { router.get( { path: '/api/alert/_health', @@ -54,6 +59,7 @@ export function healthRoute(router: IRouter, licenseState: LicenseState) { const frameworkHealth: AlertingFrameworkHealth = { isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled), + hasPermanentEncryptionKey: !encryptedSavedObjects.usingEphemeralEncryptionKey, }; return res.ok({ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 00ac5b77d00f3..e63e1c8ad2c91 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15864,8 +15864,6 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.andLabel": "AND", "xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "タイミング", "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "タイミング", - "xpack.triggersActionsUI.components.alertActionSecurityCallOut.enableTlsCta": "TLS を有効にする", - "xpack.triggersActionsUI.components.alertActionSecurityCallOut.tlsDisabledTitle": "アラート {action} を実行するには Elasticsearch と Kibana の間に TLS が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle": "メールに送信", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.addVariablePopoverButton": "変数を追加", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "サーバーからメールを送信します。", @@ -15960,9 +15958,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch": "HTTP ヘッダーを追加", "xpack.triggersActionsUI.components.deleteSelectedIdsErrorNotification.descriptionText": "{numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}を削除できませんでした", "xpack.triggersActionsUI.components.deleteSelectedIdsSuccessNotification.descriptionText": "{numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}を削除しました", - "xpack.triggersActionsUI.components.securityCallOut.enableTlsCta": "TLS を有効にする", - "xpack.triggersActionsUI.components.securityCallOut.tlsDisabledDescription": "アラートは API キー に依存し、キーを使用するには Elasticsearch と Kibana の間に TLS が必要です。", - "xpack.triggersActionsUI.components.securityCallOut.tlsDisabledTitle": "トランスポートレイヤーセキュリティを有効にする", "xpack.triggersActionsUI.connectors.breadcrumbTitle": "コネクター", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "{numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}}を削除 ", @@ -15986,8 +15981,8 @@ "xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "名前が必要です。", "xpack.triggersActionsUI.sections.actionForm.getMoreActionsTitle": "さらにアクションを表示", "xpack.triggersActionsUI.sections.actionsConnectorsList.addActionButtonLabel": "コネクターを作成", - "xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyBody": "Kibana でトリガーできるメール、Slack, Elasticsearch、およびサードパーティサービスを構成します。", - "xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyTitle": "初めてのコネクターを作成する", + "xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyBody": "Kibana でトリガーできるメール、Slack, Elasticsearch、およびサードパーティサービスを構成します。", + "xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyTitle": "初めてのコネクターを作成する", "xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteDisabledTitle": "コネクターを削除できません", "xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteLabel": "{count} 件を削除", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDescription": "このコネクターを削除", @@ -16037,7 +16032,6 @@ "xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "アラートを作成できません。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "「{alertName}」 を保存しました", - "xpack.triggersActionsUI.sections.alertAdd.securityCalloutAction": "作成", "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "インデックスを選択してください。", "xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "閉じる", "xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", @@ -16074,7 +16068,6 @@ "xpack.triggersActionsUI.sections.alertEdit.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText": "アラートを更新できません。", "xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText": "「{alertName}」 を更新しました", - "xpack.triggersActionsUI.sections.alertEdit.securityCalloutAction": "編集中", "xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel": "削除", "xpack.triggersActionsUI.sections.alertForm.actionDisabledTitle": "このアクションは無効です", "xpack.triggersActionsUI.sections.alertForm.actionIdLabel": "{connectorInstance} コネクター", @@ -16126,9 +16119,9 @@ "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle": "有効にする", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "ミュート", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "アクション", - "xpack.triggersActionsUI.sections.alertsList.emptyButton": "アラートの作成", - "xpack.triggersActionsUI.sections.alertsList.emptyDesc": "トリガーが起きたときにメール、Slack、または別のコネクターを通してアラートを受信します。", - "xpack.triggersActionsUI.sections.alertsList.emptyTitle": "初めてのアラートを作成する", + "xpack.triggersActionsUI.components.emptyPrompt.emptyButton": "アラートの作成", + "xpack.triggersActionsUI.components.emptyPrompt.emptyDesc": "トリガーが起きたときにメール、Slack、または別のコネクターを通してアラートを受信します。", + "xpack.triggersActionsUI.components.emptyPrompt.emptyTitle": "初めてのアラートを作成する", "xpack.triggersActionsUI.sections.alertsList.multipleTitle": "アラート", "xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "検索", "xpack.triggersActionsUI.sections.alertsList.singleTitle": "アラート", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f6d84431bef7f..cc75ceb988d97 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15868,8 +15868,6 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.andLabel": "且", "xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "当", "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "当", - "xpack.triggersActionsUI.components.alertActionSecurityCallOut.enableTlsCta": "启用 TLS", - "xpack.triggersActionsUI.components.alertActionSecurityCallOut.tlsDisabledTitle": "告警 {action} 在 Elasticsearch 和 Kibana 之间需要 TLS。", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle": "发送到电子邮件", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.addVariablePopoverButton": "添加变量", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "从您的服务器发送电子邮件。", @@ -15964,9 +15962,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch": "添加 HTTP 标头", "xpack.triggersActionsUI.components.deleteSelectedIdsErrorNotification.descriptionText": "无法删除 {numErrors, number} 个{numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}", "xpack.triggersActionsUI.components.deleteSelectedIdsSuccessNotification.descriptionText": "已删除 {numSuccesses, number} 个{numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}", - "xpack.triggersActionsUI.components.securityCallOut.enableTlsCta": "启用 TLS", - "xpack.triggersActionsUI.components.securityCallOut.tlsDisabledDescription": "Alerting 依赖于在 Elasticsearch 和 Kibana 之间需要 TLS 的 API 密钥。", - "xpack.triggersActionsUI.components.securityCallOut.tlsDisabledTitle": "启用传输层安全", "xpack.triggersActionsUI.connectors.breadcrumbTitle": "连接器", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "取消", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "删除{numIdsToDelete, plural, one {{singleTitle}} other { # 个{multipleTitle}}} ", @@ -15991,8 +15986,8 @@ "xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "名称必填。", "xpack.triggersActionsUI.sections.actionForm.getMoreActionsTitle": "获取更多的操作", "xpack.triggersActionsUI.sections.actionsConnectorsList.addActionButtonLabel": "创建连接器", - "xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyBody": "配置电子邮件、Slack、Elasticsearch 和 Kibana 可以触发的第三方服务。", - "xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyTitle": "创建您的首个连接器", + "xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyBody": "配置电子邮件、Slack、Elasticsearch 和 Kibana 可以触发的第三方服务。", + "xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyTitle": "创建您的首个连接器", "xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteDisabledTitle": "无法删除连接器", "xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteLabel": "删除 {count} 个", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDescription": "删除此连接器", @@ -16042,7 +16037,6 @@ "xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "无法创建告警。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "已保存“{alertName}”", - "xpack.triggersActionsUI.sections.alertAdd.securityCalloutAction": "创建", "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "选择索引。", "xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "关闭", "xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", @@ -16079,7 +16073,6 @@ "xpack.triggersActionsUI.sections.alertEdit.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText": "无法更新告警。", "xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText": "已更新“{alertName}”", - "xpack.triggersActionsUI.sections.alertEdit.securityCalloutAction": "正在编辑", "xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel": "删除", "xpack.triggersActionsUI.sections.alertForm.actionDisabledTitle": "此操作已禁用", "xpack.triggersActionsUI.sections.alertForm.actionIdLabel": "{connectorInstance} 连接器", @@ -16131,9 +16124,9 @@ "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle": "启用", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "静音", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "操作", - "xpack.triggersActionsUI.sections.alertsList.emptyButton": "创建告警", - "xpack.triggersActionsUI.sections.alertsList.emptyDesc": "触发条件满足时通过电子邮件、Slack 或其他连接器接收告警。", - "xpack.triggersActionsUI.sections.alertsList.emptyTitle": "创建您的首个告警", + "xpack.triggersActionsUI.components.emptyPrompt.emptyButton": "创建告警", + "xpack.triggersActionsUI.components.emptyPrompt.emptyDesc": "触发条件满足时通过电子邮件、Slack 或其他连接器接收告警。", + "xpack.triggersActionsUI.components.emptyPrompt.emptyTitle": "创建您的首个告警", "xpack.triggersActionsUI.sections.alertsList.multipleTitle": "告警", "xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "搜索", "xpack.triggersActionsUI.sections.alertsList.singleTitle": "告警", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.test.tsx deleted file mode 100644 index 85699cfbd750f..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.test.tsx +++ /dev/null @@ -1,78 +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, { Fragment } from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { AlertActionSecurityCallOut } from './alert_action_security_call_out'; - -import { EuiCallOut, EuiButton } from '@elastic/eui'; -import { act } from 'react-dom/test-utils'; -import { httpServiceMock } from '../../../../../../src/core/public/mocks'; - -const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' }; - -const http = httpServiceMock.createStartContract(); - -describe('alert action security call out', () => { - let useEffect: any; - - const mockUseEffect = () => { - // make react execute useEffects despite shallow rendering - useEffect.mockImplementationOnce((f: Function) => f()); - }; - - beforeEach(() => { - jest.resetAllMocks(); - useEffect = jest.spyOn(React, 'useEffect'); - mockUseEffect(); - }); - - test('renders nothing while health is loading', async () => { - http.get.mockImplementationOnce(() => new Promise(() => {})); - - let component: ShallowWrapper | undefined; - await act(async () => { - component = shallow( - - ); - }); - - expect(component?.is(Fragment)).toBeTruthy(); - expect(component?.html()).toBe(''); - }); - - test('renders nothing if keys are enabled', async () => { - http.get.mockResolvedValue({ isSufficientlySecure: true }); - - let component: ShallowWrapper | undefined; - await act(async () => { - component = shallow( - - ); - }); - - expect(component?.is(Fragment)).toBeTruthy(); - expect(component?.html()).toBe(''); - }); - - test('renders the callout if keys are disabled', async () => { - http.get.mockResolvedValue({ isSufficientlySecure: false }); - - let component: ShallowWrapper | undefined; - await act(async () => { - component = shallow( - - ); - }); - - expect(component?.find(EuiCallOut).prop('title')).toMatchInlineSnapshot( - `"Alert creation requires TLS between Elasticsearch and Kibana."` - ); - - expect(component?.find(EuiButton).prop('href')).toMatchInlineSnapshot( - `"elastic.co/guide/en/kibana/current/configuring-tls.html"` - ); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.tsx deleted file mode 100644 index f7a80202dff89..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/alert_action_security_call_out.tsx +++ /dev/null @@ -1,78 +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, { Fragment } from 'react'; -import { Option, none, some, fold, filter } from 'fp-ts/lib/Option'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { DocLinksStart, HttpSetup } from 'kibana/public'; -import { AlertingFrameworkHealth } from '../../types'; -import { health } from '../lib/alert_api'; - -interface Props { - docLinks: Pick; - action: string; - http: HttpSetup; -} - -export const AlertActionSecurityCallOut: React.FunctionComponent = ({ - http, - action, - docLinks, -}) => { - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; - - const [alertingHealth, setAlertingHealth] = React.useState>(none); - - React.useEffect(() => { - async function fetchSecurityConfigured() { - setAlertingHealth(some(await health({ http }))); - } - - fetchSecurityConfigured(); - }, [http]); - - return pipe( - alertingHealth, - filter(healthCheck => !healthCheck.isSufficientlySecure), - fold( - () => , - () => ( - - - - - - - - - ) - ) - ); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.scss b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.scss new file mode 100644 index 0000000000000..c4d12221e3a01 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.scss @@ -0,0 +1,13 @@ +@mixin padBannerWith($size) { + padding-left: $size; + padding-right: $size; +} + +.alertingHealthCheck__body { + @include padBannerWith(2 * $euiSize); +} + +.alertingFlyoutHealthCheck__body { + @include padBannerWith(2 * $euiSize); + margin-top: $euiSize; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx new file mode 100644 index 0000000000000..5156a6146f3a1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.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 from 'react'; +import { render } from '@testing-library/react'; + +import { HealthCheck } from './health_check'; + +import { act } from 'react-dom/test-utils'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; + +const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' }; + +const http = httpServiceMock.createStartContract(); + +describe('health check', () => { + test('renders spinner while health is loading', async () => { + http.get.mockImplementationOnce(() => new Promise(() => {})); + + const { queryByText, container } = render( + +

{'shouldnt render'}

+
+ ); + await act(async () => { + // wait for useEffect to run + }); + + expect(container.getElementsByClassName('euiLoadingSpinner').length).toBe(1); + expect(queryByText('shouldnt render')).not.toBeInTheDocument(); + }); + + it('renders children if keys are enabled', async () => { + http.get.mockResolvedValue({ isSufficientlySecure: true, hasPermanentEncryptionKey: true }); + + const { queryByText } = render( + +

{'should render'}

+
+ ); + await act(async () => { + // wait for useEffect to run + }); + expect(queryByText('should render')).toBeInTheDocument(); + }); + + test('renders warning if keys are disabled', async () => { + http.get.mockImplementationOnce(async () => ({ + isSufficientlySecure: false, + hasPermanentEncryptionKey: true, + })); + + const { queryAllByText } = render( + +

{'should render'}

+
+ ); + await act(async () => { + // wait for useEffect to run + }); + + const [description, action] = queryAllByText(/TLS/i); + + expect(description.textContent).toMatchInlineSnapshot( + `"Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. Learn how to enable TLS."` + ); + + expect(action.textContent).toMatchInlineSnapshot(`"Learn how to enable TLS."`); + + expect(action.getAttribute('href')).toMatchInlineSnapshot( + `"elastic.co/guide/en/kibana/current/configuring-tls.html"` + ); + }); + + test('renders warning if encryption key is ephemeral', async () => { + http.get.mockImplementationOnce(async () => ({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: false, + })); + + const { queryByText, queryByRole } = render( + +

{'should render'}

+
+ ); + await act(async () => { + // wait for useEffect to run + }); + + const description = queryByRole(/banner/i); + expect(description!.textContent).toMatchInlineSnapshot( + `"To create an alert, set a value for xpack.encrypted_saved_objects.encryptionKey in your kibana.yml file. Learn how."` + ); + + const action = queryByText(/Learn/i); + expect(action!.textContent).toMatchInlineSnapshot(`"Learn how."`); + expect(action!.getAttribute('href')).toMatchInlineSnapshot( + `"elastic.co/guide/en/kibana/current/alert-action-settings-kb.html#general-alert-action-settings"` + ); + }); + + test('renders warning if encryption key is ephemeral and keys are disabled', async () => { + http.get.mockImplementationOnce(async () => ({ + isSufficientlySecure: false, + hasPermanentEncryptionKey: false, + })); + + const { queryByText } = render( + +

{'should render'}

+
+ ); + await act(async () => { + // wait for useEffect to run + }); + + const description = queryByText(/Transport Layer Security/i); + + expect(description!.textContent).toMatchInlineSnapshot( + `"You must enable Transport Layer Security between Kibana and Elasticsearch and configure an encryption key in your kibana.yml file. Learn how"` + ); + + const action = queryByText(/Learn/i); + expect(action!.textContent).toMatchInlineSnapshot(`"Learn how"`); + expect(action!.getAttribute('href')).toMatchInlineSnapshot( + `"elastic.co/guide/en/kibana/current/alerting-getting-started.html#alerting-setup-prerequisites"` + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx new file mode 100644 index 0000000000000..c967cf5de0771 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -0,0 +1,197 @@ +/* + * 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, { Fragment } from 'react'; +import { Option, none, some, fold } from 'fp-ts/lib/Option'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiLink, EuiLoadingSpinner } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DocLinksStart, HttpSetup } from 'kibana/public'; + +import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; +import { AlertingFrameworkHealth } from '../../types'; +import { health } from '../lib/alert_api'; +import './health_check.scss'; + +interface Props { + docLinks: Pick; + http: HttpSetup; + inFlyout?: boolean; +} + +export const HealthCheck: React.FunctionComponent = ({ + docLinks, + http, + children, + inFlyout = false, +}) => { + const [alertingHealth, setAlertingHealth] = React.useState>(none); + + React.useEffect(() => { + (async function() { + setAlertingHealth(some(await health({ http }))); + })(); + }, [http]); + + const className = inFlyout ? 'alertingFlyoutHealthCheck' : 'alertingHealthCheck'; + + return pipe( + alertingHealth, + fold( + () => , + healthCheck => { + return healthCheck?.isSufficientlySecure && healthCheck?.hasPermanentEncryptionKey ? ( + {children} + ) : !healthCheck.isSufficientlySecure && !healthCheck.hasPermanentEncryptionKey ? ( + + ) : !healthCheck.hasPermanentEncryptionKey ? ( + + ) : ( + + ); + } + ) + ); +}; + +type PromptErrorProps = Pick & { + className?: string; +}; + +const TlsAndEncryptionError = ({ + docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + className, +}: PromptErrorProps) => ( + + + + } + body={ +
+

+ {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionError', { + defaultMessage: + 'You must enable Transport Layer Security between Kibana and Elasticsearch and configure an encryption key in your kibana.yml file. ', + })} + + {i18n.translate( + 'xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorAction', + { + defaultMessage: 'Learn how', + } + )} + +

+
+ } + /> +); + +const EncryptionError = ({ + docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + className, +}: PromptErrorProps) => ( + + + + } + body={ +
+

+ {i18n.translate( + 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorBeforeKey', + { + defaultMessage: 'To create an alert, set a value for ', + } + )} + {'xpack.encrypted_saved_objects.encryptionKey'} + {i18n.translate( + 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorAfterKey', + { + defaultMessage: ' in your kibana.yml file. ', + } + )} + + {i18n.translate( + 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorAction', + { + defaultMessage: 'Learn how.', + } + )} + +

+
+ } + /> +); + +const TlsError = ({ + docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + className, +}: PromptErrorProps) => ( + + + + } + body={ +
+

+ {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsError', { + defaultMessage: + 'Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. ', + })} + + {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsErrorAction', { + defaultMessage: 'Learn how to enable TLS.', + })} + +

+
+ } + /> +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.scss b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.scss new file mode 100644 index 0000000000000..fe001ce294ef4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.scss @@ -0,0 +1,3 @@ +.actEmptyConnectorsPrompt__logo + .actEmptyConnectorsPrompt__logo { + margin-left: $euiSize; +} \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx new file mode 100644 index 0000000000000..0e956ea56faa9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx @@ -0,0 +1,55 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import React, { Fragment } from 'react'; +import { EuiButton, EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiTitle } from '@elastic/eui'; +import './empty_connectors_prompt.scss'; + +export const EmptyConnectorsPrompt = ({ onCTAClicked }: { onCTAClicked: () => void }) => ( + + + + + + +

+ +

+
+
+ } + body={ +

+ +

+ } + actions={ + + + + } + /> +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx new file mode 100644 index 0000000000000..df593d587de3f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; + +export const EmptyPrompt = ({ onCTAClicked }: { onCTAClicked: () => void }) => ( + + + + } + body={ +

+ +

+ } + actions={ + + + + } + /> +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.test.tsx deleted file mode 100644 index 28bc02ec3392f..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { Fragment } from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { SecurityEnabledCallOut } from './security_call_out'; - -import { EuiCallOut, EuiButton } from '@elastic/eui'; -import { act } from 'react-dom/test-utils'; -import { httpServiceMock } from '../../../../../../src/core/public/mocks'; - -const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' }; - -const http = httpServiceMock.createStartContract(); - -describe('security call out', () => { - let useEffect: any; - - const mockUseEffect = () => { - // make react execute useEffects despite shallow rendering - useEffect.mockImplementationOnce((f: Function) => f()); - }; - - beforeEach(() => { - jest.resetAllMocks(); - useEffect = jest.spyOn(React, 'useEffect'); - mockUseEffect(); - }); - - test('renders nothing while health is loading', async () => { - http.get.mockImplementationOnce(() => new Promise(() => {})); - - let component: ShallowWrapper | undefined; - await act(async () => { - component = shallow(); - }); - - expect(component?.is(Fragment)).toBeTruthy(); - expect(component?.html()).toBe(''); - }); - - test('renders nothing if keys are enabled', async () => { - http.get.mockResolvedValue({ isSufficientlySecure: true }); - - let component: ShallowWrapper | undefined; - await act(async () => { - component = shallow(); - }); - - expect(component?.is(Fragment)).toBeTruthy(); - expect(component?.html()).toBe(''); - }); - - test('renders the callout if keys are disabled', async () => { - http.get.mockImplementationOnce(async () => ({ isSufficientlySecure: false })); - - let component: ShallowWrapper | undefined; - await act(async () => { - component = shallow(); - }); - - expect(component?.find(EuiCallOut).prop('title')).toMatchInlineSnapshot( - `"Enable Transport Layer Security"` - ); - - expect(component?.find(EuiButton).prop('href')).toMatchInlineSnapshot( - `"elastic.co/guide/en/kibana/current/configuring-tls.html"` - ); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.tsx deleted file mode 100644 index 9874a3a0697d2..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/security_call_out.tsx +++ /dev/null @@ -1,75 +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, { Fragment } from 'react'; -import { Option, none, some, fold, filter } from 'fp-ts/lib/Option'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { DocLinksStart, HttpSetup } from 'kibana/public'; - -import { AlertingFrameworkHealth } from '../../types'; -import { health } from '../lib/alert_api'; - -interface Props { - docLinks: Pick; - http: HttpSetup; -} - -export const SecurityEnabledCallOut: React.FunctionComponent = ({ docLinks, http }) => { - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; - - const [alertingHealth, setAlertingHealth] = React.useState>(none); - - React.useEffect(() => { - async function fetchSecurityConfigured() { - setAlertingHealth(some(await health({ http }))); - } - - fetchSecurityConfigured(); - }, [http]); - - return pipe( - alertingHealth, - filter(healthCheck => !healthCheck?.isSufficientlySecure), - fold( - () => , - () => ( - - -

- -

- - - -
- -
- ) - ) - ); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 7c8d798984bf2..4d0a9980f2231 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -29,8 +29,8 @@ import { hasShowActionsCapability, hasShowAlertsCapability } from './lib/capabil import { ActionsConnectorsList } from './sections/actions_connectors_list/components/actions_connectors_list'; import { AlertsList } from './sections/alerts_list/components/alerts_list'; -import { SecurityEnabledCallOut } from './components/security_call_out'; import { PLUGIN } from './constants/plugin'; +import { HealthCheck } from './components/health_check'; interface MatchParams { section: Section; @@ -88,7 +88,6 @@ export const TriggersActionsUIHome: React.FunctionComponent - @@ -142,9 +141,27 @@ export const TriggersActionsUIHome: React.FunctionComponent {canShowActions && ( - + ( + + + + )} + /> + )} + {canShowAlerts && ( + ( + + + + )} + /> )} - {canShowAlerts && } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss index 3d65b8a799b1b..70ad1cae6c1d1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss @@ -1,7 +1,3 @@ -.actConnectorsList__logo + .actConnectorsList__logo { - margin-left: $euiSize; -} - .actConnectorsList__tableRowDisabled { background-color: $euiColorLightestShade; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 81693e1d2d9d1..47e058f473946 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -10,9 +10,6 @@ import { EuiInMemoryTable, EuiSpacer, EuiButton, - EuiIcon, - EuiEmptyPrompt, - EuiTitle, EuiLink, EuiLoadingSpinner, EuiIconTip, @@ -30,6 +27,7 @@ import { ActionsConnectorsContextProvider } from '../../../context/actions_conne import { checkActionTypeEnabled } from '../../../lib/check_action_type_enabled'; import './actions_connectors_list.scss'; import { ActionConnector, ActionConnectorTableItem, ActionTypeIndex } from '../../../../types'; +import { EmptyConnectorsPrompt } from '../../../components/prompts/empty_connectors_prompt'; export const ActionsConnectorsList: React.FunctionComponent = () => { const { http, toastNotifications, capabilities, actionTypeRegistry } = useAppDependencies(); @@ -324,51 +322,6 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { /> ); - const emptyPrompt = ( - - - - - - -

- -

-
-
- } - body={ -

- -

- } - actions={ - setAddFlyoutVisibility(true)} - > - - - } - /> - ); - const noPermissionPrompt = (

{ )} {data.length !== 0 && table} - {data.length === 0 && canSave && !isLoadingActions && !isLoadingActionTypes && emptyPrompt} + {data.length === 0 && canSave && !isLoadingActions && !isLoadingActionTypes && ( + setAddFlyoutVisibility(true)} /> + )} {data.length === 0 && !canSave && noPermissionPrompt} { docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; - mockes.http.get.mockResolvedValue({ isSufficientlySecure: true }); + mockes.http.get.mockResolvedValue({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + }); const alertType = { id: 'my-alert-type', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index e025248fad52e..0620ced6365a9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -25,7 +25,7 @@ import { Alert, AlertAction, IErrorObject } from '../../../types'; import { AlertForm, validateBaseProperties } from './alert_form'; import { alertReducer } from './alert_reducer'; import { createAlert } from '../../lib/alert_api'; -import { AlertActionSecurityCallOut } from '../../components/alert_action_security_call_out'; +import { HealthCheck } from '../../components/health_check'; import { PLUGIN } from '../../constants/plugin'; interface AlertAddProps { @@ -154,62 +154,54 @@ export const AlertAdd = ({

- - - - - - - - - {i18n.translate('xpack.triggersActionsUI.sections.alertAdd.cancelButtonLabel', { - defaultMessage: 'Cancel', - })} - - - - { - setIsSaving(true); - const savedAlert = await onSaveAlert(); - setIsSaving(false); - if (savedAlert) { - closeFlyout(); - if (reloadAlerts) { - reloadAlerts(); + + + + + + + + + {i18n.translate('xpack.triggersActionsUI.sections.alertAdd.cancelButtonLabel', { + defaultMessage: 'Cancel', + })} + + + + { + setIsSaving(true); + const savedAlert = await onSaveAlert(); + setIsSaving(false); + if (savedAlert) { + closeFlyout(); + if (reloadAlerts) { + reloadAlerts(); + } } - } - }} - > - - - - - + }} + > + + + + + + ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index 6fcfb463c4c77..916ba368e0732 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -36,7 +36,10 @@ describe('alert_edit', () => { docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; - mockedCoreSetup.http.get.mockResolvedValue({ isSufficientlySecure: true }); + mockedCoreSetup.http.get.mockResolvedValue({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + }); const alertType = { id: 'my-alert-type', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 3f27a7860bafa..4255eca83be47 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -26,7 +26,7 @@ import { Alert, AlertAction, IErrorObject } from '../../../types'; import { AlertForm, validateBaseProperties } from './alert_form'; import { alertReducer } from './alert_reducer'; import { updateAlert } from '../../lib/alert_api'; -import { AlertActionSecurityCallOut } from '../../components/alert_action_security_call_out'; +import { HealthCheck } from '../../components/health_check'; import { PLUGIN } from '../../constants/plugin'; interface AlertEditProps { @@ -137,77 +137,69 @@ export const AlertEdit = ({ - - - {hasActionsDisabled && ( - - - - - )} - - - - - - - {i18n.translate('xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel', { - defaultMessage: 'Cancel', - })} - - - - { - setIsSaving(true); - const savedAlert = await onSaveAlert(); - setIsSaving(false); - if (savedAlert) { - closeFlyout(); - if (reloadAlerts) { - reloadAlerts(); - } - } - }} - > - + + {hasActionsDisabled && ( + + - - - - + + + )} + + + + + + + {i18n.translate('xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel', { + defaultMessage: 'Cancel', + })} + + + + { + setIsSaving(true); + const savedAlert = await onSaveAlert(); + setIsSaving(false); + if (savedAlert) { + closeFlyout(); + if (reloadAlerts) { + reloadAlerts(); + } + } + }} + > + + + + + + ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index afd3299f0c2bb..5d59180ff572b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -15,7 +15,6 @@ import { EuiFlexItem, EuiIcon, EuiSpacer, - EuiEmptyPrompt, EuiLink, EuiLoadingSpinner, } from '@elastic/eui'; @@ -36,6 +35,7 @@ import { loadActionTypes } from '../../../lib/action_connector_api'; import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; import { routeToAlertDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; +import { EmptyPrompt } from '../../../components/prompts/empty_prompt'; const ENTER_KEY = 13; @@ -292,44 +292,6 @@ export const AlertsList: React.FunctionComponent = () => { ); } - const emptyPrompt = ( - - - - } - body={ -

- -

- } - actions={ - setAlertFlyoutVisibility(true)} - > - - - } - /> - ); - const table = ( @@ -473,7 +435,7 @@ export const AlertsList: React.FunctionComponent = () => { ) : ( - emptyPrompt + setAlertFlyoutVisibility(true)} /> )} Date: Thu, 9 Apr 2020 00:56:52 +0300 Subject: [PATCH 40/81] [APM] Agent remote configuration: changes in Java property descriptions (#62282) * [APM] Agent remote configuration: changes in Java property descriptions * Removing newlines * Update snapshot Co-authored-by: Elastic Machine Co-authored-by: Brandon Morelli Co-authored-by: Nathan L Smith --- .../__snapshots__/index.test.ts.snap | 5 +++-- .../setting_definitions/java_settings.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap index bc435179762a2..49840d2157af7 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap @@ -153,8 +153,9 @@ Array [ }, Object { "key": "stress_monitor_gc_stress_threshold", - "type": "boolean", - "validationName": "(\\"true\\" | \\"false\\")", + "type": "float", + "validationError": "Must be a number between 0.000 and 1", + "validationName": "numberFloatRt", }, Object { "key": "stress_monitor_system_cpu_relief_threshold", diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts index bb050076b9f9a..2e10c74378549 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts @@ -20,7 +20,7 @@ export const javaSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.enableLogCorrelation.description', { defaultMessage: - "A boolean specifying if the agent should integrate into SLF4J's MDC to enable trace-log correlation. If set to `true`, the agent will set the `trace.id` and `transaction.id` for the currently active spans and transactions to the MDC. While it's allowed to enable this setting at runtime, you can't disable it without a restart." + "A boolean specifying if the agent should integrate into SLF4J's MDC to enable trace-log correlation. If set to `true`, the agent will set the `trace.id` and `transaction.id` for the currently active spans and transactions to the MDC. Since Java agent version 1.16.0, the agent also adds `error.id` of captured error to the MDC just before the error message is logged. NOTE: While it's allowed to enable this setting at runtime, you can't disable it without a restart." } ), includeAgents: ['java'] @@ -41,7 +41,7 @@ export const javaSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.circuitBreakerEnabled.description', { defaultMessage: - 'A boolean specifying whether the circuit breaker should be enabled or not. When enabled, the agent periodically polls stress monitors to detect system/process/JVM stress state. If ANY of the monitors detects a stress indication, the agent will become inactive, as if the `active` configuration option has been set to `false`, thus reducing resource consumption to a minimum. When inactive, the agent continues polling the same monitors in order to detect whether the stress state has been relieved. If ALL monitors approve that the system/process/JVM is not under stress anymore, the agent will resume and become fully functional.' + 'A boolean specifying whether the circuit breaker should be enabled or not. When enabled, the agent periodically polls stress monitors to detect system/process/JVM stress state. If ANY of the monitors detects a stress indication, the agent will pause, as if the `recording` configuration option has been set to `false`, thus reducing resource consumption to a minimum. When paused, the agent continues polling the same monitors in order to detect whether the stress state has been relieved. If ALL monitors approve that the system/process/JVM is not under stress anymore, the agent will resume and become fully functional.' } ), includeAgents: ['java'] @@ -52,7 +52,7 @@ export const javaSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.stressMonitorGcStressThreshold.label', { defaultMessage: 'Stress monitor gc stress threshold' } ), - type: 'boolean', + type: 'float', category: 'Circuit-Breaker', defaultValue: '0.95', description: i18n.translate( @@ -155,7 +155,7 @@ export const javaSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.profilingInferredSpansEnabled.description', { defaultMessage: - 'Set to `true` to make the agent create spans for method executions based on async-profiler, a sampling aka statistical profiler. Due to the nature of how sampling profilers work, the duration of the inferred spans are not exact, but only estimations. The `profiling_inferred_spans_sampling_interval` lets you fine tune the trade-off between accuracy and overhead. The inferred spans are created after a profiling session has ended. This means there is a delay between the regular and the inferred spans being visible in the UI. This feature is not available on Windows' + 'Set to `true` to make the agent create spans for method executions based on async-profiler, a sampling aka statistical profiler. Due to the nature of how sampling profilers work, the duration of the inferred spans are not exact, but only estimations. The `profiling_inferred_spans_sampling_interval` lets you fine tune the trade-off between accuracy and overhead. The inferred spans are created after a profiling session has ended. This means there is a delay between the regular and the inferred spans being visible in the UI. NOTE: This feature is not available on Windows.' } ), includeAgents: ['java'] @@ -209,7 +209,7 @@ export const javaSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.profilingInferredSpansIncludedClasses.description', { defaultMessage: - 'If set, the agent will only create inferred spans for methods which match this list. Setting a value may slightly increase performance and can reduce clutter by only creating spans for the classes you are interested in. Example: `org.example.myapp.*` This option supports the wildcard `*`, which matches zero or more characters. Examples: `/foo/*/bar/*/baz*`, `*foo*`. Matching is case insensitive by default. Prepending an element with `(?-i)` makes the matching case sensitive.' + 'If set, the agent will only create inferred spans for methods which match this list. Setting a value may slightly reduce overhead and can reduce clutter by only creating spans for the classes you are interested in. This option supports the wildcard `*`, which matches zero or more characters. Example: `org.example.myapp.*`. Matching is case insensitive by default. Prepending an element with `(?-i)` makes the matching case sensitive.' } ), includeAgents: ['java'] @@ -228,7 +228,7 @@ export const javaSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.profilingInferredSpansExcludedClasses.description', { defaultMessage: - 'Excludes classes for which no profiler-inferred spans should be created. This option supports the wildcard `*`, which matches zero or more characters. Examples: `/foo/*/bar/*/baz*`, `*foo*`. Matching is case insensitive by default. Prepending an element with `(?-i)` makes the matching case sensitive.' + 'Excludes classes for which no profiler-inferred spans should be created. This option supports the wildcard `*`, which matches zero or more characters. Matching is case insensitive by default. Prepending an element with `(?-i)` makes the matching case sensitive.' } ), includeAgents: ['java'] From 0c35762f2702c813dfb74c37bae4364eecfc95c4 Mon Sep 17 00:00:00 2001 From: Brittany Joiner Date: Wed, 8 Apr 2020 18:08:13 -0500 Subject: [PATCH 41/81] Add Error Exception Type Column (#59596) * start of error exception type * width and link * removed extra line * updated snapshot * updated snapshots * updated snapshots * Update snapshots Co-authored-by: Elastic Machine Co-authored-by: Nathan L Smith --- .../__test__/__snapshots__/List.test.tsx.snap | 287 ++++++++++++++++-- .../app/ErrorGroupOverview/List/index.tsx | 36 ++- .../elasticsearch_fieldnames.test.ts.snap | 6 + .../apm/common/elasticsearch_fieldnames.ts | 1 + .../errors/__snapshots__/queries.test.ts.snap | 2 + .../apm/server/lib/errors/get_error_groups.ts | 6 +- 6 files changed, 306 insertions(+), 32 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index 205a303bcf47b..afa0cb51cd108 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -11,6 +11,12 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` "sortable": false, "width": "96px", }, + Object { + "field": "type", + "name": "Type", + "render": [Function], + "sortable": false, + }, Object { "field": "message", "name": "Error message and culprit", @@ -142,7 +148,28 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` +
+ + Type + +
+ + List should render empty state 1`] = ` List should render empty state 1`] = ` aria-live="polite" aria-sort="descending" className="euiTableHeaderCell" - data-test-subj="tableHeaderCell_occurrenceCount_3" + data-test-subj="tableHeaderCell_occurrenceCount_4" role="columnheader" scope="col" style={ @@ -225,7 +252,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` aria-live="polite" aria-sort="none" className="euiTableHeaderCell" - data-test-subj="tableHeaderCell_latestOccurrenceAt_4" + data-test-subj="tableHeaderCell_latestOccurrenceAt_5" role="columnheader" scope="col" style={ @@ -264,7 +291,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > List should render with data 1`] = ` font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; } +.c2 { + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .c1 { max-width: 100%; white-space: nowrap; @@ -301,7 +335,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` text-overflow: ellipsis; } -.c2 { +.c3 { font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; font-size: 16px; max-width: 100%; @@ -310,7 +344,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` text-overflow: ellipsis; } -.c3 { +.c4 { font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; } @@ -324,6 +358,12 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` "sortable": false, "width": "96px", }, + Object { + "field": "type", + "name": "Type", + "render": [Function], + "sortable": false, + }, Object { "field": "message", "name": "Error message and culprit", @@ -486,7 +526,28 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` +
+ + Type + +
+ + List should render with data 1`] = ` List should render with data 1`] = ` aria-live="polite" aria-sort="descending" className="euiTableHeaderCell" - data-test-subj="tableHeaderCell_occurrenceCount_3" + data-test-subj="tableHeaderCell_occurrenceCount_4" role="columnheader" scope="col" style={ @@ -569,7 +630,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` aria-live="polite" aria-sort="none" className="euiTableHeaderCell" - data-test-subj="tableHeaderCell_latestOccurrenceAt_4" + data-test-subj="tableHeaderCell_latestOccurrenceAt_5" role="columnheader" scope="col" style={ @@ -642,6 +703,49 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
+ +
+ Type +
+ + List should render with data 1`] = ` className="" >
List should render with data 1`] = ` serviceName="opbeans-python" > List should render with data 1`] = ` onFocus={[Function]} >
@@ -812,6 +916,49 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
+ +
+ Type +
+
+ List should render with data 1`] = ` className="" >
List should render with data 1`] = ` serviceName="opbeans-python" > List should render with data 1`] = ` onFocus={[Function]} >
@@ -982,6 +1129,49 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
+ +
+ Type +
+
+ List should render with data 1`] = ` className="" >
List should render with data 1`] = ` serviceName="opbeans-python" > List should render with data 1`] = ` onFocus={[Function]} >
@@ -1152,6 +1342,49 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
+ +
+ Type +
+
+ List should render with data 1`] = ` className="" >
List should render with data 1`] = ` serviceName="opbeans-python" > List should render with data 1`] = ` onFocus={[Function]} >
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index b26833c02fe22..250b9a5d188d0 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -23,6 +23,8 @@ import { useUrlParams } from '../../../../hooks/useUrlParams'; import { ManagedTable } from '../../../shared/ManagedTable'; import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; +import { APMQueryParams } from '../../../shared/Links/url_helpers'; const GroupIdLink = styled(ErrorDetailLink)` font-family: ${fontFamilyCode}; @@ -32,6 +34,10 @@ const MessageAndCulpritCell = styled.div` ${truncate('100%')}; `; +const ErrorLink = styled(ErrorOverviewLink)` + ${truncate('100%')}; +`; + const MessageLink = styled(ErrorDetailLink)` font-family: ${fontFamilyCode}; font-size: ${fontSizes.large}; @@ -48,9 +54,8 @@ interface Props { const ErrorGroupList: React.FC = props => { const { items } = props; - const { - urlParams: { serviceName } - } = useUrlParams(); + const { urlParams } = useUrlParams(); + const { serviceName } = urlParams; if (!serviceName) { throw new Error('Service name is required'); @@ -73,6 +78,29 @@ const ErrorGroupList: React.FC = props => { ); } }, + { + name: i18n.translate('xpack.apm.errorsTable.typeColumnLabel', { + defaultMessage: 'Type' + }), + field: 'type', + sortable: false, + render: (type: string, item: ErrorGroupListAPIResponse[0]) => { + return ( + + {type} + + ); + } + }, { name: i18n.translate( 'xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel', @@ -150,7 +178,7 @@ const ErrorGroupList: React.FC = props => { ) } ], - [serviceName] + [serviceName, urlParams] ); return ( diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 5de82a9ee8788..54dd4704edfc0 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -16,6 +16,8 @@ exports[`Error ERROR_EXC_HANDLED 1`] = `undefined`; exports[`Error ERROR_EXC_MESSAGE 1`] = `undefined`; +exports[`Error ERROR_EXC_TYPE 1`] = `undefined`; + exports[`Error ERROR_GROUP_ID 1`] = `"grouping key"`; exports[`Error ERROR_LOG_LEVEL 1`] = `undefined`; @@ -144,6 +146,8 @@ exports[`Span ERROR_EXC_HANDLED 1`] = `undefined`; exports[`Span ERROR_EXC_MESSAGE 1`] = `undefined`; +exports[`Span ERROR_EXC_TYPE 1`] = `undefined`; + exports[`Span ERROR_GROUP_ID 1`] = `undefined`; exports[`Span ERROR_LOG_LEVEL 1`] = `undefined`; @@ -272,6 +276,8 @@ exports[`Transaction ERROR_EXC_HANDLED 1`] = `undefined`; exports[`Transaction ERROR_EXC_MESSAGE 1`] = `undefined`; +exports[`Transaction ERROR_EXC_TYPE 1`] = `undefined`; + exports[`Transaction ERROR_GROUP_ID 1`] = `undefined`; exports[`Transaction ERROR_LOG_LEVEL 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index bc1b346f50da7..d5c3f91eb9247 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -60,6 +60,7 @@ export const ERROR_LOG_LEVEL = 'error.log.level'; export const ERROR_LOG_MESSAGE = 'error.log.message'; export const ERROR_EXC_MESSAGE = 'error.exception.message'; // only to be used in es queries, since error.exception is now an array export const ERROR_EXC_HANDLED = 'error.exception.handled'; // only to be used in es queries, since error.exception is now an array +export const ERROR_EXC_TYPE = 'error.exception.type'; export const ERROR_PAGE_URL = 'error.page.url'; // METRICS diff --git a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap index b9ac9d5431700..982ad558dc91d 100644 --- a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap @@ -73,6 +73,7 @@ Object { "error.log.message", "error.exception.message", "error.exception.handled", + "error.exception.type", "error.culprit", "error.grouping_key", "@timestamp", @@ -148,6 +149,7 @@ Object { "error.log.message", "error.exception.message", "error.exception.handled", + "error.exception.type", "error.culprit", "error.grouping_key", "@timestamp", diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index 8ea6df5a9898a..5221d737866f4 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -8,6 +8,7 @@ import { ERROR_CULPRIT, ERROR_EXC_HANDLED, ERROR_EXC_MESSAGE, + ERROR_EXC_TYPE, ERROR_GROUP_ID, ERROR_LOG_MESSAGE } from '../../../common/elasticsearch_fieldnames'; @@ -67,6 +68,7 @@ export async function getErrorGroups({ ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, ERROR_EXC_HANDLED, + ERROR_EXC_TYPE, ERROR_CULPRIT, ERROR_GROUP_ID, '@timestamp' @@ -99,6 +101,7 @@ export async function getErrorGroups({ exception?: Array<{ handled?: boolean; message?: string; + type?: string; }>; culprit: APMError['error']['culprit']; grouping_key: APMError['error']['grouping_key']; @@ -120,7 +123,8 @@ export async function getErrorGroups({ culprit: source.error.culprit, groupId: source.error.grouping_key, latestOccurrenceAt: source['@timestamp'], - handled: source.error.exception?.[0].handled + handled: source.error.exception?.[0].handled, + type: source.error.exception?.[0].type }; }); From c643148f3613df41565a25e49b0f8c88fb17876a Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Wed, 8 Apr 2020 17:36:20 -0600 Subject: [PATCH 42/81] [SIEM][Detection Engine] Fix rule notification critical bugs ## Summary Fixes critical bugs found during testing of the rule notification. * Fixes a bug where when you turn on rules quickly such as ML rules you would see these message below. This message can also be seen when you first create a rule with an action notification. This is a race condition with how we update rules multiple times when we really should only update it once and do it before enabling a rule ``` server log [12:18:35.986] [error][alerting][alerting][plugins][plugins] Executing Alert "63b828b5-24b9-4d55-83ee-8a8201fe2d76" has resulted in Error: [security_exception] missing authentication credentials for REST request [/_security/user/_has_privileges], with { header={ WWW-Authenticate={ 0="Bearer realm=\"security\"" & 1="ApiKey" & 2="Basic realm=\"security\" charset=\"UTF-8\"" } } ``` * Fixes a bug where we were using `ruleParams.interval` when we should have been using `ruleAlertSavedObject.attributes.schedule.interval`. When changing rule notifications to run daily, weekly, etc.. you would see this exception being thrown: ``` server log [21:23:08.028] [error][alerting][alerting][plugins][plugins] Executing Alert "fedcccc0-7c69-4e2f-83f8-d8ee88ab5484" has resulted in Error: "from" or "to" was not provided to signals count query ``` * Fixes misc typing issues found * Fixes it to where we no longer make multiple DB calls but rather pass down objects we already have. * Changes the work flow to where we only update, create, or patch the alerting object once which fixes the race condition and improves the backend performance. * Removes left over unused code * Applied https://en.wikipedia.org/wiki/Single-entry_single-exit to functions where it made sense and easier to read. ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../legacy/plugins/siem/common/constants.ts | 2 - .../notifications/add_tags.ts | 2 +- .../create_notifications.test.ts | 2 - .../notifications/create_notifications.ts | 5 +- .../rules_notification_alert_type.ts | 4 +- .../detection_engine/notifications/types.ts | 9 ++-- .../update_notifications.test.ts | 9 +--- .../notifications/update_notifications.ts | 27 ++++------ .../routes/rules/create_rules_bulk_route.ts | 1 + .../routes/rules/create_rules_route.ts | 1 + .../routes/rules/import_rules_route.ts | 1 + .../routes/rules/update_rules_bulk_route.ts | 1 + .../routes/rules/update_rules_route.ts | 1 + .../create_rule_actions_saved_object.ts | 8 +-- .../delete_rule_actions_saved_object.ts | 9 ++-- .../get_rule_actions_saved_object.ts | 16 +++--- ...ate_or_create_rule_actions_saved_object.ts | 11 ++-- .../update_rule_actions_saved_object.ts | 15 ++---- .../rules/create_rules.test.ts | 1 + .../detection_engine/rules/create_rules.ts | 4 +- .../rules/install_prepacked_rules.ts | 1 + .../lib/detection_engine/rules/patch_rules.ts | 2 +- .../lib/detection_engine/rules/types.ts | 6 +-- .../rules/update_rule_actions.ts | 54 ------------------- .../rules/update_rules.test.ts | 3 ++ .../detection_engine/rules/update_rules.ts | 6 ++- .../rules/update_rules_notifications.ts | 9 +--- .../lib/detection_engine/signals/types.ts | 7 ++- 28 files changed, 75 insertions(+), 142 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 662fb8fb8ef68..22f1b3beffa35 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -65,8 +65,6 @@ export const INTERNAL_IDENTIFIER = '__internal'; export const INTERNAL_RULE_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_id`; export const INTERNAL_RULE_ALERT_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_alert_id`; export const INTERNAL_IMMUTABLE_KEY = `${INTERNAL_IDENTIFIER}_immutable`; -export const INTERNAL_NOTIFICATION_ID_KEY = `${INTERNAL_IDENTIFIER}_notification_id`; -export const INTERNAL_NOTIFICATION_RULE_ID_KEY = `${INTERNAL_IDENTIFIER}_notification_rule_id`; /** * Detection engine routes diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/add_tags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/add_tags.ts index 6955e57d099be..14b2e1ae9e366 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/add_tags.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/add_tags.ts @@ -6,5 +6,5 @@ import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants'; -export const addTags = (tags: string[] = [], ruleAlertId: string): string[] => +export const addTags = (tags: string[], ruleAlertId: string): string[] => Array.from(new Set([...tags, `${INTERNAL_RULE_ALERT_ID_KEY}:${ruleAlertId}`])); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts index 073251b68f414..3878f5dae8889 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts @@ -24,7 +24,6 @@ describe('createNotifications', () => { enabled: true, interval: '', name: '', - tags: [], }); expect(alertsClient.create).toHaveBeenCalledWith( @@ -52,7 +51,6 @@ describe('createNotifications', () => { enabled: true, interval: '', name: '', - tags: [], }); expect(alertsClient.create).toHaveBeenCalledWith( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts index 3a1697f1c8afc..ccd7576255d83 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts @@ -17,12 +17,11 @@ export const createNotifications = async ({ ruleAlertId, interval, name, - tags, }: CreateNotificationParams): Promise => alertsClient.create({ data: { name, - tags: addTags(tags, ruleAlertId), + tags: addTags([], ruleAlertId), alertTypeId: NOTIFICATIONS_ID, consumer: APP_ID, params: { @@ -30,7 +29,7 @@ export const createNotifications = async ({ }, schedule: { interval }, enabled, - actions: actions?.map(transformRuleToAlertAction), + actions: actions.map(transformRuleToAlertAction), throttle: null, }, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index ced81098c9f8e..e4ad53de742d6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -45,7 +45,9 @@ export const rulesNotificationAlertType = ({ const ruleParams = { ...ruleAlertParams, name: ruleName, id: ruleAlertSavedObject.id }; const fromInMs = parseScheduleDates( - previousStartedAt ? previousStartedAt.toISOString() : `now-${ruleParams.interval}` + previousStartedAt + ? previousStartedAt.toISOString() + : `now-${ruleAlertSavedObject.attributes.schedule.interval}` )?.format('x'); const toInMs = parseScheduleDates(startedAt.toISOString())?.format('x'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts index 128a7965cd7dc..32a8737adc7c9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts @@ -45,10 +45,11 @@ export interface Clients { alertsClient: AlertsClient; } -export type UpdateNotificationParams = Omit & { +export type UpdateNotificationParams = Omit< + NotificationAlertParams, + 'interval' | 'actions' | 'tags' +> & { actions: RuleAlertAction[]; - id?: string; - tags?: string[]; interval: string | null | undefined; ruleAlertId: string; } & Clients; @@ -64,8 +65,6 @@ export interface NotificationAlertParams { ruleAlertId: string; interval: string; name: string; - tags?: string[]; - throttle?: null; } export type CreateNotificationParams = NotificationAlertParams & Clients; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts index 4c077dd9fc1fb..e1f7526438c31 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts @@ -9,6 +9,7 @@ import { updateNotifications } from './update_notifications'; import { readNotifications } from './read_notifications'; import { createNotifications } from './create_notifications'; import { getNotificationResult } from '../routes/__mocks__/request_responses'; +import { UpdateNotificationParams } from './types'; jest.mock('./read_notifications'); jest.mock('./create_notifications'); @@ -30,7 +31,6 @@ describe('updateNotifications', () => { enabled: true, interval: '10m', name: '', - tags: [], }); expect(alertsClient.update).toHaveBeenCalledWith( @@ -48,14 +48,13 @@ describe('updateNotifications', () => { it('should create a new notification if did not exist', async () => { (readNotifications as jest.Mock).mockResolvedValue(null); - const params = { + const params: UpdateNotificationParams = { alertsClient, actions: [], ruleAlertId: 'new-rule-id', enabled: true, interval: '10m', name: '', - tags: [], }; await updateNotifications(params); @@ -73,7 +72,6 @@ describe('updateNotifications', () => { enabled: true, interval: null, name: '', - tags: [], }); expect(alertsClient.delete).toHaveBeenCalledWith( @@ -98,7 +96,6 @@ describe('updateNotifications', () => { enabled: true, interval: '10m', name: '', - tags: [], }); expect(alertsClient.update).toHaveBeenCalledWith( @@ -125,10 +122,8 @@ describe('updateNotifications', () => { alertsClient, actions: [], enabled: true, - id: notification.id, ruleAlertId, name: notification.name, - tags: notification.tags, interval: null, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts index 3197d21c0e95a..ac0de406aceb2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts @@ -15,50 +15,41 @@ export const updateNotifications = async ({ alertsClient, actions, enabled, - id, ruleAlertId, name, - tags, interval, }: UpdateNotificationParams): Promise => { - const notification = await readNotifications({ alertsClient, id, ruleAlertId }); + const notification = await readNotifications({ alertsClient, id: undefined, ruleAlertId }); if (interval && notification) { - const result = await alertsClient.update({ + return alertsClient.update({ id: notification.id, data: { - tags: addTags(tags, ruleAlertId), + tags: addTags([], ruleAlertId), name, schedule: { interval, }, - actions: actions?.map(transformRuleToAlertAction), + actions: actions.map(transformRuleToAlertAction), params: { ruleAlertId, }, throttle: null, }, }); - return result; - } - - if (interval && !notification) { - const result = await createNotifications({ + } else if (interval && !notification) { + return createNotifications({ alertsClient, enabled, - tags, name, interval, actions, ruleAlertId, }); - return result; - } - - if (!interval && notification) { + } else if (!interval && notification) { await alertsClient.delete({ id: notification.id }); return null; + } else { + return null; } - - return null; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index d0e36515946a8..5377e9039785e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -144,6 +144,7 @@ export const createRulesBulkRoute = (router: IRouter) => { note, version, lists, + actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is set to rule, otherwise we are a notification and should not enable it, }); const ruleActions = await updateRulesNotifications({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 6038ad2095323..9a329b78b8f12 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -132,6 +132,7 @@ export const createRulesRoute = (router: IRouter): void => { note, version: 1, lists, + actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it, }); const ruleActions = await updateRulesNotifications({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 43e970702ba72..29ae5056a3ae8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -196,6 +196,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config note, version, lists, + actions: [], // Actions are not imported nor exported at this time }); resolve({ rule_id: ruleId, status_code: 200 }); } else if (rule != null && request.query.overwrite) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 9916972f41843..36e15780f5cb3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -122,6 +122,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { note, version, lists, + actions, }); if (rule != null) { const ruleActions = await updateRulesNotifications({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 21dd2a4429cca..0444c757a9b31 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -118,6 +118,7 @@ export const updateRulesRoute = (router: IRouter) => { note, version, lists, + actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it }); if (rule != null) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts index 97cfc1d2d9ea7..991690d901d8a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts @@ -9,6 +9,7 @@ import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; import { getThrottleOptions, getRuleActionsFromSavedObject } from './utils'; +import { RulesActionsSavedObject } from './get_rule_actions_saved_object'; interface CreateRuleActionsSavedObject { ruleAlertId: string; @@ -22,12 +23,7 @@ export const createRuleActionsSavedObject = async ({ savedObjectsClient, actions = [], throttle, -}: CreateRuleActionsSavedObject): Promise<{ - id: string; - actions: RuleAlertAction[]; - alertThrottle: string | null; - ruleThrottle: string; -}> => { +}: CreateRuleActionsSavedObject): Promise => { const ruleActionsSavedObject = await savedObjectsClient.create< IRuleActionsAttributesSavedObjectAttributes >(ruleActionsSavedObjectType, { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts index 864281da5bafd..91489334940bd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts @@ -18,8 +18,9 @@ export const deleteRuleActionsSavedObject = async ({ savedObjectsClient, }: DeleteRuleActionsSavedObject): Promise<{} | null> => { const ruleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); - - if (!ruleActions) return null; - - return savedObjectsClient.delete(ruleActionsSavedObjectType, ruleActions.id); + if (ruleActions != null) { + return savedObjectsClient.delete(ruleActionsSavedObjectType, ruleActions.id); + } else { + return null; + } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts index 61b544db5a18a..dad35f6cb1f96 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts @@ -15,15 +15,17 @@ interface GetRuleActionsSavedObject { savedObjectsClient: AlertServices['savedObjectsClient']; } -export const getRuleActionsSavedObject = async ({ - ruleAlertId, - savedObjectsClient, -}: GetRuleActionsSavedObject): Promise<{ +export interface RulesActionsSavedObject { id: string; actions: RuleAlertAction[]; alertThrottle: string | null; ruleThrottle: string; -} | null> => { +} + +export const getRuleActionsSavedObject = async ({ + ruleAlertId, + savedObjectsClient, +}: GetRuleActionsSavedObject): Promise => { const { saved_objects } = await savedObjectsClient.find< IRuleActionsAttributesSavedObjectAttributes >({ @@ -35,7 +37,7 @@ export const getRuleActionsSavedObject = async ({ if (!saved_objects[0]) { return null; + } else { + return getRuleActionsFromSavedObject(saved_objects[0]); } - - return getRuleActionsFromSavedObject(saved_objects[0]); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts index adc87150f89a7..d79c61f6200e3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts @@ -24,16 +24,17 @@ export const updateOrCreateRuleActionsSavedObject = async ({ actions, throttle, }: UpdateOrCreateRuleActionsSavedObject): Promise => { - const currentRuleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); + const ruleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); - if (currentRuleActions) { + if (ruleActions != null) { return updateRuleActionsSavedObject({ ruleAlertId, savedObjectsClient, actions, throttle, - }) as Promise; + ruleActions, + }); + } else { + return createRuleActionsSavedObject({ ruleAlertId, savedObjectsClient, actions, throttle }); } - - return createRuleActionsSavedObject({ ruleAlertId, savedObjectsClient, actions, throttle }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts index a15005110c56b..2a2c84838ed93 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts @@ -6,7 +6,7 @@ import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; -import { getRuleActionsSavedObject } from './get_rule_actions_saved_object'; +import { RulesActionsSavedObject } from './get_rule_actions_saved_object'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { getThrottleOptions } from './utils'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; @@ -16,6 +16,7 @@ interface DeleteRuleActionsSavedObject { savedObjectsClient: AlertServices['savedObjectsClient']; actions: RuleAlertAction[] | undefined; throttle: string | null | undefined; + ruleActions: RulesActionsSavedObject; } export const updateRuleActionsSavedObject = async ({ @@ -23,16 +24,8 @@ export const updateRuleActionsSavedObject = async ({ savedObjectsClient, actions, throttle, -}: DeleteRuleActionsSavedObject): Promise<{ - ruleThrottle: string; - alertThrottle: string | null; - actions: RuleAlertAction[]; - id: string; -} | null> => { - const ruleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); - - if (!ruleActions) return null; - + ruleActions, +}: DeleteRuleActionsSavedObject): Promise => { const throttleOptions = throttle ? getThrottleOptions(throttle) : { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts index 4c8d0f51f251b..a60f1d4177978 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts @@ -34,6 +34,7 @@ describe('createRules', () => { interval: '', name: '', tags: [], + actions: [], }); expect(alertsClient.create).toHaveBeenCalledWith( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index bebf4f350483b..91effb4741b8b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { Alert } from '../../../../../../../plugins/alerting/common'; import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { CreateRuleParams } from './types'; @@ -42,6 +43,7 @@ export const createRules = async ({ note, version, lists, + actions, }: CreateRuleParams): Promise => { // TODO: Remove this and use regular lists once the feature is stable for a release const listsParam = hasListsFeature() ? { lists } : {}; @@ -81,7 +83,7 @@ export const createRules = async ({ }, schedule: { interval }, enabled, - actions: [], + actions: actions.map(transformRuleToAlertAction), throttle: null, }, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index bcbe460fb6a66..6d4bacb9cc243 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -83,6 +83,7 @@ export const installPrepackagedRules = ( note, version, lists, + actions: [], // At this time there is no pre-packaged actions }), ]; }, []); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index d7655a15499eb..5c4889ec5fd68 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -120,7 +120,7 @@ export const patchRules = async ({ id: rule.id, data: { tags: addTags(tags ?? rule.tags, rule.params.ruleId, immutable ?? rule.params.immutable), - throttle: rule.throttle, + throttle: null, name: calculateName({ updatedName: name, originalName: rule.name }), schedule: { interval: calculateInterval(interval, rule.schedule.interval), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index 38b1097a845f8..b1bed5d716155 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -142,12 +142,12 @@ export interface Clients { actionsClient: ActionsClient; } -export type PatchRuleParams = Partial> & { +export type PatchRuleParams = Partial> & { id: string | undefined | null; savedObjectsClient: SavedObjectsClientContract; } & Clients; -export type UpdateRuleParams = Omit & { +export type UpdateRuleParams = Omit & { id: string | undefined | null; savedObjectsClient: SavedObjectsClientContract; } & Clients; @@ -157,7 +157,7 @@ export type DeleteRuleParams = Clients & { ruleId: string | undefined | null; }; -export type CreateRuleParams = Omit & { +export type CreateRuleParams = Omit & { ruleId: string; } & Clients; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts deleted file mode 100644 index e6ee1e6a29764..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.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 { - AlertsClient, - AlertServices, - PartialAlert, -} from '../../../../../../../plugins/alerting/server'; -import { getRuleActionsSavedObject } from '../rule_actions/get_rule_actions_saved_object'; -import { readRules } from './read_rules'; -import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; - -interface UpdateRuleActions { - alertsClient: AlertsClient; - savedObjectsClient: AlertServices['savedObjectsClient']; - ruleAlertId: string; -} - -export const updateRuleActions = async ({ - alertsClient, - savedObjectsClient, - ruleAlertId, -}: UpdateRuleActions): Promise => { - const rule = await readRules({ alertsClient, id: ruleAlertId }); - if (rule == null) { - return null; - } - - const ruleActions = await getRuleActionsSavedObject({ - savedObjectsClient, - ruleAlertId, - }); - - if (!ruleActions) { - return null; - } - - return alertsClient.update({ - id: ruleAlertId, - data: { - actions: !ruleActions.alertThrottle - ? ruleActions.actions.map(transformRuleToAlertAction) - : [], - throttle: null, - name: rule.name, - tags: rule.tags, - schedule: rule.schedule, - params: rule.params, - }, - }); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts index ca299db6ace50..72f4cbcbe68e8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts @@ -35,6 +35,7 @@ describe('updateRules', () => { interval: '', name: '', tags: [], + actions: [], }); expect(alertsClient.disable).toHaveBeenCalledWith( @@ -61,6 +62,7 @@ describe('updateRules', () => { interval: '', name: '', tags: [], + actions: [], }); expect(alertsClient.enable).toHaveBeenCalledWith( @@ -89,6 +91,7 @@ describe('updateRules', () => { interval: '', name: '', tags: [], + actions: [], }); expect(alertsClient.update).toHaveBeenCalledWith( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 0e70e05f4de78..99326768ed33b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PartialAlert } from '../../../../../../../plugins/alerting/server'; import { readRules } from './read_rules'; import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './types'; @@ -46,6 +47,7 @@ export const updateRules = async ({ lists, anomalyThreshold, machineLearningJobId, + actions, }: UpdateRuleParams): Promise => { const rule = await readRules({ alertsClient, ruleId, id }); if (rule == null) { @@ -90,8 +92,8 @@ export const updateRules = async ({ tags: addTags(tags, rule.params.ruleId, rule.params.immutable), name, schedule: { interval }, - actions: rule.actions, - throttle: rule.throttle, + actions: actions.map(transformRuleToAlertAction), + throttle: null, params: { description, ruleId: rule.params.ruleId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts index bb66a5ee1342f..994a54048b71a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts @@ -8,7 +8,6 @@ import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { AlertsClient, AlertServices } from '../../../../../../../plugins/alerting/server'; import { updateOrCreateRuleActionsSavedObject } from '../rule_actions/update_or_create_rule_actions_saved_object'; import { updateNotifications } from '../notifications/update_notifications'; -import { updateRuleActions } from './update_rule_actions'; import { RuleActions } from '../rule_actions/types'; interface UpdateRulesNotifications { @@ -37,19 +36,13 @@ export const updateRulesNotifications = async ({ throttle, }); - await updateRuleActions({ - alertsClient, - savedObjectsClient, - ruleAlertId, - }); - await updateNotifications({ alertsClient, ruleAlertId, enabled, name, actions: ruleActions.actions, - interval: ruleActions?.alertThrottle, + interval: ruleActions.alertThrottle, }); return ruleActions; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index d4469351de544..040e32aa0d360 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -162,5 +162,10 @@ export interface AlertAttributes { } export interface RuleAlertAttributes extends AlertAttributes { - params: Omit & { ruleId: string }; + params: Omit< + RuleAlertParams, + 'ruleId' | 'name' | 'enabled' | 'interval' | 'tags' | 'actions' | 'throttle' + > & { + ruleId: string; + }; } From 274cb805e1ed5138b0e0cd285aa9d420be5ce2b4 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Wed, 8 Apr 2020 19:58:50 -0400 Subject: [PATCH 43/81] =?UTF-8?q?[SIEM]=20[Detection=20Engine]=20Fixes=20b?= =?UTF-8?q?ug=20when=20notification=20doesn't=E2=80=A6=20(#63013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set refresh on bulk create to 'wait_for' when actions are present, so we do not respond until the newly indexed signals are searchable. * set refresh on bulk create to 'wait_for' when actions are present, so we do not respond until the newly indexed signals are searchable * fix types in tests --- .../signals/bulk_create_ml_signals.ts | 3 +- .../signals/search_after_bulk_create.test.ts | 8 +++++ .../signals/search_after_bulk_create.ts | 6 +++- .../signals/signal_rule_alert_type.test.ts | 32 +++++++++++++++++++ .../signals/signal_rule_alert_type.ts | 3 ++ .../signals/single_bulk_create.test.ts | 6 ++++ .../signals/single_bulk_create.ts | 6 ++-- .../siem/server/lib/detection_engine/types.ts | 2 ++ 8 files changed, 62 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index 355041d9efbdb..ba8938f116fc6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -10,7 +10,7 @@ import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../../../src/core/server'; import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams } from '../types'; +import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; import { AnomalyResults, Anomaly } from '../../machine_learning'; @@ -29,6 +29,7 @@ interface BulkCreateMlSignalsParams { updatedBy: string; interval: string; enabled: boolean; + refresh: RefreshTypes; tags: string[]; throttle: string; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 414270ffcdd5c..81600b0b8dd9b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -52,6 +52,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -126,6 +127,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -156,6 +158,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -198,6 +201,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -240,6 +244,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -284,6 +289,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -328,6 +334,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -374,6 +381,7 @@ describe('searchAfterAndBulkCreate', () => { enabled: true, pageSize: 1, filter: undefined, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index ff81730bc4a72..3a964cb91fbdb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -6,7 +6,7 @@ import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams } from '../types'; +import { RuleTypeParams, RefreshTypes } from '../types'; import { Logger } from '../../../../../../../../src/core/server'; import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; @@ -30,6 +30,7 @@ interface SearchAfterAndBulkCreateParams { enabled: boolean; pageSize: number; filter: unknown; + refresh: RefreshTypes; tags: string[]; throttle: string; } @@ -61,6 +62,7 @@ export const searchAfterAndBulkCreate = async ({ interval, enabled, pageSize, + refresh, tags, throttle, }: SearchAfterAndBulkCreateParams): Promise => { @@ -92,6 +94,7 @@ export const searchAfterAndBulkCreate = async ({ updatedBy, interval, enabled, + refresh, tags, throttle, }); @@ -179,6 +182,7 @@ export const searchAfterAndBulkCreate = async ({ updatedBy, interval, enabled, + refresh, tags, throttle, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 3d6f443ce60d6..03fb5832fdf42 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -105,6 +105,7 @@ describe('rules_notification_alert_type', () => { }; (ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService); (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(0)); + (searchAfterAndBulkCreate as jest.Mock).mockClear(); (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({ success: true, searchAfterTimes: [], @@ -149,6 +150,37 @@ describe('rules_notification_alert_type', () => { }); }); + it("should set refresh to 'wait_for' when actions are present", async () => { + const ruleAlert = getResult(); + ruleAlert.actions = [ + { + actionTypeId: '.slack', + params: { + message: + 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', + }, + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ]; + + savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + await alert.executor(payload); + expect((searchAfterAndBulkCreate as jest.Mock).mock.calls[0][0].refresh).toEqual('wait_for'); + (searchAfterAndBulkCreate as jest.Mock).mockClear(); + }); + + it('should set refresh to false when actions are not present', async () => { + await alert.executor(payload); + expect((searchAfterAndBulkCreate as jest.Mock).mock.calls[0][0].refresh).toEqual(false); + (searchAfterAndBulkCreate as jest.Mock).mockClear(); + }); + it('should call scheduleActions if signalsCount was greater than 0 and rule has actions defined', async () => { const ruleAlert = getResult(); ruleAlert.actions = [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index faac4a547fc17..0357f906f8035 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -98,6 +98,7 @@ export const signalRulesAlertType = ({ params: ruleParams, } = savedObject.attributes; const updatedAt = savedObject.updated_at ?? ''; + const refresh = actions.length ? 'wait_for' : false; const buildRuleMessage = buildRuleMessageFactory({ id: alertId, ruleId, @@ -181,6 +182,7 @@ export const signalRulesAlertType = ({ updatedAt, interval, enabled, + refresh, tags, }); result.success = success; @@ -241,6 +243,7 @@ export const signalRulesAlertType = ({ interval, enabled, pageSize: searchAfterSize, + refresh, tags, throttle, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts index 56f061cdfa3ca..45365b446cbf0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -159,6 +159,7 @@ describe('singleBulkCreate', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -192,6 +193,7 @@ describe('singleBulkCreate', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -217,6 +219,7 @@ describe('singleBulkCreate', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -243,6 +246,7 @@ describe('singleBulkCreate', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -271,6 +275,7 @@ describe('singleBulkCreate', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); @@ -365,6 +370,7 @@ describe('singleBulkCreate', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts index 6dd8823b57e4d..fc33d0e15e43f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts @@ -9,7 +9,7 @@ import { performance } from 'perf_hooks'; import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { SignalSearchResponse, BulkResponse } from './types'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams } from '../types'; +import { RuleTypeParams, RefreshTypes } from '../types'; import { generateId, makeFloatString } from './utils'; import { buildBulkBody } from './build_bulk_body'; import { Logger } from '../../../../../../../../src/core/server'; @@ -31,6 +31,7 @@ interface SingleBulkCreateParams { enabled: boolean; tags: string[]; throttle: string; + refresh: RefreshTypes; } /** @@ -77,6 +78,7 @@ export const singleBulkCreate = async ({ updatedBy, interval, enabled, + refresh, tags, throttle, }: SingleBulkCreateParams): Promise => { @@ -124,7 +126,7 @@ export const singleBulkCreate = async ({ const start = performance.now(); const response: BulkResponse = await services.callCluster('bulk', { index: signalsIndex, - refresh: false, + refresh, body: bulkBody, }); const end = performance.now(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index d3fa98fd73d3a..035f1b10ff8b2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -149,3 +149,5 @@ export type CallWithRequest, V> = ( params: T, options?: CallAPIOptions ) => Promise; + +export type RefreshTypes = false | 'wait_for'; From 82e048a5fb57de3afd309e301536a90971edd7de Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 9 Apr 2020 09:28:44 +0200 Subject: [PATCH 44/81] add embed flag to saved object url as well (#62926) --- src/plugins/share/public/components/url_panel_content.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx index 2b77b6f4592a8..2b1159be89003 100644 --- a/src/plugins/share/public/components/url_panel_content.tsx +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -166,7 +166,7 @@ export class UrlPanelContent extends Component { // Get the application route, after the hash, and remove the #. const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true); - return formatUrl({ + let formattedUrl = formatUrl({ protocol: parsedUrl.protocol, auth: parsedUrl.auth, host: parsedUrl.host, @@ -180,6 +180,11 @@ export class UrlPanelContent extends Component { }, }), }); + if (this.props.isEmbedded) { + formattedUrl = this.makeUrlEmbeddable(url); + } + + return formattedUrl; }; private getSnapshotUrl = () => { From 7b0e9d00aafa995c4a6261be7a1446362647e170 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 9 Apr 2020 11:43:51 +0200 Subject: [PATCH 45/81] [ML] Functional transform tests - stabilize source selection (#63087) This PR adds a retry to the transform source selection service method for functional tests. --- .../functional/services/transform_ui/source_selection.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/services/transform_ui/source_selection.ts b/x-pack/test/functional/services/transform_ui/source_selection.ts index d2ef2c67f0004..38a819e285d67 100644 --- a/x-pack/test/functional/services/transform_ui/source_selection.ts +++ b/x-pack/test/functional/services/transform_ui/source_selection.ts @@ -8,6 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformSourceSelectionProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); return { async assertSourceListContainsEntry(sourceName: string) { @@ -23,8 +24,10 @@ export function TransformSourceSelectionProvider({ getService }: FtrProviderCont async selectSource(sourceName: string) { await this.filterSourceSelection(sourceName); - await testSubjects.clickWhenNotDisabled(`savedObjectTitle${sourceName}`); - await testSubjects.existOrFail('transformPageCreateTransform'); + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.clickWhenNotDisabled(`savedObjectTitle${sourceName}`); + await testSubjects.existOrFail('transformPageCreateTransform', { timeout: 10 * 1000 }); + }); }, }; } From 7ec635798cd2b7d659fc8a247c1e8f96a2cec678 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Thu, 9 Apr 2020 13:39:14 +0300 Subject: [PATCH 46/81] [data.search.aggs]: Clean up TimeBuckets implementation (#62123) * Created tests for time_buckets. Clean up code. Removed getConfig method. * Fixed comments * Fixed comments (2) * Fixes * Removed __cached__ * Fixes for comments * some refactoring * Fixed comment about config Co-authored-by: Elastic Machine --- .../search/aggs/buckets/date_histogram.ts | 29 ++-- .../lib/time_buckets/time_buckets.test.ts | 121 +++++++++++++ .../buckets/lib/time_buckets/time_buckets.ts | 161 +++--------------- .../utils/calculate_auto_time_expression.ts | 13 +- .../public/legacy/build_pipeline.ts | 7 +- 5 files changed, 178 insertions(+), 153 deletions(-) create mode 100644 src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts 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 e6fd259fabc92..57f3aa85ad944 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -44,20 +44,15 @@ const updateTimeBuckets = ( timefilter: TimefilterContract, customBuckets?: IBucketDateHistogramAggConfig['buckets'] ) => { - const bounds = agg.params.timeRange ? timefilter.calculateBounds(agg.params.timeRange) : null; + const bounds = + agg.params.timeRange && agg.fieldIsTimeField() + ? timefilter.calculateBounds(agg.params.timeRange) + : undefined; const buckets = customBuckets || agg.buckets; - buckets.setBounds(agg.fieldIsTimeField() && bounds); + buckets.setBounds(bounds); buckets.setInterval(agg.params.interval); }; -// TODO: Need to incorporate these properly into TimeBuckets -interface ITimeBuckets { - setBounds: Function; - getScaledDateFormat: TimeBuckets['getScaledDateFormat']; - setInterval: Function; - getInterval: Function; -} - export interface DateHistogramBucketAggDependencies { uiSettings: IUiSettingsClient; query: QuerySetup; @@ -65,7 +60,7 @@ export interface DateHistogramBucketAggDependencies { } export interface IBucketDateHistogramAggConfig extends IBucketAggConfig { - buckets: ITimeBuckets; + buckets: TimeBuckets; } export function isDateHistogramBucketAggConfig(agg: any): agg is IBucketDateHistogramAggConfig { @@ -113,7 +108,12 @@ export const getDateHistogramBucketAgg = ({ if (buckets) return buckets; const { timefilter } = query.timefilter; - buckets = new TimeBuckets({ uiSettings }); + buckets = new TimeBuckets({ + 'histogram:maxBars': uiSettings.get('histogram:maxBars'), + 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + dateFormat: uiSettings.get('dateFormat'), + 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), + }); updateTimeBuckets(this, timefilter, buckets); return buckets; @@ -206,7 +206,8 @@ export const getDateHistogramBucketAgg = ({ ...dateHistogramInterval(interval.expression), }; - const scaleMetrics = scaleMetricValues && interval.scaled && interval.scale < 1; + const scaleMetrics = + scaleMetricValues && interval.scaled && interval.scale && interval.scale < 1; if (scaleMetrics && aggs) { const metrics = aggs.aggs.filter(a => isMetricAggType(a.type)); const all = every(metrics, (a: IBucketAggConfig) => { @@ -218,7 +219,7 @@ export const getDateHistogramBucketAgg = ({ }); if (all) { output.metricScale = interval.scale; - output.metricScaleText = interval.preScaled.description; + output.metricScaleText = interval.preScaled?.description || ''; } } }, diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts new file mode 100644 index 0000000000000..af3c15167295c --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts @@ -0,0 +1,121 @@ +/* + * 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 moment from 'moment'; + +import { TimeBuckets, TimeBucketsConfig } from './time_buckets'; + +describe('TimeBuckets', () => { + const timeBucketConfig: TimeBucketsConfig = { + 'histogram:maxBars': 4, + 'histogram:barTarget': 3, + dateFormat: 'YYYY-MM-DD', + 'dateFormat:scaled': [ + ['', 'HH:mm:ss.SSS'], + ['PT1S', 'HH:mm:ss'], + ['PT1M', 'HH:mm'], + ['PT1H', 'YYYY-MM-DD HH:mm'], + ['P1DT', 'YYYY-MM-DD'], + ['P1YT', 'YYYY'], + ], + }; + + test('setBounds/getBounds - bounds is correct', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + const bounds = { + min: moment('2020-03-25'), + max: moment('2020-03-31'), + }; + timeBuckets.setBounds(bounds); + const timeBucketsBounds = timeBuckets.getBounds(); + + expect(timeBucketsBounds).toEqual(bounds); + }); + + test('setBounds/getBounds - bounds is undefined', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + const bounds = { + min: moment('2020-03-25'), + max: moment('2020-03-31'), + }; + timeBuckets.setBounds(bounds); + let timeBucketsBounds = timeBuckets.getBounds(); + + expect(timeBucketsBounds).toEqual(bounds); + + timeBuckets.setBounds(); + timeBucketsBounds = timeBuckets.getBounds(); + + expect(timeBucketsBounds).toBeUndefined(); + }); + + test('setInterval/getInterval - intreval is a string', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + timeBuckets.setInterval('20m'); + const interval = timeBuckets.getInterval(); + + expect(interval.description).toEqual('20 minutes'); + expect(interval.esValue).toEqual(20); + expect(interval.esUnit).toEqual('m'); + expect(interval.expression).toEqual('20m'); + }); + + test('setInterval/getInterval - intreval is a string and bounds is defined', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + const bounds = { + min: moment('2020-03-25'), + max: moment('2020-03-31'), + }; + timeBuckets.setBounds(bounds); + timeBuckets.setInterval('20m'); + const interval = timeBuckets.getInterval(); + + expect(interval.description).toEqual('day'); + expect(interval.esValue).toEqual(1); + expect(interval.esUnit).toEqual('d'); + expect(interval.expression).toEqual('1d'); + expect(interval.scaled).toBeTruthy(); + expect(interval.scale).toEqual(0.013888888888888888); + + if (interval.preScaled) { + expect(interval.preScaled.description).toEqual('20 minutes'); + expect(interval.preScaled.esValue).toEqual(20); + expect(interval.preScaled.esUnit).toEqual('m'); + expect(interval.preScaled.expression).toEqual('20m'); + } + }); + + test('setInterval/getInterval - intreval is a "auto"', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + timeBuckets.setInterval('auto'); + const interval = timeBuckets.getInterval(); + + expect(interval.description).toEqual('0 milliseconds'); + expect(interval.esValue).toEqual(0); + expect(interval.esUnit).toEqual('ms'); + expect(interval.expression).toEqual('0ms'); + }); + + test('getScaledDateFormat', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + timeBuckets.setInterval('20m'); + timeBuckets.getScaledDateFormat(); + const format = timeBuckets.getScaledDateFormat(); + expect(format).toEqual('HH:mm'); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts index c14f02e7decdf..b8d6586652d6b 100644 --- a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts +++ b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts @@ -17,11 +17,11 @@ * under the License. */ -import _ from 'lodash'; -import moment from 'moment'; +import { isString, isObject as isObjectLodash, isPlainObject, sortBy } from 'lodash'; +import moment, { Moment } from 'moment'; -import { IUiSettingsClient } from 'src/core/public'; import { parseInterval } from '../../../../../../common'; +import { TimeRangeBounds } from '../../../../../query'; import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval'; import { convertDurationToNormalizedEsInterval, @@ -29,37 +29,30 @@ import { EsInterval, } from './calc_es_interval'; -interface Bounds { - min: Date | number | null; - max: Date | number | null; -} - interface TimeBucketsInterval extends moment.Duration { // TODO double-check whether all of these are needed description: string; esValue: EsInterval['value']; esUnit: EsInterval['unit']; expression: EsInterval['expression']; - overflow: moment.Duration | boolean; - preScaled?: moment.Duration; + preScaled?: TimeBucketsInterval; scale?: number; scaled?: boolean; } function isObject(o: any): o is Record { - return _.isObject(o); -} - -function isString(s: any): s is string { - return _.isString(s); + return isObjectLodash(o); } function isValidMoment(m: any): boolean { return m && 'isValid' in m && m.isValid(); } -interface TimeBucketsConfig { - uiSettings: IUiSettingsClient; +export interface TimeBucketsConfig { + 'histogram:maxBars': number; + 'histogram:barTarget': number; + dateFormat: string; + 'dateFormat:scaled': string[][]; } /** @@ -70,108 +63,17 @@ interface TimeBucketsConfig { * @param {[type]} display [description] */ export class TimeBuckets { - private getConfig: (key: string) => any; - - private _lb: Bounds['min'] = null; - private _ub: Bounds['max'] = null; + private _timeBucketConfig: TimeBucketsConfig; + private _lb: TimeRangeBounds['min']; + private _ub: TimeRangeBounds['max']; private _originalInterval: string | null = null; private _i?: moment.Duration | 'auto'; // because other parts of Kibana arbitrarily add properties [key: string]: any; - static __cached__(self: TimeBuckets) { - let cache: any = {}; - const sameMoment = same(moment.isMoment); - const sameDuration = same(moment.isDuration); - - const desc: Record = { - __cached__: { - value: self, - }, - }; - - const breakers: Record = { - setBounds: 'bounds', - clearBounds: 'bounds', - setInterval: 'interval', - }; - - const resources: Record = { - bounds: { - setup() { - return [self._lb, self._ub]; - }, - changes(prev: any) { - return !sameMoment(prev[0], self._lb) || !sameMoment(prev[1], self._ub); - }, - }, - interval: { - setup() { - return self._i; - }, - changes(prev: any) { - return !sameDuration(prev, self._i); - }, - }, - }; - - function cachedGetter(prop: string) { - return { - value: (...rest: any) => { - if (cache.hasOwnProperty(prop)) { - return cache[prop]; - } - - return (cache[prop] = self[prop](...rest)); - }, - }; - } - - function cacheBreaker(prop: string) { - const resource = resources[breakers[prop]]; - const setup = resource.setup; - const changes = resource.changes; - const fn = self[prop]; - - return { - value: (...args: any) => { - const prev = setup.call(self); - const ret = fn.apply(self, ...args); - - if (changes.call(self, prev)) { - cache = {}; - } - - return ret; - }, - }; - } - - function same(checkType: any) { - return function(a: any, b: any) { - if (a === b) return true; - if (checkType(a) === checkType(b)) return +a === +b; - return false; - }; - } - - _.forOwn(TimeBuckets.prototype, (fn, prop) => { - if (!prop || prop[0] === '_') return; - - if (breakers.hasOwnProperty(prop)) { - desc[prop] = cacheBreaker(prop); - } else { - desc[prop] = cachedGetter(prop); - } - }); - - return Object.create(self, desc); - } - - constructor({ uiSettings }: TimeBucketsConfig) { - this.getConfig = (key: string) => uiSettings.get(key); - return TimeBuckets.__cached__(this); + constructor(timeBucketConfig: TimeBucketsConfig) { + this._timeBucketConfig = timeBucketConfig; } /** @@ -182,10 +84,10 @@ export class TimeBuckets { * @return {moment.duration|undefined} */ private getDuration(): moment.Duration | undefined { - if (this._ub === null || this._lb === null || !this.hasBounds()) { + if (this._ub === undefined || this._lb === undefined || !this.hasBounds()) { return; } - const difference = (this._ub as number) - (this._lb as number); + const difference = this._ub.valueOf() - this._lb.valueOf(); return moment.duration(difference, 'ms'); } @@ -200,22 +102,20 @@ export class TimeBuckets { * * @returns {undefined} */ - setBounds(input?: Bounds | Bounds[]) { + setBounds(input?: TimeRangeBounds | TimeRangeBounds[]) { if (!input) return this.clearBounds(); let bounds; - if (_.isPlainObject(input) && !Array.isArray(input)) { + if (isPlainObject(input) && !Array.isArray(input)) { // accept the response from timefilter.getActiveBounds() bounds = [input.min, input.max]; } else { bounds = Array.isArray(input) ? input : []; } - const moments = _(bounds) - .map(_.ary(moment, 1)) - .sortBy(Number); + const moments: Moment[] = sortBy(bounds, Number); - const valid = moments.size() === 2 && moments.every(isValidMoment); + const valid = moments.length === 2 && moments.every(isValidMoment); if (!valid) { this.clearBounds(); throw new Error('invalid bounds set: ' + input); @@ -236,7 +136,7 @@ export class TimeBuckets { * @return {undefined} */ clearBounds() { - this._lb = this._ub = null; + this._lb = this._ub = undefined; } /** @@ -262,7 +162,7 @@ export class TimeBuckets { * object * */ - getBounds(): Bounds | undefined { + getBounds(): TimeRangeBounds | undefined { if (!this.hasBounds()) return; return { min: this._lb, @@ -278,11 +178,10 @@ export class TimeBuckets { * - Any object from src/legacy/ui/agg_types.js * - "auto" * - Pass a valid moment unit - * - a moment.duration object. * * @param {object|string|moment.duration} input - see desc */ - setInterval(input: null | string | Record | moment.Duration) { + setInterval(input: null | string | Record) { let interval = input; // selection object -> val @@ -351,7 +250,7 @@ export class TimeBuckets { const readInterval = () => { const interval = this._i; if (moment.isDuration(interval)) return interval; - return calcAutoIntervalNear(this.getConfig('histogram:barTarget'), Number(duration)); + return calcAutoIntervalNear(this._timeBucketConfig['histogram:barTarget'], Number(duration)); }; const parsedInterval = readInterval(); @@ -362,7 +261,7 @@ export class TimeBuckets { return interval; } - const maxLength: number = this.getConfig('histogram:maxBars'); + const maxLength: number = this._timeBucketConfig['histogram:maxBars']; const approxLen = Number(duration) / Number(interval); let scaled; @@ -396,10 +295,6 @@ export class TimeBuckets { esValue: esInterval.value, esUnit: esInterval.unit, expression: esInterval.expression, - overflow: - Number(duration) > Number(interval) - ? moment.duration(Number(interval) - Number(duration)) - : false, }); }; @@ -423,7 +318,7 @@ export class TimeBuckets { */ getScaledDateFormat() { const interval = this.getInterval(); - const rules = this.getConfig('dateFormat:scaled'); + const rules = this._timeBucketConfig['dateFormat:scaled']; for (let i = rules.length - 1; i >= 0; i--) { const rule = rules[i]; @@ -432,6 +327,6 @@ export class TimeBuckets { } } - return this.getConfig('dateFormat'); + return this._timeBucketConfig.dateFormat; } } diff --git a/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts b/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts index 459de66d057d4..9d976784329cc 100644 --- a/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts +++ b/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import moment from 'moment'; import { IUiSettingsClient } from 'src/core/public'; import { TimeBuckets } from '../buckets/lib/time_buckets'; import { toAbsoluteDates, TimeRange } from '../../../../common'; @@ -28,12 +28,17 @@ export function getCalculateAutoTimeExpression(uiSettings: IUiSettingsClient) { return; } - const buckets = new TimeBuckets({ uiSettings }); + const buckets = new TimeBuckets({ + 'histogram:maxBars': uiSettings.get('histogram:maxBars'), + 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + dateFormat: uiSettings.get('dateFormat'), + 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), + }); buckets.setInterval('auto'); buckets.setBounds({ - min: dates.from, - max: dates.to, + min: moment(dates.from), + max: moment(dates.to), }); return buckets.getInterval().expression; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 18af94c919247..f3192ba3da81f 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -94,8 +94,11 @@ const getSchemas = ( const createSchemaConfig = (accessor: number, agg: IAggConfig): SchemaConfig => { if (isDateHistogramBucketAggConfig(agg)) { agg.params.timeRange = timeRange; - const bounds = agg.params.timeRange ? timefilter.calculateBounds(agg.params.timeRange) : null; - agg.buckets.setBounds(agg.fieldIsTimeField() && bounds); + const bounds = + agg.params.timeRange && agg.fieldIsTimeField() + ? timefilter.calculateBounds(agg.params.timeRange) + : undefined; + agg.buckets.setBounds(bounds); agg.buckets.setInterval(agg.params.interval); } From 530732c9dd3fa9ab2db6af2c58b52f2a60288a36 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 9 Apr 2020 14:03:44 +0200 Subject: [PATCH 47/81] [ML] Functional tests - stabilize typing in mml input (#63091) This PR wraps the model memory value setting during anomaly detection wizards in a retry. --- .../services/machine_learning/job_wizard_common.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts index 36181b66786d5..af33ec2301edc 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts @@ -330,9 +330,11 @@ export function MachineLearningJobWizardCommonProvider( await this.ensureAdvancedSectionOpen(); subj = advancedSectionSelector(subj); } - await mlCommon.setValueWithChecks(subj, modelMemoryLimit, { clearWithKeyboard: true }); - await this.assertModelMemoryLimitValue(modelMemoryLimit, { - withAdvancedSection: sectionOptions.withAdvancedSection, + await retry.tryForTime(15 * 1000, async () => { + await mlCommon.setValueWithChecks(subj, modelMemoryLimit, { clearWithKeyboard: true }); + await this.assertModelMemoryLimitValue(modelMemoryLimit, { + withAdvancedSection: sectionOptions.withAdvancedSection, + }); }); }, From 8d21b6b6f3ccd0388e3a325e96e90f05df0c42b5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 9 Apr 2020 14:06:01 +0200 Subject: [PATCH 48/81] Move search source parsing and serializing to data (#59919) --- ...-plugins-data-public.createsearchsource.md | 15 ++ .../kibana-plugin-plugins-data-public.md | 1 + ...plugin-plugins-data-public.searchsource.md | 1 + ...gins-data-public.searchsource.serialize.md | 27 ++++ .../kibana/public/discover/build_services.ts | 1 + .../management/saved_object_registry.ts | 1 + .../components/flyout/__jest__/flyout.test.js | 3 +- .../objects_table/components/flyout/flyout.js | 3 +- .../objects/lib/resolve_saved_objects.test.ts | 87 +++++----- .../objects/lib/resolve_saved_objects.ts | 59 ++++--- .../public/visualize/np_ready/legacy_app.js | 1 + .../timelion/public/services/saved_sheets.ts | 1 + .../ui/public/new_platform/set_services.ts | 2 + src/plugins/dashboard/public/plugin.tsx | 3 +- .../saved_dashboards/saved_dashboards.ts | 3 +- src/plugins/data/public/index.ts | 1 + src/plugins/data/public/plugin.ts | 2 +- src/plugins/data/public/public.api.md | 38 +++-- src/plugins/data/public/search/index.ts | 1 + src/plugins/data/public/search/mocks.ts | 1 + .../data/public/search/search_service.ts | 5 +- .../create_search_source.test.ts | 151 ++++++++++++++++++ .../search_source/create_search_source.ts | 113 +++++++++++++ .../data/public/search/search_source/index.ts | 1 + .../data/public/search/search_source/mocks.ts | 1 + .../search_source/search_source.test.ts | 75 ++++++++- .../search/search_source/search_source.ts | 82 ++++++++++ src/plugins/data/public/search/types.ts | 2 + src/plugins/saved_objects/public/plugin.ts | 1 + .../saved_object/helpers/apply_es_resp.ts | 32 +++- .../helpers/build_saved_object.ts | 3 +- .../helpers/hydrate_index_pattern.ts | 10 +- .../helpers/parse_search_source.ts | 97 ----------- .../helpers/serialize_saved_object.ts | 62 ++----- .../public/saved_object/saved_object.test.ts | 52 +++--- src/plugins/saved_objects/public/types.ts | 10 +- src/plugins/visualizations/public/plugin.ts | 3 + .../public/saved_visualizations/_saved_vis.ts | 3 +- src/plugins/visualizations/public/services.ts | 2 + .../services/gis_map_saved_object_loader.js | 1 + .../use_search_items/use_search_items.ts | 1 + 41 files changed, 685 insertions(+), 273 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md create mode 100644 src/plugins/data/public/search/search_source/create_search_source.test.ts create mode 100644 src/plugins/data/public/search/search_source/create_search_source.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/parse_search_source.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md new file mode 100644 index 0000000000000..5c5aa348eecdf --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [createSearchSource](./kibana-plugin-plugins-data-public.createsearchsource.md) + +## createSearchSource variable + +Deserializes a json string and a set of referenced objects to a `SearchSource` instance. Use this method to re-create the search source serialized using `searchSource.serialize`. + +This function is a factory function that returns the actual utility when calling it with the required service dependency (index patterns contract). A pre-wired version is also exposed in the start contract of the data plugin as part of the search service + +Signature: + +```typescript +createSearchSource: (indexPatterns: Pick) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 6964c070097c5..fc0dab94a0f65 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -102,6 +102,7 @@ | [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-public.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | | [connectToQueryState](./kibana-plugin-plugins-data-public.connecttoquerystate.md) | Helper to setup two-way syncing of global data and a state container | | [createSavedQueryService](./kibana-plugin-plugins-data-public.createsavedqueryservice.md) | | +| [createSearchSource](./kibana-plugin-plugins-data-public.createsearchsource.md) | Deserializes a json string and a set of referenced objects to a SearchSource instance. Use this method to re-create the search source serialized using searchSource.serialize.This function is a factory function that returns the actual utility when calling it with the required service dependency (index patterns contract). A pre-wired version is also exposed in the start contract of the data plugin as part of the search service | | [ES\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.es_search_strategy.md) | | | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md index 8e1dbb6e2671d..5f2fc809a5590 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md @@ -38,6 +38,7 @@ export declare class SearchSource | [getParent()](./kibana-plugin-plugins-data-public.searchsource.getparent.md) | | Get the parent of this SearchSource {undefined\|searchSource} | | [getSearchRequestBody()](./kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md) | | | | [onRequestStart(handler)](./kibana-plugin-plugins-data-public.searchsource.onrequeststart.md) | | Add a handler that will be notified whenever requests start | +| [serialize()](./kibana-plugin-plugins-data-public.searchsource.serialize.md) | | Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object.The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named kibanaSavedObjectMeta.searchSourceJSON.index and kibanaSavedObjectMeta.searchSourceJSON.filter[<number>].meta.index.Using createSearchSource, the instance can be re-created. | | [setField(field, value)](./kibana-plugin-plugins-data-public.searchsource.setfield.md) | | | | [setFields(newFields)](./kibana-plugin-plugins-data-public.searchsource.setfields.md) | | | | [setParent(parent, options)](./kibana-plugin-plugins-data-public.searchsource.setparent.md) | | Set a searchSource that this source should inherit from | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md new file mode 100644 index 0000000000000..52d25dec01dfd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [serialize](./kibana-plugin-plugins-data-public.searchsource.serialize.md) + +## SearchSource.serialize() method + +Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object. + +The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index` and `kibanaSavedObjectMeta.searchSourceJSON.filter[].meta.index`. + +Using `createSearchSource`, the instance can be re-created. + +Signature: + +```typescript +serialize(): { + searchSourceJSON: string; + references: SavedObjectReference[]; + }; +``` +Returns: + +`{ + searchSourceJSON: string; + references: SavedObjectReference[]; + }` + diff --git a/src/legacy/core_plugins/kibana/public/discover/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/build_services.ts index 180ff13cdddc0..a3a99a0ded523 100644 --- a/src/legacy/core_plugins/kibana/public/discover/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/build_services.ts @@ -72,6 +72,7 @@ export async function buildServices( const services = { savedObjectsClient: core.savedObjects.client, indexPatterns: plugins.data.indexPatterns, + search: plugins.data.search, chrome: core.chrome, overlays: core.overlays, }; diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts index 7261b2ba03372..705be68a141e7 100644 --- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts +++ b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts @@ -56,6 +56,7 @@ export const savedObjectManagementRegistry: ISavedObjectsManagementRegistry = { const services = { savedObjectsClient: npStart.core.savedObjects.client, indexPatterns: npStart.plugins.data.indexPatterns, + search: npStart.plugins.data.search, chrome: npStart.core.chrome, overlays: npStart.core.overlays, }; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js index 5d14c4609b918..0d16e0ae35dd6 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js @@ -519,7 +519,8 @@ describe('Flyout', () => { expect(resolveIndexPatternConflicts).toHaveBeenCalledWith( component.instance().resolutions, mockConflictedIndexPatterns, - true + true, + defaultProps.indexPatterns ); expect(saveObjects).toHaveBeenCalledWith( mockConflictedSavedObjectsLinkedToSavedSearches, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js index 105c279218375..da2221bb54203 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js @@ -358,7 +358,8 @@ export class Flyout extends Component { importCount += await resolveIndexPatternConflicts( resolutions, conflictedIndexPatterns, - isOverwriteAllChecked + isOverwriteAllChecked, + this.props.indexPatterns ); } this.setState({ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts index 8243aa69ac082..dc6d2643145ff 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts @@ -84,7 +84,7 @@ describe('resolveSavedObjects', () => { }, } as unknown) as IndexPatternsContract; - const services = [ + const services = ([ { type: 'search', get: async () => { @@ -124,7 +124,7 @@ describe('resolveSavedObjects', () => { }; }, }, - ] as SavedObjectLoader[]; + ] as unknown) as SavedObjectLoader[]; const overwriteAll = false; @@ -176,7 +176,7 @@ describe('resolveSavedObjects', () => { }, } as unknown) as IndexPatternsContract; - const services = [ + const services = ([ { type: 'search', get: async () => { @@ -217,7 +217,7 @@ describe('resolveSavedObjects', () => { }; }, }, - ] as SavedObjectLoader[]; + ] as unknown) as SavedObjectLoader[]; const overwriteAll = false; @@ -237,33 +237,38 @@ describe('resolveSavedObjects', () => { describe('resolveIndexPatternConflicts', () => { it('should resave resolutions', async () => { - const hydrateIndexPattern = jest.fn(); const save = jest.fn(); - const conflictedIndexPatterns = [ + const conflictedIndexPatterns = ([ { obj: { - searchSource: { - getOwnField: (field: string) => { - return field === 'index' ? '1' : undefined; + save, + }, + doc: { + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: '1', + }), }, }, - hydrateIndexPattern, - save, }, }, { obj: { - searchSource: { - getOwnField: (field: string) => { - return field === 'index' ? '3' : undefined; + save, + }, + doc: { + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: '3', + }), }, }, - hydrateIndexPattern, - save, }, }, - ]; + ] as unknown) as Array<{ obj: SavedObject; doc: any }>; const resolutions = [ { @@ -282,43 +287,49 @@ describe('resolveSavedObjects', () => { const overwriteAll = false; - await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll); - expect(hydrateIndexPattern.mock.calls.length).toBe(2); + await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({ + get: (id: string) => Promise.resolve({ id }), + } as unknown) as IndexPatternsContract); + expect(conflictedIndexPatterns[0].obj.searchSource!.getField('index')!.id).toEqual('2'); + expect(conflictedIndexPatterns[1].obj.searchSource!.getField('index')!.id).toEqual('4'); expect(save.mock.calls.length).toBe(2); expect(save).toHaveBeenCalledWith({ confirmOverwrite: !overwriteAll }); - expect(hydrateIndexPattern).toHaveBeenCalledWith('2'); - expect(hydrateIndexPattern).toHaveBeenCalledWith('4'); }); it('should resolve filter index conflicts', async () => { - const hydrateIndexPattern = jest.fn(); const save = jest.fn(); - const conflictedIndexPatterns = [ + const conflictedIndexPatterns = ([ { obj: { - searchSource: { - getOwnField: (field: string) => { - return field === 'index' ? '1' : [{ meta: { index: 'filterIndex' } }]; + save, + }, + doc: { + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: '1', + filter: [{ meta: { index: 'filterIndex' } }], + }), }, - setField: jest.fn(), }, - hydrateIndexPattern, - save, }, }, { obj: { - searchSource: { - getOwnField: (field: string) => { - return field === 'index' ? '3' : undefined; + save, + }, + doc: { + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: '3', + }), }, }, - hydrateIndexPattern, - save, }, }, - ]; + ] as unknown) as Array<{ obj: SavedObject; doc: any }>; const resolutions = [ { @@ -337,9 +348,11 @@ describe('resolveSavedObjects', () => { const overwriteAll = false; - await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll); + await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({ + get: (id: string) => Promise.resolve({ id }), + } as unknown) as IndexPatternsContract); - expect(conflictedIndexPatterns[0].obj.searchSource.setField).toHaveBeenCalledWith('filter', [ + expect(conflictedIndexPatterns[0].obj.searchSource!.getField('filter')).toEqual([ { meta: { index: 'newFilterIndex' } }, ]); expect(save.mock.calls.length).toBe(2); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts index 902de654f5f85..d9473367f7502 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts @@ -18,12 +18,17 @@ */ import { i18n } from '@kbn/i18n'; -import { OverlayStart } from 'src/core/public'; +import { cloneDeep } from 'lodash'; +import { OverlayStart, SavedObjectReference } from 'src/core/public'; import { SavedObject, SavedObjectLoader, } from '../../../../../../../../plugins/saved_objects/public'; -import { IndexPatternsContract, IIndexPattern } from '../../../../../../../../plugins/data/public'; +import { + IndexPatternsContract, + IIndexPattern, + createSearchSource, +} from '../../../../../../../../plugins/data/public'; type SavedObjectsRawDoc = Record; @@ -126,7 +131,7 @@ async function importIndexPattern( async function importDocument(obj: SavedObject, doc: SavedObjectsRawDoc, overwriteAll: boolean) { await obj.applyESResp({ references: doc._references || [], - ...doc, + ...cloneDeep(doc), }); return await obj.save({ confirmOverwrite: !overwriteAll }); } @@ -160,41 +165,57 @@ async function awaitEachItemInParallel(list: T[], op: (item: T) => R) { export async function resolveIndexPatternConflicts( resolutions: Array<{ oldId: string; newId: string }>, conflictedIndexPatterns: any[], - overwriteAll: boolean + overwriteAll: boolean, + indexPatterns: IndexPatternsContract ) { let importCount = 0; - await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj }) => { - // Resolve search index reference: - let oldIndexId = obj.searchSource.getOwnField('index'); - // Depending on the object, this can either be the raw id or the actual index pattern object - if (typeof oldIndexId !== 'string') { - oldIndexId = oldIndexId.id; - } - let resolution = resolutions.find(({ oldId }) => oldId === oldIndexId); - if (resolution) { - const newIndexId = resolution.newId; - await obj.hydrateIndexPattern(newIndexId); + await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj, doc }) => { + const serializedSearchSource = JSON.parse( + doc._source.kibanaSavedObjectMeta?.searchSourceJSON || '{}' + ); + const oldIndexId = serializedSearchSource.index; + let allResolved = true; + const inlineResolution = resolutions.find(({ oldId }) => oldId === oldIndexId); + if (inlineResolution) { + serializedSearchSource.index = inlineResolution.newId; + } else { + allResolved = false; } // Resolve filter index reference: - const filter = (obj.searchSource.getOwnField('filter') || []).map((f: any) => { + const filter = (serializedSearchSource.filter || []).map((f: any) => { if (!(f.meta && f.meta.index)) { return f; } - resolution = resolutions.find(({ oldId }) => oldId === f.meta.index); + const resolution = resolutions.find(({ oldId }) => oldId === f.meta.index); return resolution ? { ...f, ...{ meta: { ...f.meta, index: resolution.newId } } } : f; }); if (filter.length > 0) { - obj.searchSource.setField('filter', filter); + serializedSearchSource.filter = filter; } - if (!resolution) { + const replacedReferences = (doc._references || []).map((reference: SavedObjectReference) => { + const resolution = resolutions.find(({ oldId }) => oldId === reference.id); + if (resolution) { + return { ...reference, id: resolution.newId }; + } else { + allResolved = false; + } + + return reference; + }); + + if (!allResolved) { // The user decided to skip this conflict so do nothing return; } + obj.searchSource = await createSearchSource(indexPatterns)( + JSON.stringify(serializedSearchSource), + replacedReferences + ); if (await saveObject(obj, overwriteAll)) { importCount++; } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js index 7c9ab32ab2f72..a710d3e318749 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js @@ -71,6 +71,7 @@ const getResolvedResults = deps => { return createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, + search: data.search, chrome: core.chrome, overlays: core.overlays, }).get(results.vis.data.savedSearchId); diff --git a/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts b/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts index 201b21f932988..e7f431a178ea0 100644 --- a/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts +++ b/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts @@ -28,6 +28,7 @@ const savedObjectsClient = npStart.core.savedObjects.client; const services = { savedObjectsClient, indexPatterns: npStart.plugins.data.indexPatterns, + search: npStart.plugins.data.search, chrome: npStart.core.chrome, overlays: npStart.core.overlays, }; diff --git a/src/legacy/ui/public/new_platform/set_services.ts b/src/legacy/ui/public/new_platform/set_services.ts index 8cf015d5dff5c..400f31e73ffa1 100644 --- a/src/legacy/ui/public/new_platform/set_services.ts +++ b/src/legacy/ui/public/new_platform/set_services.ts @@ -72,9 +72,11 @@ export function setStartServices(npStart: NpStart) { visualizationsServices.setAggs(npStart.plugins.data.search.aggs); visualizationsServices.setOverlays(npStart.core.overlays); visualizationsServices.setChrome(npStart.core.chrome); + visualizationsServices.setSearch(npStart.plugins.data.search); const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: npStart.core.savedObjects.client, indexPatterns: npStart.plugins.data.indexPatterns, + search: npStart.plugins.data.search, chrome: npStart.core.chrome, overlays: npStart.core.overlays, visualizationTypes: visualizationsServices.getTypes(), diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index c98fa612dc7af..322d734d9f39f 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -284,7 +284,7 @@ export class DashboardPlugin const { notifications } = core; const { uiActions, - data: { indexPatterns }, + data: { indexPatterns, search }, } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -300,6 +300,7 @@ export class DashboardPlugin const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns, + search, chrome: core.chrome, overlays: core.overlays, }); diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 2a1e64fa88a02..09357072a13a6 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -18,13 +18,14 @@ */ import { SavedObjectsClientContract, ChromeStart, OverlayStart } from 'kibana/public'; -import { IndexPatternsContract } from '../../../../plugins/data/public'; +import { DataPublicPluginStart, IndexPatternsContract } from '../../../../plugins/data/public'; import { SavedObjectLoader } from '../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; interface Services { savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; + search: DataPublicPluginStart['search']; chrome: ChromeStart; overlays: OverlayStart; } diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index efafea44167d4..06a46065baa84 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -366,6 +366,7 @@ export { SearchStrategyProvider, ISearchSource, SearchSource, + createSearchSource, SearchSourceFields, EsQuerySortValue, SortDirection, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 15067077afc43..2ebe377b3b32f 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -155,7 +155,7 @@ export class DataPublicPlugin implements Plugin({ timefilter: { timefil // @public (undocumented) export const createSavedQueryService: (savedObjectsClient: Pick) => SavedQueryService; +// @public +export const createSearchSource: (indexPatterns: Pick) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise; + // Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1667,6 +1671,10 @@ export class SearchSource { // (undocumented) history: SearchRequest[]; onRequestStart(handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise): void; + serialize(): { + searchSourceJSON: string; + references: SavedObjectReference[]; + }; // (undocumented) setField(field: K, value: SearchSourceFields[K]): this; // (undocumented) @@ -1881,21 +1889,21 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromEvent" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 1687d749f46e2..cce973d632f41 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -54,6 +54,7 @@ export { SearchSourceFields, EsQuerySortValue, SortDirection, + createSearchSource, } from './search_source'; export { SearchInterceptor } from './search_interceptor'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index b70e889066a45..cb1c625a72959 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -33,6 +33,7 @@ export const searchStartMock: jest.Mocked = { aggs: searchAggsStartMock(), setInterceptor: jest.fn(), search: jest.fn(), + createSearchSource: jest.fn(), __LEGACY: { AggConfig: jest.fn() as any, AggType: jest.fn(), diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 42f31ef450d28..6124682184821 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -25,6 +25,8 @@ import { TStrategyTypes } from './strategy_types'; import { getEsClient, LegacyApiCaller } from './es_client'; import { ES_SEARCH_STRATEGY, DEFAULT_SEARCH_STRATEGY } from '../../common/search'; import { esSearchStrategyProvider } from './es_search/es_search_strategy'; +import { IndexPatternsContract } from '../index_patterns/index_patterns'; +import { createSearchSource } from './search_source'; import { QuerySetup } from '../query/query_service'; import { GetInternalStartServicesFn } from '../types'; import { SearchInterceptor } from './search_interceptor'; @@ -108,7 +110,7 @@ export class SearchService implements Plugin { }; } - public start(core: CoreStart): ISearchStart { + public start(core: CoreStart, indexPatterns: IndexPatternsContract): ISearchStart { /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -145,6 +147,7 @@ export class SearchService implements Plugin { // TODO: should an intercepror have a destroy method? this.searchInterceptor = searchInterceptor; }, + createSearchSource: createSearchSource(indexPatterns), __LEGACY: { esClient: this.esClient!, AggConfig, diff --git a/src/plugins/data/public/search/search_source/create_search_source.test.ts b/src/plugins/data/public/search/search_source/create_search_source.test.ts new file mode 100644 index 0000000000000..d49ce5a0d11f8 --- /dev/null +++ b/src/plugins/data/public/search/search_source/create_search_source.test.ts @@ -0,0 +1,151 @@ +/* + * 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 { createSearchSource as createSearchSourceFactory } from './create_search_source'; +import { IIndexPattern } from '../../../common/index_patterns'; +import { IndexPatternsContract } from '../../index_patterns/index_patterns'; +import { Filter } from '../../../common/es_query/filters'; + +describe('createSearchSource', function() { + let createSearchSource: ReturnType; + const indexPatternMock: IIndexPattern = {} as IIndexPattern; + let indexPatternContractMock: jest.Mocked; + + beforeEach(() => { + indexPatternContractMock = ({ + get: jest.fn().mockReturnValue(Promise.resolve(indexPatternMock)), + } as unknown) as jest.Mocked; + createSearchSource = createSearchSourceFactory(indexPatternContractMock); + }); + + it('should fail if JSON is invalid', () => { + expect(createSearchSource('{', [])).rejects.toThrow(); + expect(createSearchSource('0', [])).rejects.toThrow(); + expect(createSearchSource('"abcdefg"', [])).rejects.toThrow(); + }); + + it('should set fields', async () => { + const searchSource = await createSearchSource( + JSON.stringify({ + highlightAll: true, + query: { + query: '', + language: 'kuery', + }, + }), + [] + ); + expect(searchSource.getOwnField('highlightAll')).toBe(true); + expect(searchSource.getOwnField('query')).toEqual({ + query: '', + language: 'kuery', + }); + }); + + it('should resolve referenced index pattern', async () => { + const searchSource = await createSearchSource( + JSON.stringify({ + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }), + [ + { + id: '123-456', + type: 'index-pattern', + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }, + ] + ); + expect(indexPatternContractMock.get).toHaveBeenCalledWith('123-456'); + expect(searchSource.getOwnField('index')).toBe(indexPatternMock); + }); + + it('should set filters and resolve referenced index patterns', async () => { + const searchSource = await createSearchSource( + JSON.stringify({ + filter: [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Clothing", + }, + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + }, + query: { + match_phrase: { + 'category.keyword': "Men's Clothing", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }), + [ + { + id: '123-456', + type: 'index-pattern', + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + }, + ] + ); + const filters = searchSource.getOwnField('filter') as Filter[]; + expect(filters[0]).toMatchInlineSnapshot(` + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "123-456", + "key": "category.keyword", + "negate": false, + "params": Object { + "query": "Men's Clothing", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "category.keyword": "Men's Clothing", + }, + }, + } + `); + }); + + it('should migrate legacy queries on the fly', async () => { + const searchSource = await createSearchSource( + JSON.stringify({ + highlightAll: true, + query: 'a:b', + }), + [] + ); + expect(searchSource.getOwnField('query')).toEqual({ + query: 'a:b', + language: 'lucene', + }); + }); +}); diff --git a/src/plugins/data/public/search/search_source/create_search_source.ts b/src/plugins/data/public/search/search_source/create_search_source.ts new file mode 100644 index 0000000000000..35b7ac4eb9762 --- /dev/null +++ b/src/plugins/data/public/search/search_source/create_search_source.ts @@ -0,0 +1,113 @@ +/* + * 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 _ from 'lodash'; +import { SavedObjectReference } from 'kibana/public'; +import { migrateLegacyQuery } from '../../../../kibana_legacy/public'; +import { InvalidJSONProperty } from '../../../../kibana_utils/public'; +import { SearchSource } from './search_source'; +import { IndexPatternsContract } from '../../index_patterns/index_patterns'; +import { SearchSourceFields } from './types'; + +/** + * Deserializes a json string and a set of referenced objects to a `SearchSource` instance. + * Use this method to re-create the search source serialized using `searchSource.serialize`. + * + * This function is a factory function that returns the actual utility when calling it with the + * required service dependency (index patterns contract). A pre-wired version is also exposed in + * the start contract of the data plugin as part of the search service + * + * @param indexPatterns The index patterns contract of the data plugin + * + * @return Wired utility function taking two parameters `searchSourceJson`, the json string + * returned by `serializeSearchSource` and `references`, a list of references including the ones + * returned by `serializeSearchSource`. + * + * @public */ +export const createSearchSource = (indexPatterns: IndexPatternsContract) => async ( + searchSourceJson: string, + references: SavedObjectReference[] +) => { + const searchSource = new SearchSource(); + + // if we have a searchSource, set its values based on the searchSourceJson field + let searchSourceValues: Record; + try { + searchSourceValues = JSON.parse(searchSourceJson); + } catch (e) { + throw new InvalidJSONProperty( + `Invalid JSON in search source. ${e.message} JSON: ${searchSourceJson}` + ); + } + + // This detects a scenario where documents with invalid JSON properties have been imported into the saved object index. + // (This happened in issue #20308) + if (!searchSourceValues || typeof searchSourceValues !== 'object') { + throw new InvalidJSONProperty('Invalid JSON in search source.'); + } + + // Inject index id if a reference is saved + if (searchSourceValues.indexRefName) { + const reference = references.find(ref => ref.name === searchSourceValues.indexRefName); + if (!reference) { + throw new Error(`Could not find reference for ${searchSourceValues.indexRefName}`); + } + searchSourceValues.index = reference.id; + delete searchSourceValues.indexRefName; + } + + if (searchSourceValues.filter && Array.isArray(searchSourceValues.filter)) { + searchSourceValues.filter.forEach((filterRow: any) => { + if (!filterRow.meta || !filterRow.meta.indexRefName) { + return; + } + const reference = references.find((ref: any) => ref.name === filterRow.meta.indexRefName); + if (!reference) { + throw new Error(`Could not find reference for ${filterRow.meta.indexRefName}`); + } + filterRow.meta.index = reference.id; + delete filterRow.meta.indexRefName; + }); + } + + if (searchSourceValues.index && typeof searchSourceValues.index === 'string') { + searchSourceValues.index = await indexPatterns.get(searchSourceValues.index); + } + + const searchSourceFields = searchSource.getFields(); + const fnProps = _.transform( + searchSourceFields, + function(dynamic, val, name) { + if (_.isFunction(val) && name) dynamic[name] = val; + }, + {} + ); + + // This assignment might hide problems because the type of values passed from the parsed JSON + // might not fit the SearchSourceFields interface. + const newFields: SearchSourceFields = _.defaults(searchSourceValues, fnProps); + + searchSource.setFields(newFields); + const query = searchSource.getOwnField('query'); + + if (typeof query !== 'undefined') { + searchSource.setField('query', migrateLegacyQuery(query)); + } + + return searchSource; +}; diff --git a/src/plugins/data/public/search/search_source/index.ts b/src/plugins/data/public/search/search_source/index.ts index 10f1b2bc332e1..0e9f530d0968a 100644 --- a/src/plugins/data/public/search/search_source/index.ts +++ b/src/plugins/data/public/search/search_source/index.ts @@ -18,4 +18,5 @@ */ export * from './search_source'; +export { createSearchSource } from './create_search_source'; export { SortDirection, EsQuerySortValue, SearchSourceFields } from './types'; diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/public/search/search_source/mocks.ts index 700bea741bd6a..1ef7c1187a9e0 100644 --- a/src/plugins/data/public/search/search_source/mocks.ts +++ b/src/plugins/data/public/search/search_source/mocks.ts @@ -37,4 +37,5 @@ export const searchSourceMock: MockedKeys = { getSearchRequestBody: jest.fn(), destroy: jest.fn(), history: [], + serialize: jest.fn(), }; diff --git a/src/plugins/data/public/search/search_source/search_source.test.ts b/src/plugins/data/public/search/search_source/search_source.test.ts index fcd116a3f4121..6bad093d31402 100644 --- a/src/plugins/data/public/search/search_source/search_source.test.ts +++ b/src/plugins/data/public/search/search_source/search_source.test.ts @@ -18,7 +18,7 @@ */ import { SearchSource } from './search_source'; -import { IndexPattern } from '../..'; +import { IndexPattern, SortDirection } from '../..'; import { mockDataServices } from '../aggs/test_helpers'; jest.mock('../fetch', () => ({ @@ -150,4 +150,77 @@ describe('SearchSource', function() { expect(parentFn).toBeCalledWith(searchSource, options); }); }); + + describe('#serialize', function() { + it('should reference index patterns', () => { + const indexPattern123 = { id: '123' } as IndexPattern; + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern123); + const { searchSourceJSON, references } = searchSource.serialize(); + expect(references[0].id).toEqual('123'); + expect(references[0].type).toEqual('index-pattern'); + expect(JSON.parse(searchSourceJSON).indexRefName).toEqual(references[0].name); + }); + + it('should add other fields', () => { + const searchSource = new SearchSource(); + searchSource.setField('highlightAll', true); + searchSource.setField('from', 123456); + const { searchSourceJSON } = searchSource.serialize(); + expect(JSON.parse(searchSourceJSON).highlightAll).toEqual(true); + expect(JSON.parse(searchSourceJSON).from).toEqual(123456); + }); + + it('should omit sort and size', () => { + const searchSource = new SearchSource(); + searchSource.setField('highlightAll', true); + searchSource.setField('from', 123456); + searchSource.setField('sort', { field: SortDirection.asc }); + searchSource.setField('size', 200); + const { searchSourceJSON } = searchSource.serialize(); + expect(Object.keys(JSON.parse(searchSourceJSON))).toEqual(['highlightAll', 'from']); + }); + + it('should serialize filters', () => { + const searchSource = new SearchSource(); + const filter = [ + { + query: 'query', + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + ]; + searchSource.setField('filter', filter); + const { searchSourceJSON } = searchSource.serialize(); + expect(JSON.parse(searchSourceJSON).filter).toEqual(filter); + }); + + it('should reference index patterns in filters separately from index field', () => { + const searchSource = new SearchSource(); + const indexPattern123 = { id: '123' } as IndexPattern; + searchSource.setField('index', indexPattern123); + const filter = [ + { + query: 'query', + meta: { + alias: 'alias', + disabled: false, + negate: false, + index: '456', + }, + }, + ]; + searchSource.setField('filter', filter); + const { searchSourceJSON, references } = searchSource.serialize(); + expect(references[0].id).toEqual('123'); + expect(references[0].type).toEqual('index-pattern'); + expect(JSON.parse(searchSourceJSON).indexRefName).toEqual(references[0].name); + expect(references[1].id).toEqual('456'); + expect(references[1].type).toEqual('index-pattern'); + expect(JSON.parse(searchSourceJSON).filter[0].meta.indexRefName).toEqual(references[1].name); + }); + }); }); diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index 0c3321f03dabc..c70db7bb82ef7 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -70,6 +70,7 @@ */ import _ from 'lodash'; +import { SavedObjectReference } from 'kibana/public'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/public'; @@ -419,4 +420,85 @@ export class SearchSource { return searchRequest; } + + /** + * Serializes the instance to a JSON string and a set of referenced objects. + * Use this method to get a representation of the search source which can be stored in a saved object. + * + * The references returned by this function can be mixed with other references in the same object, + * however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index` + * and `kibanaSavedObjectMeta.searchSourceJSON.filter[].meta.index`. + * + * Using `createSearchSource`, the instance can be re-created. + * @param searchSource The search source to serialize + * @public */ + public serialize() { + const references: SavedObjectReference[] = []; + + const { + filter: originalFilters, + ...searchSourceFields + }: Omit = _.omit(this.getFields(), ['sort', 'size']); + let serializedSearchSourceFields: Omit & { + indexRefName?: string; + filter?: Array & { meta: Filter['meta'] & { indexRefName?: string } }>; + } = searchSourceFields; + if (searchSourceFields.index) { + const indexId = searchSourceFields.index.id!; + const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + references.push({ + name: refName, + type: 'index-pattern', + id: indexId, + }); + serializedSearchSourceFields = { + ...serializedSearchSourceFields, + indexRefName: refName, + index: undefined, + }; + } + if (originalFilters) { + const filters = this.getFilters(originalFilters); + serializedSearchSourceFields = { + ...serializedSearchSourceFields, + filter: filters.map((filterRow, i) => { + if (!filterRow.meta || !filterRow.meta.index) { + return filterRow; + } + const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + references.push({ + name: refName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + return { + ...filterRow, + meta: { + ...filterRow.meta, + indexRefName: refName, + index: undefined, + }, + }; + }), + }; + } + + return { searchSourceJSON: JSON.stringify(serializedSearchSourceFields), references }; + } + + private getFilters(filterField: SearchSourceFields['filter']): Filter[] { + if (!filterField) { + return []; + } + + if (Array.isArray(filterField)) { + return filterField; + } + + if (_.isFunction(filterField)) { + return this.getFilters(filterField()); + } + + return [filterField]; + } } diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 03cbfa9f8ed84..ba6e44f47b75e 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -18,6 +18,7 @@ */ import { CoreStart } from 'kibana/public'; +import { createSearchSource } from './search_source'; import { SearchAggsSetup, SearchAggsStart, SearchAggsStartLegacy } from './aggs'; import { ISearch, ISearchGeneric } from './i_search'; import { TStrategyTypes } from './strategy_types'; @@ -89,5 +90,6 @@ export interface ISearchStart { aggs: SearchAggsStart; setInterceptor: (searchInterceptor: SearchInterceptor) => void; search: ISearchGeneric; + createSearchSource: ReturnType; __LEGACY: ISearchStartLegacy & SearchAggsStartLegacy; } diff --git a/src/plugins/saved_objects/public/plugin.ts b/src/plugins/saved_objects/public/plugin.ts index 0f5773c00283e..7927238e12066 100644 --- a/src/plugins/saved_objects/public/plugin.ts +++ b/src/plugins/saved_objects/public/plugin.ts @@ -39,6 +39,7 @@ export class SavedObjectsPublicPlugin SavedObjectClass: createSavedObjectClass({ indexPatterns: data.indexPatterns, savedObjectsClient: core.savedObjects.client, + search: data.search, chrome: core.chrome, overlays: core.overlays, }), diff --git a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts index 2e965eaf1989b..9776887b6d741 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts @@ -18,9 +18,8 @@ */ import _ from 'lodash'; import { EsResponse, SavedObject, SavedObjectConfig } from '../../types'; -import { parseSearchSource } from './parse_search_source'; import { expandShorthand, SavedObjectNotFound } from '../../../../kibana_utils/public'; -import { IndexPattern } from '../../../../data/public'; +import { DataPublicPluginStart, IndexPattern } from '../../../../data/public'; /** * A given response of and ElasticSearch containing a plain saved object is applied to the given @@ -29,13 +28,13 @@ import { IndexPattern } from '../../../../data/public'; export async function applyESResp( resp: EsResponse, savedObject: SavedObject, - config: SavedObjectConfig + config: SavedObjectConfig, + createSearchSource: DataPublicPluginStart['search']['createSearchSource'] ) { const mapping = expandShorthand(config.mapping); const esType = config.type || ''; savedObject._source = _.cloneDeep(resp._source); const injectReferences = config.injectReferences; - const hydrateIndexPattern = savedObject.hydrateIndexPattern!; if (typeof resp.found === 'boolean' && !resp.found) { throw new SavedObjectNotFound(esType, savedObject.id || ''); } @@ -64,13 +63,34 @@ export async function applyESResp( _.assign(savedObject, savedObject._source); savedObject.lastSavedTitle = savedObject.title; - await parseSearchSource(savedObject, esType, meta.searchSourceJSON, resp.references); - await hydrateIndexPattern(); + if (config.searchSource) { + try { + savedObject.searchSource = await createSearchSource(meta.searchSourceJSON, resp.references); + } catch (error) { + if ( + error.constructor.name === 'SavedObjectNotFound' && + error.savedObjectType === 'index-pattern' + ) { + // if parsing the search source fails because the index pattern wasn't found, + // remember the reference - this is required for error handling on legacy imports + savedObject.unresolvedIndexPatternReference = { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + id: JSON.parse(meta.searchSourceJSON).index, + type: 'index-pattern', + }; + } + + throw error; + } + } + if (injectReferences && resp.references && resp.references.length > 0) { injectReferences(savedObject, resp.references); } + if (typeof config.afterESResp === 'function') { savedObject = await config.afterESResp(savedObject); } + return savedObject; } diff --git a/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts index b9043890e2775..e8faef4e9e040 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts @@ -81,7 +81,8 @@ export function buildSavedObject( */ savedObject.init = _.once(() => intializeSavedObject(savedObject, savedObjectsClient, config)); - savedObject.applyESResp = (resp: EsResponse) => applyESResp(resp, savedObject, config); + savedObject.applyESResp = (resp: EsResponse) => + applyESResp(resp, savedObject, config, services.search.createSearchSource); /** * Serialize this object diff --git a/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts b/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts index b55538e4073ba..84275cf35befb 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts @@ -31,25 +31,19 @@ export async function hydrateIndexPattern( indexPatterns: IndexPatternsContract, config: SavedObjectConfig ) { - const clearSavedIndexPattern = !!config.clearSavedIndexPattern; const indexPattern = config.indexPattern; if (!savedObject.searchSource) { return null; } - if (clearSavedIndexPattern) { - savedObject.searchSource!.setField('index', undefined); - return null; - } - - const index = id || indexPattern || savedObject.searchSource!.getOwnField('index'); + const index = id || indexPattern || savedObject.searchSource.getOwnField('index'); if (typeof index !== 'string' || !index) { return null; } const indexObj = await indexPatterns.get(index); - savedObject.searchSource!.setField('index', indexObj); + savedObject.searchSource.setField('index', indexObj); return indexObj; } diff --git a/src/plugins/saved_objects/public/saved_object/helpers/parse_search_source.ts b/src/plugins/saved_objects/public/saved_object/helpers/parse_search_source.ts deleted file mode 100644 index cdb191f9e7df8..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/parse_search_source.ts +++ /dev/null @@ -1,97 +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 _ from 'lodash'; -import { migrateLegacyQuery } from '../../../../kibana_legacy/public'; -import { SavedObject } from '../../types'; -import { InvalidJSONProperty } from '../../../../kibana_utils/public'; - -export function parseSearchSource( - savedObject: SavedObject, - esType: string, - searchSourceJson: string, - references: any[] -) { - if (!savedObject.searchSource) return; - - // if we have a searchSource, set its values based on the searchSourceJson field - let searchSourceValues: Record; - try { - searchSourceValues = JSON.parse(searchSourceJson); - } catch (e) { - throw new InvalidJSONProperty( - `Invalid JSON in ${esType} "${savedObject.id}". ${e.message} JSON: ${searchSourceJson}` - ); - } - - // This detects a scenario where documents with invalid JSON properties have been imported into the saved object index. - // (This happened in issue #20308) - if (!searchSourceValues || typeof searchSourceValues !== 'object') { - throw new InvalidJSONProperty(`Invalid searchSourceJSON in ${esType} "${savedObject.id}".`); - } - - // Inject index id if a reference is saved - if (searchSourceValues.indexRefName) { - const reference = references.find( - (ref: Record) => ref.name === searchSourceValues.indexRefName - ); - if (!reference) { - throw new Error( - `Could not find reference for ${ - searchSourceValues.indexRefName - } on ${savedObject.getEsType()} ${savedObject.id}` - ); - } - searchSourceValues.index = reference.id; - delete searchSourceValues.indexRefName; - } - - if (searchSourceValues.filter) { - searchSourceValues.filter.forEach((filterRow: any) => { - if (!filterRow.meta || !filterRow.meta.indexRefName) { - return; - } - const reference = references.find((ref: any) => ref.name === filterRow.meta.indexRefName); - if (!reference) { - throw new Error( - `Could not find reference for ${ - filterRow.meta.indexRefName - } on ${savedObject.getEsType()}` - ); - } - filterRow.meta.index = reference.id; - delete filterRow.meta.indexRefName; - }); - } - - const searchSourceFields = savedObject.searchSource.getFields(); - const fnProps = _.transform( - searchSourceFields, - function(dynamic: Record, val: any, name: string | undefined) { - if (_.isFunction(val) && name) dynamic[name] = val; - }, - {} - ); - - savedObject.searchSource.setFields(_.defaults(searchSourceValues, fnProps)); - const query = savedObject.searchSource.getOwnField('query'); - - if (typeof query !== 'undefined') { - savedObject.searchSource.setField('query', migrateLegacyQuery(query)); - } -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts index 8a020ca03aea3..78f9eeb8b5fb1 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts @@ -17,7 +17,6 @@ * under the License. */ import _ from 'lodash'; -import angular from 'angular'; import { SavedObject, SavedObjectConfig } from '../../types'; import { expandShorthand } from '../../../../kibana_utils/public'; @@ -41,57 +40,16 @@ export function serializeSavedObject(savedObject: SavedObject, config: SavedObje }); if (savedObject.searchSource) { - let searchSourceFields: Record = _.omit(savedObject.searchSource.getFields(), [ - 'sort', - 'size', - ]); - if (searchSourceFields.index) { - // searchSourceFields.index will normally be an IndexPattern, but can be a string in two scenarios: - // (1) `init()` (and by extension `hydrateIndexPattern()`) hasn't been called on Saved Object - // (2) The IndexPattern doesn't exist, so we fail to resolve it in `hydrateIndexPattern()` - const indexId = - typeof searchSourceFields.index === 'string' - ? searchSourceFields.index - : searchSourceFields.index.id; - const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; - references.push({ - name: refName, - type: 'index-pattern', - id: indexId, - }); - searchSourceFields = { - ...searchSourceFields, - indexRefName: refName, - index: undefined, - }; - } - if (searchSourceFields.filter) { - searchSourceFields = { - ...searchSourceFields, - filter: searchSourceFields.filter.map((filterRow: any, i: number) => { - if (!filterRow.meta || !filterRow.meta.index) { - return filterRow; - } - const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; - references.push({ - name: refName, - type: 'index-pattern', - id: filterRow.meta.index, - }); - return { - ...filterRow, - meta: { - ...filterRow.meta, - indexRefName: refName, - index: undefined, - }, - }; - }), - }; - } - attributes.kibanaSavedObjectMeta = { - searchSourceJSON: angular.toJson(searchSourceFields), - }; + const { + searchSourceJSON, + references: searchSourceReferences, + } = savedObject.searchSource.serialize(); + attributes.kibanaSavedObjectMeta = { searchSourceJSON }; + references.push(...searchSourceReferences); + } + + if (savedObject.unresolvedIndexPatternReference) { + references.push(savedObject.unresolvedIndexPatternReference); } return { attributes, references }; diff --git a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts index 08389e9e3c97f..60c66f84080b2 100644 --- a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts +++ b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts @@ -103,9 +103,11 @@ describe('Saved Object', () => { } beforeEach(() => { + (dataStartMock.search.createSearchSource as jest.Mock).mockReset(); SavedObjectClass = createSavedObjectClass({ savedObjectsClient: savedObjectsClientStub, indexPatterns: dataStartMock.indexPatterns, + search: dataStartMock.search, } as SavedObjectKibanaServices); }); @@ -269,7 +271,7 @@ describe('Saved Object', () => { ); }); - it('when index exists in searchSourceJSON', () => { + it('when search source references saved object', () => { const id = '123'; stubESResponse(getMockedDocResponse(id)); return createInitializedSavedObject({ type: 'dashboard', searchSource: true }).then( @@ -409,18 +411,17 @@ describe('Saved Object', () => { }); }); - it('throws error invalid JSON is detected', async () => { + it('forwards thrown exceptions from createSearchSource', async () => { + (dataStartMock.search.createSearchSource as jest.Mock).mockImplementation(() => { + throw new InvalidJSONProperty(''); + }); const savedObject = await createInitializedSavedObject({ type: 'dashboard', searchSource: true, }); const response = { found: true, - _source: { - kibanaSavedObjectMeta: { - searchSourceJSON: '"{\\n \\"filter\\": []\\n}"', - }, - }, + _source: {}, }; try { @@ -586,23 +587,24 @@ describe('Saved Object', () => { }); }); - it('injects references from searchSourceJSON', async () => { + it('passes references to search source parsing function', async () => { const savedObject = new SavedObjectClass({ type: 'dashboard', searchSource: true }); return savedObject.init!().then(() => { + const searchSourceJSON = JSON.stringify({ + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + filter: [ + { + meta: { + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + }, + }, + ], + }); const response = { found: true, _source: { kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', - filter: [ - { - meta: { - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', - }, - }, - ], - }), + searchSourceJSON, }, }, references: [ @@ -619,16 +621,10 @@ describe('Saved Object', () => { ], }; savedObject.applyESResp(response); - expect(savedObject.searchSource!.getFields()).toEqual({ - index: 'my-index-1', - filter: [ - { - meta: { - index: 'my-index-2', - }, - }, - ], - }); + expect(dataStartMock.search.createSearchSource).toBeCalledWith( + searchSourceJSON, + response.references + ); }); }); }); diff --git a/src/plugins/saved_objects/public/types.ts b/src/plugins/saved_objects/public/types.ts index 99088df84ec36..3184038040952 100644 --- a/src/plugins/saved_objects/public/types.ts +++ b/src/plugins/saved_objects/public/types.ts @@ -24,7 +24,12 @@ import { SavedObjectAttributes, SavedObjectReference, } from 'kibana/public'; -import { IIndexPattern, IndexPatternsContract, ISearchSource } from '../../data/public'; +import { + DataPublicPluginStart, + IIndexPattern, + IndexPatternsContract, + ISearchSource, +} from '../../data/public'; export interface SavedObject { _serialize: () => { attributes: SavedObjectAttributes; references: SavedObjectReference[] }; @@ -49,6 +54,7 @@ export interface SavedObject { searchSource?: ISearchSource; showInRecentlyAccessed: boolean; title: string; + unresolvedIndexPatternReference?: SavedObjectReference; } export interface SavedObjectSaveOpts { @@ -65,6 +71,7 @@ export interface SavedObjectCreationOpts { export interface SavedObjectKibanaServices { savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; + search: DataPublicPluginStart['search']; chrome: ChromeStart; overlays: OverlayStart; } @@ -72,7 +79,6 @@ export interface SavedObjectKibanaServices { export interface SavedObjectConfig { // is only used by visualize afterESResp?: (savedObject: SavedObject) => Promise; - clearSavedIndexPattern?: boolean; defaults?: any; extractReferences?: (opts: { attributes: SavedObjectAttributes; diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 216defcee9016..8fcb84b19a9be 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -26,6 +26,7 @@ import { setCapabilities, setHttp, setIndexPatterns, + setSearch, setSavedObjects, setUsageCollector, setFilterManager, @@ -140,6 +141,7 @@ export class VisualizationsPlugin setHttp(core.http); setSavedObjects(core.savedObjects); setIndexPatterns(data.indexPatterns); + setSearch(data.search); setFilterManager(data.query.filterManager); setExpressions(expressions); setUiActions(uiActions); @@ -150,6 +152,7 @@ export class VisualizationsPlugin const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, + search: data.search, chrome: core.chrome, overlays: core.overlays, visualizationTypes: types, diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts index bc96e08f4b9da..c99c7a4c2caa1 100644 --- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts @@ -35,7 +35,7 @@ import { extractReferences, injectReferences } from './saved_visualization_refer import { IIndexPattern, ISearchSource, SearchSource } from '../../../../plugins/data/public'; import { ISavedVis, SerializedVis } from '../types'; import { createSavedSearchesLoader } from '../../../../plugins/discover/public'; -import { getChrome, getOverlays, getIndexPatterns, getSavedObjects } from '../services'; +import { getChrome, getOverlays, getIndexPatterns, getSavedObjects, getSearch } from '../services'; export const convertToSerializedVis = async (savedVis: ISavedVis): Promise => { const { visState } = savedVis; @@ -87,6 +87,7 @@ const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: const savedSearch = await createSavedSearchesLoader({ savedObjectsClient: getSavedObjects().client, indexPatterns: getIndexPatterns(), + search: getSearch(), chrome: getChrome(), overlays: getOverlays(), }).get(savedSearchId); diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index c4668fa4b0c79..618c61dff176a 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -63,6 +63,8 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter('Search'); + export const [getUsageCollector, setUsageCollector] = createGetterSetter( 'UsageCollection' ); diff --git a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js index 252d602e8f564..bc636c0b200f8 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js +++ b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js @@ -17,6 +17,7 @@ module.service('gisMapSavedObjectLoader', function() { const services = { savedObjectsClient, indexPatterns: npStart.plugins.data.indexPatterns, + search: npStart.plugins.data.search, chrome: npStart.core.chrome, overlays: npStart.core.overlays, }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index f5f9e98fe659c..feff17b813112 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -29,6 +29,7 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { const savedSearches = createSavedSearchesLoader({ savedObjectsClient, indexPatterns, + search: appDeps.data.search, chrome: appDeps.chrome, overlays: appDeps.overlays, }); From 2a1c8d8de477f3aa94f010a8919ed8b9fb266117 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 9 Apr 2020 14:30:21 +0200 Subject: [PATCH 49/81] [Discover] Hide time picker when an indexpattern without timefield is selected (#62134) * Assign valid value whether the timepicker should be displayed * Add functional tests --- .../discover/np_ready/angular/discover.html | 2 +- .../_indexpattern_without_timefield.ts | 52 +++++++++++++++ test/functional/apps/discover/index.js | 1 + .../index_pattern_without_timefield/data.json | 65 +++++++++++++++++++ .../mappings.json | 39 +++++++++++ 5 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 test/functional/apps/discover/_indexpattern_without_timefield.ts create mode 100644 test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json create mode 100644 test/functional/fixtures/es_archiver/index_pattern_without_timefield/mappings.json diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html index fb38f3e7d4c49..d068e824a3e0a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html @@ -11,7 +11,7 @@

{{screenTitle}}

query="state.query" saved-query-id="state.savedQuery" screen-title="screenTitle" - show-date-picker="enableTimeRangeSelector" + show-date-picker="indexPattern.isTimeBased()" show-save-query="showSaveQuery" show-search-bar="true" use-default-behaviors="true" diff --git a/test/functional/apps/discover/_indexpattern_without_timefield.ts b/test/functional/apps/discover/_indexpattern_without_timefield.ts new file mode 100644 index 0000000000000..87a2da7e44a5e --- /dev/null +++ b/test/functional/apps/discover/_indexpattern_without_timefield.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); + + describe('indexpattern without timefield', function() { + before(async function() { + await esArchiver.loadIfNeeded('index_pattern_without_timefield'); + }); + + beforeEach(async function() { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.selectIndexPattern('without-timefield'); + }); + + after(async function unloadMakelogs() { + await esArchiver.unload('index_pattern_without_timefield'); + }); + + it('should not display a timepicker', async function() { + const timepickerExists = await PageObjects.timePicker.timePickerExists(); + expect(timepickerExists).to.be(false); + }); + + it('should display a timepicker after switching to an index pattern with timefield', async function() { + expect(await PageObjects.timePicker.timePickerExists()).to.be(false); + await PageObjects.discover.selectIndexPattern('with-timefield'); + expect(await PageObjects.timePicker.timePickerExists()).to.be(true); + }); + }); +} diff --git a/test/functional/apps/discover/index.js b/test/functional/apps/discover/index.js index 582c979a194f4..50f140b99aa1a 100644 --- a/test/functional/apps/discover/index.js +++ b/test/functional/apps/discover/index.js @@ -47,5 +47,6 @@ export default function({ getService, loadTestFile }) { loadTestFile(require.resolve('./_doc_navigation')); loadTestFile(require.resolve('./_date_nanos')); loadTestFile(require.resolve('./_date_nanos_mixed')); + loadTestFile(require.resolve('./_indexpattern_without_timefield')); }); } diff --git a/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json new file mode 100644 index 0000000000000..9493408a30040 --- /dev/null +++ b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json @@ -0,0 +1,65 @@ +{ + "type": "doc", + "value": { + "id": "index-pattern:without-timefield", + "index": ".kibana", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "title": "without-timefield" + }, + "type": "index-pattern" + } + } +} + +{ + "type": "doc", + "value": { + "id": "AU_x3-TaGFA8no6QjiSJ", + "index": "without-timefield", + "source": { + "@message" : "5", + "@timestamp": "2019-09-22T23:50:13.253Z", + "referer": "http://twitter.com/error/takuya-onishi", + "request": "/uploads/dafydd-williams.jpg", + "response": "200", + "type": "apache", + "url": "https://media-for-the-masses.theacademyofperformingartsandscience.org/uploads/dafydd-williams.jpg" + } + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:with-timefield", + "index": ".kibana", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "title": "with-timefield", + "timeFieldName": "@timestamp" + }, + "type": "index-pattern" + } + } +} + +{ + "type": "doc", + "value": { + "id": "AU_x3-TaGFA8no6QjiSJ", + "index": "with-timefield", + "source": { + "@message" : "5", + "@timestamp": "2019-09-22T23:50:13.253Z", + "referer": "http://twitter.com/error/takuya-onishi", + "request": "/uploads/dafydd-williams.jpg", + "response": "200", + "type": "apache", + "url": "https://media-for-the-masses.theacademyofperformingartsandscience.org/uploads/dafydd-williams.jpg" + } + } +} + diff --git a/test/functional/fixtures/es_archiver/index_pattern_without_timefield/mappings.json b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/mappings.json new file mode 100644 index 0000000000000..0096111923951 --- /dev/null +++ b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/mappings.json @@ -0,0 +1,39 @@ +{ + "type": "index", + "value": { + "index": "without-timefield", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "index": "with-timefield", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} From 883af7008934bc7d9bf26fc7c5663ff6a2ab8355 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 9 Apr 2020 09:39:33 -0400 Subject: [PATCH 50/81] [Remote clusters] Fix flaky jest tests (#58768) --- .../remote_clusters_add.test.js | 2 - .../remote_clusters_edit.test.js | 44 ++++++++----------- .../remote_clusters_list.test.js | 18 ++++---- .../remote_cluster_form.test.js.snap | 6 ++- .../remote_cluster_form.js | 2 +- .../remote_cluster_add/remote_cluster_add.js | 6 ++- 6 files changed, 40 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_add.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_add.test.js index 78482198b1a5d..569c9a6c56c5a 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_add.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_add.test.js @@ -7,8 +7,6 @@ import { pageHelpers, nextTick, setupEnvironment } from './helpers'; import { NON_ALPHA_NUMERIC_CHARS, ACCENTED_CHARS } from './helpers/constants'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.remoteClustersAdd; describe('Create Remote cluster', () => { diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_edit.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_edit.test.js index f7625d9eec090..a5905227f49b8 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_edit.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_edit.test.js @@ -4,29 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); +import { act } from 'react-dom/test-utils'; + import { RemoteClusterForm } from '../../public/application/sections/components/remote_cluster_form'; -// import { pageHelpers, setupEnvironment, nextTick } from './helpers'; -import { pageHelpers, nextTick } from './helpers'; +import { pageHelpers, setupEnvironment } from './helpers'; import { REMOTE_CLUSTER_EDIT, REMOTE_CLUSTER_EDIT_NAME } from './helpers/constants'; -// const { setup } = pageHelpers.remoteClustersEdit; +const { setup } = pageHelpers.remoteClustersEdit; const { setup: setupRemoteClustersAdd } = pageHelpers.remoteClustersAdd; -// FLAKY: https://github.com/elastic/kibana/issues/57762 -// FLAKY: https://github.com/elastic/kibana/issues/57997 -// FLAKY: https://github.com/elastic/kibana/issues/57998 -describe.skip('Edit Remote cluster', () => { - // let server; - // let httpRequestsMockHelpers; +describe('Edit Remote cluster', () => { + let server; + let httpRequestsMockHelpers; let component; let find; let exists; - - /** - * - * commented out due to hooks being called regardless of skip - * https://github.com/facebook/jest/issues/8379 + let waitFor; beforeAll(() => { ({ server, httpRequestsMockHelpers } = setupEnvironment()); @@ -39,13 +32,12 @@ describe.skip('Edit Remote cluster', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse([REMOTE_CLUSTER_EDIT]); - ({ component, find, exists } = setup()); - await nextTick(100); // We need to wait next tick for the mock server response to kick in - component.update(); + await act(async () => { + ({ component, find, exists, waitFor } = setup()); + await waitFor('remoteClusterForm'); + }); }); - */ - test('should have the title of the page set correctly', () => { expect(exists('remoteClusterPageTitle')).toBe(true); expect(find('remoteClusterPageTitle').text()).toEqual('Edit remote cluster'); @@ -60,14 +52,16 @@ describe.skip('Edit Remote cluster', () => { * the "create" remote cluster, we won't test it again but simply make sure that * the form component is indeed shared between the 2 app sections. */ - test('should use the same Form component as the "" component', async () => { - const { component: addRemoteClusterComponent } = setupRemoteClustersAdd(); + test('should use the same Form component as the "" component', async () => { + let addRemoteClusterTestBed; - await nextTick(); - addRemoteClusterComponent.update(); + await act(async () => { + addRemoteClusterTestBed = setupRemoteClustersAdd(); + addRemoteClusterTestBed.waitFor('remoteClusterAddPage'); + }); const formEdit = component.find(RemoteClusterForm); - const formAdd = addRemoteClusterComponent.find(RemoteClusterForm); + const formAdd = addRemoteClusterTestBed.component.find(RemoteClusterForm); expect(formEdit.length).toBe(1); expect(formAdd.length).toBe(1); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js index 954deb8b98d3e..bc73387831c9d 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { act } from 'react-dom/test-utils'; import { pageHelpers, @@ -17,8 +18,6 @@ import { getRemoteClusterMock } from '../../fixtures/remote_cluster'; import { PROXY_MODE } from '../../common/constants'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.remoteClustersList; describe('', () => { @@ -78,6 +77,7 @@ describe('', () => { let actions; let tableCellsValues; let rows; + let waitFor; // For deterministic tests, we need to make sure that remoteCluster1 comes before remoteCluster2 // in the table list that is rendered. As the table orders alphabetically by index name @@ -110,11 +110,11 @@ describe('', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); - // Mount the component - ({ component, find, exists, table, actions } = setup()); + await act(async () => { + ({ component, find, exists, table, actions, waitFor } = setup()); - await nextTick(100); // Make sure that the Http request is fulfilled - component.update(); + await waitFor('remoteClusterListTable'); + }); // Read the remote clusters list table ({ rows, tableCellsValues } = table.getMetaData('remoteClusterListTable')); @@ -241,8 +241,10 @@ describe('', () => { actions.clickBulkDeleteButton(); actions.clickConfirmModalDeleteRemoteCluster(); - await nextTick(600); // there is a 500ms timeout in the api action - component.update(); + await act(async () => { + await nextTick(600); // there is a 500ms timeout in the api action + component.update(); + }); ({ rows } = table.getMetaData('remoteClusterListTable')); diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap index 1e6c2c4d289aa..35c566548f158 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap @@ -118,9 +118,12 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u } save={[Function]} > - +
{this.renderSaveErrorFeedback()} - + diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js index 0531310bd097b..4f9c5dcd38254 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js @@ -57,7 +57,11 @@ export class RemoteClusterAdd extends PureComponent { const { isAddingCluster, addClusterError } = this.props; return ( - + Date: Thu, 9 Apr 2020 09:41:38 -0400 Subject: [PATCH 51/81] [Endpoint][EPM] Endpoint depending on ingest manager to initialize (#62871) * Endpoint successfully depending on ingest manager to initialize * Moving the endpoint functional tests to their own directory to avoid enabling ingest in the base tests * Removing page objects and other endpoint fields from base functional * Updating code owners with new functional location * Pointing resolver tests at endpoint functional tests * Pointing space tests at the endpoint functional directory * Adding jest test names --- .github/CODEOWNERS | 3 +- x-pack/plugins/endpoint/kibana.json | 2 +- .../public/applications/endpoint/index.tsx | 2 + .../endpoint/mocks/dependencies_start_mock.ts | 3 ++ .../applications/endpoint/view/setup.tsx | 52 +++++++++++++++++++ x-pack/plugins/endpoint/public/plugin.ts | 2 + .../ingest_manager/common/types/models/epm.ts | 1 + x-pack/plugins/ingest_manager/public/index.ts | 2 + .../plugins/ingest_manager/public/plugin.ts | 20 ++++++- x-pack/scripts/functional_tests.js | 2 + x-pack/test/functional/config.js | 5 -- .../endpoint/alerts/api_feature/mappings.json | 3 +- x-pack/test/functional/page_objects/index.ts | 4 -- .../apps/endpoint/alerts.ts | 0 .../feature_controls/endpoint_spaces.ts | 0 .../apps/endpoint/feature_controls/index.ts | 0 .../apps/endpoint/header_nav.ts | 0 .../apps/endpoint/host_list.ts | 0 .../apps/endpoint/index.ts | 0 .../apps/endpoint/landing_page.ts | 7 ++- .../apps/endpoint/policy_list.ts | 0 x-pack/test/functional_endpoint/config.ts | 37 +++++++++++++ .../ftr_provider_context.d.ts | 12 +++++ .../page_objects/endpoint_alerts_page.ts | 0 .../page_objects/endpoint_page.ts | 0 .../functional_endpoint/page_objects/index.ts | 15 ++++++ .../apps/endpoint/index.ts | 14 +++++ .../apps/endpoint/landing_page.ts | 22 ++++++++ .../config.ts | 30 +++++++++++ .../ftr_provider_context.d.ts | 12 +++++ x-pack/test/plugin_functional/config.ts | 4 +- 31 files changed, 238 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/setup.tsx rename x-pack/test/{functional => functional_endpoint}/apps/endpoint/alerts.ts (100%) rename x-pack/test/{functional => functional_endpoint}/apps/endpoint/feature_controls/endpoint_spaces.ts (100%) rename x-pack/test/{functional => functional_endpoint}/apps/endpoint/feature_controls/index.ts (100%) rename x-pack/test/{functional => functional_endpoint}/apps/endpoint/header_nav.ts (100%) rename x-pack/test/{functional => functional_endpoint}/apps/endpoint/host_list.ts (100%) rename x-pack/test/{functional => functional_endpoint}/apps/endpoint/index.ts (100%) rename x-pack/test/{functional => functional_endpoint}/apps/endpoint/landing_page.ts (72%) rename x-pack/test/{functional => functional_endpoint}/apps/endpoint/policy_list.ts (100%) create mode 100644 x-pack/test/functional_endpoint/config.ts create mode 100644 x-pack/test/functional_endpoint/ftr_provider_context.d.ts rename x-pack/test/{functional => functional_endpoint}/page_objects/endpoint_alerts_page.ts (100%) rename x-pack/test/{functional => functional_endpoint}/page_objects/endpoint_page.ts (100%) create mode 100644 x-pack/test/functional_endpoint/page_objects/index.ts create mode 100644 x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/index.ts create mode 100644 x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/landing_page.ts create mode 100644 x-pack/test/functional_endpoint_ingest_failure/config.ts create mode 100644 x-pack/test/functional_endpoint_ingest_failure/ftr_provider_context.d.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index feaf47e45fd69..e707250ff3261 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -202,7 +202,8 @@ # Endpoint /x-pack/plugins/endpoint/ @elastic/endpoint-app-team /x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team -/x-pack/test/functional/apps/endpoint/ @elastic/endpoint-app-team +/x-pack/test/functional_endpoint/ @elastic/endpoint-app-team +/x-pack/test/functional_endpoint_ingest_failure/ @elastic/endpoint-app-team /x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team # SIEM diff --git a/x-pack/plugins/endpoint/kibana.json b/x-pack/plugins/endpoint/kibana.json index 5b8bec7777406..4b48c83fb0e7c 100644 --- a/x-pack/plugins/endpoint/kibana.json +++ b/x-pack/plugins/endpoint/kibana.json @@ -3,7 +3,7 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "endpoint"], - "requiredPlugins": ["features", "embeddable", "data", "dataEnhanced"], + "requiredPlugins": ["features", "embeddable", "data", "dataEnhanced", "ingestManager"], "server": true, "ui": true } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 89a6302351a54..82ac95160519c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -18,6 +18,7 @@ import { PolicyList } from './view/policy'; import { PolicyDetails } from './view/policy'; import { HeaderNavigation } from './components/header_nav'; import { AppRootProvider } from './view/app_root_provider'; +import { Setup } from './view/setup'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. @@ -48,6 +49,7 @@ const AppRoot: React.FunctionComponent = React.memo( ({ history, store, coreStart, depsStart }) => { return ( + & { */ export interface DepsStartMock { data: DataMock; + ingestManager: IngestManagerStart; } /** @@ -54,5 +56,6 @@ export const depsStartMock: () => DepsStartMock = () => { return { data: dataMock, + ingestManager: { success: true }, }; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/setup.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/setup.tsx new file mode 100644 index 0000000000000..a826e1f30f75d --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/setup.tsx @@ -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 * as React from 'react'; +import { i18n } from '@kbn/i18n'; +import { NotificationsStart } from 'kibana/public'; +import { IngestManagerStart } from '../../../../../ingest_manager/public'; + +export const Setup: React.FunctionComponent<{ + ingestManager: IngestManagerStart; + notifications: NotificationsStart; +}> = ({ ingestManager, notifications }) => { + React.useEffect(() => { + const defaultText = i18n.translate('xpack.endpoint.ingestToastMessage', { + defaultMessage: 'Ingest Manager failed during its setup.', + }); + + const title = i18n.translate('xpack.endpoint.ingestToastTitle', { + defaultMessage: 'App failed to initialize', + }); + + const displayToastWithModal = (text: string) => { + const errorText = new Error(defaultText); + // we're leveraging the notification's error toast which is usually used for displaying stack traces of an + // actually Error. Instead of displaying a stack trace we'll display the more detailed error text when the + // user clicks `See the full error` button to see the modal + errorText.stack = text; + notifications.toasts.addError(errorText, { + title, + }); + }; + + const displayToast = () => { + notifications.toasts.addDanger({ + title, + text: defaultText, + }); + }; + + if (!ingestManager.success) { + if (ingestManager.error) { + displayToastWithModal(ingestManager.error.message); + } else { + displayToast(); + } + } + }, [ingestManager, notifications.toasts]); + + return null; +}; diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts index ee5bbe71ae8aa..9964454add801 100644 --- a/x-pack/plugins/endpoint/public/plugin.ts +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -8,6 +8,7 @@ import { Plugin, CoreSetup, AppMountParameters, CoreStart } from 'kibana/public' import { EmbeddableSetup } from 'src/plugins/embeddable/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { i18n } from '@kbn/i18n'; +import { IngestManagerStart } from '../../ingest_manager/public'; import { ResolverEmbeddableFactory } from './embeddables/resolver'; export type EndpointPluginStart = void; @@ -18,6 +19,7 @@ export interface EndpointPluginSetupDependencies { } export interface EndpointPluginStartDependencies { data: DataPublicPluginStart; + ingestManager: IngestManagerStart; } /** diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index efa6621001038..5524e7505d74b 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -252,6 +252,7 @@ export enum IngestAssetType { export enum DefaultPackages { base = 'base', system = 'system', + endpoint = 'endpoint', } export interface IndexTemplate { diff --git a/x-pack/plugins/ingest_manager/public/index.ts b/x-pack/plugins/ingest_manager/public/index.ts index aa1e0e79e548b..c11ad60dffee4 100644 --- a/x-pack/plugins/ingest_manager/public/index.ts +++ b/x-pack/plugins/ingest_manager/public/index.ts @@ -6,6 +6,8 @@ import { PluginInitializerContext } from 'src/core/public'; import { IngestManagerPlugin } from './plugin'; +export { IngestManagerStart } from './plugin'; + export const plugin = (initializerContext: PluginInitializerContext) => { return new IngestManagerPlugin(initializerContext); }; diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index d7be1c1f1fe6e..77bba0bb0f990 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -17,11 +17,20 @@ import { LicensingPluginSetup } from '../../licensing/public'; import { PLUGIN_ID } from '../common/constants'; import { IngestManagerConfigType } from '../common/types'; +import { setupRouteService } from '../common'; export { IngestManagerConfigType } from '../common/types'; export type IngestManagerSetup = void; -export type IngestManagerStart = void; +/** + * Describes public IngestManager plugin contract returned at the `start` stage. + */ +export interface IngestManagerStart { + success: boolean; + error?: { + message: string; + }; +} export interface IngestManagerSetupDeps { licensing: LicensingPluginSetup; @@ -61,7 +70,14 @@ export class IngestManagerPlugin }); } - public start(core: CoreStart) {} + public async start(core: CoreStart): Promise { + try { + const { isInitialized: success } = await core.http.post(setupRouteService.getSetupPath()); + return { success }; + } catch (error) { + return { success: false, error: { message: error.body?.message || 'Unknown error' } }; + } + } public stop() {} } diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 7943da07716a1..061c9e4a0d921 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -43,6 +43,8 @@ const onlyNotInCoverageTests = [ require.resolve('../test/licensing_plugin/config.ts'), require.resolve('../test/licensing_plugin/config.public.ts'), require.resolve('../test/licensing_plugin/config.legacy.ts'), + require.resolve('../test/functional_endpoint_ingest_failure/config.ts'), + require.resolve('../test/functional_endpoint/config.ts'), ]; require('@kbn/plugin-helpers').babelRegister(); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index cff555feace18..bc9a67da731cc 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -57,7 +57,6 @@ export default async function({ readConfigFile }) { resolve(__dirname, './apps/cross_cluster_replication'), resolve(__dirname, './apps/remote_clusters'), resolve(__dirname, './apps/transform'), - resolve(__dirname, './apps/endpoint'), // This license_management file must be last because it is destructive. resolve(__dirname, './apps/license_management'), ], @@ -88,7 +87,6 @@ export default async function({ readConfigFile }) { '--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"', '--telemetry.banner=false', '--timelion.ui.enabled=true', - '--xpack.endpoint.enabled=true', ], }, uiSettings: { @@ -199,9 +197,6 @@ export default async function({ readConfigFile }) { pathname: '/app/kibana/', hash: '/management/elasticsearch/transform', }, - endpoint: { - pathname: '/app/endpoint', - }, }, // choose where esArchiver should load archives from diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json index 64dc395ab69a4..7068c24a4b26c 100644 --- a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json +++ b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json @@ -389,7 +389,8 @@ "type": "nested" }, "file_extension": { - "type": "long" + "ignore_above": 1024, + "type": "keyword" }, "project_file": { "properties": { diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 07c5719ae53c5..782d57adea770 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -46,8 +46,6 @@ import { LensPageProvider } from './lens_page'; import { InfraMetricExplorerProvider } from './infra_metric_explorer'; import { RoleMappingsPageProvider } from './role_mappings_page'; import { SpaceSelectorPageProvider } from './space_selector_page'; -import { EndpointPageProvider } from './endpoint_page'; -import { EndpointAlertsPageProvider } from './endpoint_alerts_page'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -81,6 +79,4 @@ export const pageObjects = { copySavedObjectsToSpace: CopySavedObjectsToSpacePageProvider, lens: LensPageProvider, roleMappings: RoleMappingsPageProvider, - endpoint: EndpointPageProvider, - endpointAlerts: EndpointAlertsPageProvider, }; diff --git a/x-pack/test/functional/apps/endpoint/alerts.ts b/x-pack/test/functional_endpoint/apps/endpoint/alerts.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/alerts.ts rename to x-pack/test/functional_endpoint/apps/endpoint/alerts.ts diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts rename to x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/index.ts b/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/index.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/feature_controls/index.ts rename to x-pack/test/functional_endpoint/apps/endpoint/feature_controls/index.ts diff --git a/x-pack/test/functional/apps/endpoint/header_nav.ts b/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/header_nav.ts rename to x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts diff --git a/x-pack/test/functional/apps/endpoint/host_list.ts b/x-pack/test/functional_endpoint/apps/endpoint/host_list.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/host_list.ts rename to x-pack/test/functional_endpoint/apps/endpoint/host_list.ts diff --git a/x-pack/test/functional/apps/endpoint/index.ts b/x-pack/test/functional_endpoint/apps/endpoint/index.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/index.ts rename to x-pack/test/functional_endpoint/apps/endpoint/index.ts diff --git a/x-pack/test/functional/apps/endpoint/landing_page.ts b/x-pack/test/functional_endpoint/apps/endpoint/landing_page.ts similarity index 72% rename from x-pack/test/functional/apps/endpoint/landing_page.ts rename to x-pack/test/functional_endpoint/apps/endpoint/landing_page.ts index 65af91feae407..b4da4631aa60b 100644 --- a/x-pack/test/functional/apps/endpoint/landing_page.ts +++ b/x-pack/test/functional_endpoint/apps/endpoint/landing_page.ts @@ -7,8 +7,9 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -export default ({ getPageObjects }: FtrProviderContext) => { +export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'endpoint']); + const testSubjects = getService('testSubjects'); describe('Endpoint landing page', function() { this.tags('ciGroup7'); @@ -20,5 +21,9 @@ export default ({ getPageObjects }: FtrProviderContext) => { const welcomeEndpointMessage = await pageObjects.endpoint.welcomeEndpointTitle(); expect(welcomeEndpointMessage).to.be('Hello World'); }); + + it('Does not display a toast indicating that the ingest manager failed to initialize', async () => { + await testSubjects.missingOrFail('euiToastHeader'); + }); }); }; diff --git a/x-pack/test/functional/apps/endpoint/policy_list.ts b/x-pack/test/functional_endpoint/apps/endpoint/policy_list.ts similarity index 100% rename from x-pack/test/functional/apps/endpoint/policy_list.ts rename to x-pack/test/functional_endpoint/apps/endpoint/policy_list.ts diff --git a/x-pack/test/functional_endpoint/config.ts b/x-pack/test/functional_endpoint/config.ts new file mode 100644 index 0000000000000..37bf57b67b47e --- /dev/null +++ b/x-pack/test/functional_endpoint/config.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { pageObjects } from './page_objects'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + + return { + ...xpackFunctionalConfig.getAll(), + pageObjects, + testFiles: [resolve(__dirname, './apps/endpoint')], + junit: { + reportName: 'X-Pack Endpoint Functional Tests', + }, + apps: { + ...xpackFunctionalConfig.get('apps'), + endpoint: { + pathname: '/app/endpoint', + }, + }, + kbnTestServer: { + ...xpackFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), + '--xpack.endpoint.enabled=true', + '--xpack.ingestManager.enabled=true', + '--xpack.ingestManager.fleet.enabled=true', + ], + }, + }; +} diff --git a/x-pack/test/functional_endpoint/ftr_provider_context.d.ts b/x-pack/test/functional_endpoint/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..21ab5d5a4e554 --- /dev/null +++ b/x-pack/test/functional_endpoint/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { pageObjects } from './page_objects'; +import { services } from '../functional/services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/functional/page_objects/endpoint_alerts_page.ts b/x-pack/test/functional_endpoint/page_objects/endpoint_alerts_page.ts similarity index 100% rename from x-pack/test/functional/page_objects/endpoint_alerts_page.ts rename to x-pack/test/functional_endpoint/page_objects/endpoint_alerts_page.ts diff --git a/x-pack/test/functional/page_objects/endpoint_page.ts b/x-pack/test/functional_endpoint/page_objects/endpoint_page.ts similarity index 100% rename from x-pack/test/functional/page_objects/endpoint_page.ts rename to x-pack/test/functional_endpoint/page_objects/endpoint_page.ts diff --git a/x-pack/test/functional_endpoint/page_objects/index.ts b/x-pack/test/functional_endpoint/page_objects/index.ts new file mode 100644 index 0000000000000..8138ce2eeccb3 --- /dev/null +++ b/x-pack/test/functional_endpoint/page_objects/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; +import { EndpointPageProvider } from './endpoint_page'; +import { EndpointAlertsPageProvider } from './endpoint_alerts_page'; + +export const pageObjects = { + ...xpackFunctionalPageObjects, + endpoint: EndpointPageProvider, + endpointAlerts: EndpointAlertsPageProvider, +}; diff --git a/x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/index.ts b/x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/index.ts new file mode 100644 index 0000000000000..ae35f3e525461 --- /dev/null +++ b/x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('endpoint when the ingest manager fails to setup correctly', function() { + this.tags('ciGroup7'); + + loadTestFile(require.resolve('./landing_page')); + }); +} diff --git a/x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/landing_page.ts b/x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/landing_page.ts new file mode 100644 index 0000000000000..d29250ca3bed4 --- /dev/null +++ b/x-pack/test/functional_endpoint_ingest_failure/apps/endpoint/landing_page.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + describe('home page', function() { + const pageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + before(async () => { + await pageObjects.common.navigateToApp('endpoint'); + }); + + it('displays an error toast', async () => { + await testSubjects.existOrFail('euiToastHeader'); + }); + }); +}; diff --git a/x-pack/test/functional_endpoint_ingest_failure/config.ts b/x-pack/test/functional_endpoint_ingest_failure/config.ts new file mode 100644 index 0000000000000..a2055a4bbdc18 --- /dev/null +++ b/x-pack/test/functional_endpoint_ingest_failure/config.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. + */ + +import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional_endpoint/config.ts') + ); + + return { + ...xpackFunctionalConfig.getAll(), + testFiles: [resolve(__dirname, './apps/endpoint')], + junit: { + reportName: 'X-Pack Endpoint Without Ingest Functional Tests', + }, + kbnTestServer: { + ...xpackFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), + // use a bogus port so the ingest manager setup will fail + '--xpack.ingestManager.epm.registryUrl=http://127.0.0.1:12345', + ], + }, + }; +} diff --git a/x-pack/test/functional_endpoint_ingest_failure/ftr_provider_context.d.ts b/x-pack/test/functional_endpoint_ingest_failure/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..0e4b47471d419 --- /dev/null +++ b/x-pack/test/functional_endpoint_ingest_failure/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { pageObjects } from '../functional_endpoint/page_objects'; +import { services } from '../functional/services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index 6c3c496da71f6..aa3c9bd24842a 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -14,7 +14,9 @@ import { pageObjects } from './page_objects'; /* eslint-disable import/no-default-export */ export default async function({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional_endpoint/config.ts') + ); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(resolve(__dirname, 'plugins')); From abe3ccf1cc45213c2991af83e261a12ce19e91d1 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Thu, 9 Apr 2020 17:00:45 +0300 Subject: [PATCH 52/81] [Table Vis] Fix visualization overflow (#62630) * Fix data table vis overflowing * Change overflow to hidden --- src/plugins/vis_default_editor/public/_default.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/vis_default_editor/public/_default.scss b/src/plugins/vis_default_editor/public/_default.scss index 985381eeb56a5..2187baea547da 100644 --- a/src/plugins/vis_default_editor/public/_default.scss +++ b/src/plugins/vis_default_editor/public/_default.scss @@ -72,6 +72,7 @@ display: flex; flex-basis: 100%; flex: 1; + overflow: hidden; @include euiBreakpoint('xs', 's', 'm') { // If we are on a small screen we force the visualization to take 100% width. From f9ba963af90d41b74e8d7dde9981c9a5cee53127 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 9 Apr 2020 10:33:55 -0400 Subject: [PATCH 53/81] [ML] DF Analytics: update memory estimate after adding exclude fields (#62850) * update mml estimate when excludes changes * ensure manually input mml is validated once estimated mml loads --- .../create_analytics_form.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 0c83dfb6a2346..199100d8b5ab0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -55,7 +55,14 @@ export const CreateAnalyticsForm: FC = ({ actions, sta const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const { setFormState, setEstimatedModelMemoryLimit } = actions; const mlContext = useMlContext(); - const { form, indexPatternsMap, isAdvancedEditorEnabled, isJobCreated, requestMessages } = state; + const { + estimatedModelMemoryLimit, + form, + indexPatternsMap, + isAdvancedEditorEnabled, + isJobCreated, + requestMessages, + } = state; const forceInput = useRef(null); const firstUpdate = useRef(true); @@ -152,6 +159,9 @@ export const CreateAnalyticsForm: FC = ({ actions, sta const debouncedGetExplainData = debounce(async () => { const shouldUpdateModelMemoryLimit = !firstUpdate.current || !modelMemoryLimit; + const shouldUpdateEstimatedMml = + !firstUpdate.current || !modelMemoryLimit || estimatedModelMemoryLimit === ''; + if (firstUpdate.current) { firstUpdate.current = false; } @@ -167,13 +177,12 @@ export const CreateAnalyticsForm: FC = ({ actions, sta const jobConfig = getJobConfigFromFormState(form); delete jobConfig.dest; delete jobConfig.model_memory_limit; - delete jobConfig.analyzed_fields; const resp: DfAnalyticsExplainResponse = await ml.dataFrameAnalytics.explainDataFrameAnalytics( jobConfig ); const expectedMemoryWithoutDisk = resp.memory_estimation?.expected_memory_without_disk; - if (shouldUpdateModelMemoryLimit) { + if (shouldUpdateEstimatedMml) { setEstimatedModelMemoryLimit(expectedMemoryWithoutDisk); } @@ -340,7 +349,14 @@ export const CreateAnalyticsForm: FC = ({ actions, sta return () => { debouncedGetExplainData.cancel(); }; - }, [jobType, sourceIndex, sourceIndexNameEmpty, dependentVariable, trainingPercent]); + }, [ + jobType, + sourceIndex, + sourceIndexNameEmpty, + dependentVariable, + trainingPercent, + JSON.stringify(excludes), + ]); // Temp effect to close the context menu popover on Clone button click useEffect(() => { From 90d2b18bc5c4db75311733aea305c87b06d43675 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Thu, 9 Apr 2020 07:43:36 -0700 Subject: [PATCH 54/81] Add Data - Adding cloud reset password link to cloud instructions (#62835) * Adding cloud reset password link to cloud filebeat instructions * Auditbeat gets the cool reset password link * And the other beats instructions get the awesome password reset link * Changing the i18n id to more closely match the on-prem cloud id * Changing text for forgot password * Removing now unused translations * "Forgot your password" -> "Forgot the password" * "Elastic Cloud UI" -> "Elastic Cloud" Co-authored-by: Elastic Machine --- .../instructions/auditbeat_instructions.ts | 33 +++---------------- .../instructions/cloud_instructions.ts | 31 +++++++++++++++++ .../instructions/filebeat_instructions.ts | 33 +++---------------- .../instructions/functionbeat_instructions.ts | 17 ++-------- .../instructions/heartbeat_instructions.ts | 33 +++---------------- .../instructions/metricbeat_instructions.ts | 33 +++---------------- .../instructions/winlogbeat_instructions.ts | 9 ++--- x-pack/plugins/cloud/public/plugin.ts | 5 +-- x-pack/plugins/cloud/server/config.ts | 2 ++ .../translations/translations/ja-JP.json | 19 ----------- .../translations/translations/zh-CN.json | 19 ----------- 11 files changed, 61 insertions(+), 173 deletions(-) create mode 100644 src/plugins/home/server/tutorials/instructions/cloud_instructions.ts diff --git a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts index 4c85ad3985b3d..2a6cfa0358709 100644 --- a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts @@ -22,6 +22,7 @@ import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; +import { cloudPasswordAndResetLink } from './cloud_instructions'; export const createAuditbeatInstructions = (context?: TutorialContext) => ({ INSTALL: { @@ -305,13 +306,7 @@ export const createAuditbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.auditbeatCloudInstructions.config.osxTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, DEB: { title: i18n.translate('home.tutorials.common.auditbeatCloudInstructions.config.debTitle', { @@ -327,13 +322,7 @@ export const createAuditbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.auditbeatCloudInstructions.config.debTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, RPM: { title: i18n.translate('home.tutorials.common.auditbeatCloudInstructions.config.rpmTitle', { @@ -349,13 +338,7 @@ export const createAuditbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.auditbeatCloudInstructions.config.rpmTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, WINDOWS: { title: i18n.translate( @@ -374,13 +357,7 @@ export const createAuditbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, }, }); diff --git a/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts b/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts new file mode 100644 index 0000000000000..a18e21d2b43dd --- /dev/null +++ b/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts @@ -0,0 +1,31 @@ +/* + * 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'; + +export const cloudPasswordAndResetLink = i18n.translate( + 'home.tutorials.common.cloudInstructions.passwordAndResetLink', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user.' + + `\\{#config.cloud.resetPasswordUrl\\} + Forgot the password? [Reset in Elastic Cloud](\\{config.cloud.resetPasswordUrl\\}). + \\{/config.cloud.resetPasswordUrl\\}`, + values: { passwordTemplate: '``' }, + } +); diff --git a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts index 66efa36ec9bcd..0e99033b2ea69 100644 --- a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts @@ -22,6 +22,7 @@ import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; +import { cloudPasswordAndResetLink } from './cloud_instructions'; export const createFilebeatInstructions = (context?: TutorialContext) => ({ INSTALL: { @@ -299,13 +300,7 @@ export const createFilebeatCloudInstructions = () => ({ }, }), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.filebeatCloudInstructions.config.osxTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, DEB: { title: i18n.translate('home.tutorials.common.filebeatCloudInstructions.config.debTitle', { @@ -318,13 +313,7 @@ export const createFilebeatCloudInstructions = () => ({ }, }), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.filebeatCloudInstructions.config.debTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, RPM: { title: i18n.translate('home.tutorials.common.filebeatCloudInstructions.config.rpmTitle', { @@ -337,13 +326,7 @@ export const createFilebeatCloudInstructions = () => ({ }, }), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.filebeatCloudInstructions.config.rpmTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, WINDOWS: { title: i18n.translate('home.tutorials.common.filebeatCloudInstructions.config.windowsTitle', { @@ -359,13 +342,7 @@ export const createFilebeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.filebeatCloudInstructions.config.windowsTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, }, }); diff --git a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts index ee13b9c5eefd8..06ff84146b5d8 100644 --- a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts @@ -22,6 +22,7 @@ import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; +import { cloudPasswordAndResetLink } from './cloud_instructions'; export const createFunctionbeatInstructions = (context?: TutorialContext) => ({ INSTALL: { @@ -200,13 +201,7 @@ export const createFunctionbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.functionbeatCloudInstructions.config.osxTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, WINDOWS: { title: i18n.translate( @@ -225,13 +220,7 @@ export const createFunctionbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.functionbeatCloudInstructions.config.windowsTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, }, }); diff --git a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts index 33f5defc0273f..fa5bf5df13b6b 100644 --- a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts @@ -22,6 +22,7 @@ import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; +import { cloudPasswordAndResetLink } from './cloud_instructions'; export const createHeartbeatInstructions = (context?: TutorialContext) => ({ INSTALL: { @@ -280,13 +281,7 @@ export const createHeartbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.heartbeatCloudInstructions.config.osxTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, DEB: { title: i18n.translate('home.tutorials.common.heartbeatCloudInstructions.config.debTitle', { @@ -302,13 +297,7 @@ export const createHeartbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.heartbeatCloudInstructions.config.debTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, RPM: { title: i18n.translate('home.tutorials.common.heartbeatCloudInstructions.config.rpmTitle', { @@ -324,13 +313,7 @@ export const createHeartbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.heartbeatCloudInstructions.config.rpmTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, WINDOWS: { title: i18n.translate( @@ -349,13 +332,7 @@ export const createHeartbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.heartbeatCloudInstructions.config.windowsTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, }, }); diff --git a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts index 9fdc70e0703a4..651405941610f 100644 --- a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts @@ -22,6 +22,7 @@ import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; +import { cloudPasswordAndResetLink } from './cloud_instructions'; export const createMetricbeatInstructions = (context?: TutorialContext) => ({ INSTALL: { @@ -295,13 +296,7 @@ export const createMetricbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.metricbeatCloudInstructions.config.osxTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, DEB: { title: i18n.translate('home.tutorials.common.metricbeatCloudInstructions.config.debTitle', { @@ -317,13 +312,7 @@ export const createMetricbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.metricbeatCloudInstructions.config.debTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, RPM: { title: i18n.translate('home.tutorials.common.metricbeatCloudInstructions.config.rpmTitle', { @@ -339,13 +328,7 @@ export const createMetricbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.metricbeatCloudInstructions.config.rpmTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, WINDOWS: { title: i18n.translate( @@ -364,13 +347,7 @@ export const createMetricbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.metricbeatCloudInstructions.config.windowsTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, }, }); diff --git a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts index 9d7d0660d3d6c..27d7822e080a3 100644 --- a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts @@ -22,6 +22,7 @@ import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; +import { cloudPasswordAndResetLink } from './cloud_instructions'; export const createWinlogbeatInstructions = (context?: TutorialContext) => ({ INSTALL: { @@ -130,13 +131,7 @@ export const createWinlogbeatCloudInstructions = () => ({ } ), commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: i18n.translate( - 'home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPost', - { - defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', - values: { passwordTemplate: '``' }, - } - ), + textPost: cloudPasswordAndResetLink, }, }, }); diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 2b8247066bfc3..62e21392f7110 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -11,6 +11,7 @@ import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; interface CloudConfigType { id?: string; + resetPasswordUrl?: string; } interface CloudSetupDependencies { @@ -26,13 +27,13 @@ export class CloudPlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { - const { id } = this.initializerContext.config.get(); + const { id, resetPasswordUrl } = this.initializerContext.config.get(); const isCloudEnabled = getIsCloudEnabled(id); if (home) { home.environment.update({ cloud: isCloudEnabled }); if (isCloudEnabled) { - home.tutorials.setVariable('cloud', { id }); + home.tutorials.setVariable('cloud', { id, resetPasswordUrl }); } } diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index 77e493dc3b7dc..d899b45aebdfe 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -21,6 +21,7 @@ const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), id: schema.maybe(schema.string()), apm: schema.maybe(apmConfigSchema), + resetPasswordUrl: schema.maybe(schema.string()), }); export type CloudConfigType = TypeOf; @@ -28,6 +29,7 @@ export type CloudConfigType = TypeOf; export const config: PluginConfigDescriptor = { exposeToBrowser: { id: true, + resetPasswordUrl: true, }, schema: configSchema, }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e63e1c8ad2c91..16c4b11bb6a6e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1196,16 +1196,12 @@ "home.tutorials.common.auditbeat.cloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.auditbeat.premCloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.auditbeat.premInstructions.gettingStarted.title": "はじめに", - "home.tutorials.common.auditbeatCloudInstructions.config.debTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.auditbeatCloudInstructions.config.debTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatCloudInstructions.config.debTitle": "構成を編集する", - "home.tutorials.common.auditbeatCloudInstructions.config.osxTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.auditbeatCloudInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatCloudInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.auditbeatCloudInstructions.config.rpmTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.auditbeatCloudInstructions.config.rpmTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatCloudInstructions.config.rpmTitle": "構成を編集する", - "home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.auditbeatInstructions.config.debTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。", @@ -1247,16 +1243,12 @@ "home.tutorials.common.filebeat.cloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.filebeat.premCloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.filebeat.premInstructions.gettingStarted.title": "はじめに", - "home.tutorials.common.filebeatCloudInstructions.config.debTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.filebeatCloudInstructions.config.debTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatCloudInstructions.config.debTitle": "構成を編集する", - "home.tutorials.common.filebeatCloudInstructions.config.osxTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.filebeatCloudInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatCloudInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.filebeatCloudInstructions.config.rpmTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.filebeatCloudInstructions.config.rpmTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatCloudInstructions.config.rpmTitle": "構成を編集する", - "home.tutorials.common.filebeatCloudInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.filebeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatCloudInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.filebeatEnableInstructions.debTextPost": "「/etc/filebeat/modules.d/{moduleName}.yml」ファイルで設定を変更します。", @@ -1311,10 +1303,8 @@ "home.tutorials.common.functionbeatAWSInstructions.textPost": "「」と「」がアカウント資格情報、「us-east-1」がご希望の地域です。", "home.tutorials.common.functionbeatAWSInstructions.textPre": "環境で AWS アカウント認証情報を設定します。", "home.tutorials.common.functionbeatAWSInstructions.title": "AWS 認証情報の設定", - "home.tutorials.common.functionbeatCloudInstructions.config.osxTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.functionbeatCloudInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.functionbeatCloudInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.functionbeatCloudInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.functionbeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.functionbeatCloudInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.functionbeatEnableOnPremInstructions.defaultTextPost": "「」が投入するロググループの名前で、「」が Functionbeat デプロイのステージングに使用されるが有効な S3 バケット名です。", @@ -1345,16 +1335,12 @@ "home.tutorials.common.heartbeat.cloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.heartbeat.premCloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.heartbeat.premInstructions.gettingStarted.title": "はじめに", - "home.tutorials.common.heartbeatCloudInstructions.config.debTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.heartbeatCloudInstructions.config.debTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatCloudInstructions.config.debTitle": "構成を編集する", - "home.tutorials.common.heartbeatCloudInstructions.config.osxTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.heartbeatCloudInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatCloudInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.heartbeatCloudInstructions.config.rpmTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.heartbeatCloudInstructions.config.rpmTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatCloudInstructions.config.rpmTitle": "構成を編集する", - "home.tutorials.common.heartbeatCloudInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.heartbeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatCloudInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.heartbeatEnableCloudInstructions.debTextPre": "「heartbeat.yml」ファイルの「heartbeat.monitors」設定を変更します。", @@ -1414,16 +1400,12 @@ "home.tutorials.common.metricbeat.cloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.metricbeat.premCloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.metricbeat.premInstructions.gettingStarted.title": "はじめに", - "home.tutorials.common.metricbeatCloudInstructions.config.debTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.metricbeatCloudInstructions.config.debTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatCloudInstructions.config.debTitle": "構成を編集する", - "home.tutorials.common.metricbeatCloudInstructions.config.osxTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.metricbeatCloudInstructions.config.osxTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatCloudInstructions.config.osxTitle": "構成を編集する", - "home.tutorials.common.metricbeatCloudInstructions.config.rpmTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.metricbeatCloudInstructions.config.rpmTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatCloudInstructions.config.rpmTitle": "構成を編集する", - "home.tutorials.common.metricbeatCloudInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.metricbeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatCloudInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.metricbeatEnableInstructions.debTextPost": "「/etc/metricbeat/modules.d/{moduleName}.yml」ファイルで設定を変更します。", @@ -1478,7 +1460,6 @@ "home.tutorials.common.winlogbeat.cloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.winlogbeat.premCloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.winlogbeat.premInstructions.gettingStarted.title": "はじめに", - "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワードです。", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.winlogbeatInstructions.config.windowsTextPost": "{passwordTemplate} が「Elastic」ユーザーのパスワード、{esUrlTemplate} が Elasticsearch の URL、{kibanaUrlTemplate} が Kibana の URL です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index cc75ceb988d97..c81fad386ac2f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1197,16 +1197,12 @@ "home.tutorials.common.auditbeat.cloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.auditbeat.premCloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.auditbeat.premInstructions.gettingStarted.title": "入门", - "home.tutorials.common.auditbeatCloudInstructions.config.debTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.auditbeatCloudInstructions.config.debTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.auditbeatCloudInstructions.config.debTitle": "编辑配置", - "home.tutorials.common.auditbeatCloudInstructions.config.osxTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.auditbeatCloudInstructions.config.osxTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.auditbeatCloudInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.auditbeatCloudInstructions.config.rpmTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.auditbeatCloudInstructions.config.rpmTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.auditbeatCloudInstructions.config.rpmTitle": "编辑配置", - "home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.auditbeatInstructions.config.debTextPost": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。", @@ -1248,16 +1244,12 @@ "home.tutorials.common.filebeat.cloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.filebeat.premCloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.filebeat.premInstructions.gettingStarted.title": "入门", - "home.tutorials.common.filebeatCloudInstructions.config.debTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.filebeatCloudInstructions.config.debTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.filebeatCloudInstructions.config.debTitle": "编辑配置", - "home.tutorials.common.filebeatCloudInstructions.config.osxTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.filebeatCloudInstructions.config.osxTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.filebeatCloudInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.filebeatCloudInstructions.config.rpmTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.filebeatCloudInstructions.config.rpmTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.filebeatCloudInstructions.config.rpmTitle": "编辑配置", - "home.tutorials.common.filebeatCloudInstructions.config.windowsTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.filebeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.filebeatCloudInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.filebeatEnableInstructions.debTextPost": "在 `/etc/filebeat/modules.d/{moduleName}.yml` 文件中修改设置。", @@ -1312,10 +1304,8 @@ "home.tutorials.common.functionbeatAWSInstructions.textPost": "其中 `` 和 `` 是您的帐户凭据,`us-east-1` 是所需的地区。", "home.tutorials.common.functionbeatAWSInstructions.textPre": "在环境中设置您的 AWS 帐户凭据:", "home.tutorials.common.functionbeatAWSInstructions.title": "设置 AWS 凭据", - "home.tutorials.common.functionbeatCloudInstructions.config.osxTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.functionbeatCloudInstructions.config.osxTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.functionbeatCloudInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.functionbeatCloudInstructions.config.windowsTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.functionbeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.functionbeatCloudInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.functionbeatEnableOnPremInstructions.defaultTextPost": "其中 `` 是要采集的日志组名称,`` 是将用于暂存 Functionbeat 部署的有效 S3 存储桶名称。", @@ -1346,16 +1336,12 @@ "home.tutorials.common.heartbeat.cloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.heartbeat.premCloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.heartbeat.premInstructions.gettingStarted.title": "入门", - "home.tutorials.common.heartbeatCloudInstructions.config.debTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.heartbeatCloudInstructions.config.debTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.heartbeatCloudInstructions.config.debTitle": "编辑配置", - "home.tutorials.common.heartbeatCloudInstructions.config.osxTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.heartbeatCloudInstructions.config.osxTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.heartbeatCloudInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.heartbeatCloudInstructions.config.rpmTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.heartbeatCloudInstructions.config.rpmTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.heartbeatCloudInstructions.config.rpmTitle": "编辑配置", - "home.tutorials.common.heartbeatCloudInstructions.config.windowsTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.heartbeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.heartbeatCloudInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.heartbeatEnableCloudInstructions.debTextPre": "在 `heartbeat.yml` 文件中编辑 `heartbeat.monitors` 设置。", @@ -1415,16 +1401,12 @@ "home.tutorials.common.metricbeat.cloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.metricbeat.premCloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.metricbeat.premInstructions.gettingStarted.title": "入门", - "home.tutorials.common.metricbeatCloudInstructions.config.debTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.metricbeatCloudInstructions.config.debTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.metricbeatCloudInstructions.config.debTitle": "编辑配置", - "home.tutorials.common.metricbeatCloudInstructions.config.osxTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.metricbeatCloudInstructions.config.osxTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.metricbeatCloudInstructions.config.osxTitle": "编辑配置", - "home.tutorials.common.metricbeatCloudInstructions.config.rpmTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.metricbeatCloudInstructions.config.rpmTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.metricbeatCloudInstructions.config.rpmTitle": "编辑配置", - "home.tutorials.common.metricbeatCloudInstructions.config.windowsTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.metricbeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.metricbeatCloudInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.metricbeatEnableInstructions.debTextPost": "在 `/etc/metricbeat/modules.d/{moduleName}.yml` 文件中修改设置。", @@ -1479,7 +1461,6 @@ "home.tutorials.common.winlogbeat.cloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.winlogbeat.premCloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.winlogbeat.premInstructions.gettingStarted.title": "入门", - "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPost": "其中 {passwordTemplate} 是 `elastic` 用户的密码。", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPre": "修改 {path} 以设置 Elastic Cloud 的连接信息:", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.winlogbeatInstructions.config.windowsTextPost": "其中,{passwordTemplate} 是 `elastic` 用户的密码,{esUrlTemplate} 是 Elasticsearch 的 URL,{kibanaUrlTemplate} 是 Kibana 的 URL。", From 80384c3209d8b49ed43fd2b505cd37f8cbf38ac7 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 9 Apr 2020 07:50:11 -0700 Subject: [PATCH 55/81] Exposed AddMessageVariables as separate component (#63007) * Exposed AddMessageVariables as separate component and added styles to allow to handle bigger list of messageVariables * Fixed failing tests and styles * Fixed due to comments --- .../translations/translations/ja-JP.json | 15 -- .../translations/translations/zh-CN.json | 15 -- .../components/add_message_variables.scss | 4 + .../components/add_message_variables.tsx | 69 +++++++ .../components/builtin_action_types/email.tsx | 62 +++--- .../builtin_action_types/es_index.test.tsx | 4 +- .../builtin_action_types/es_index.tsx | 62 ++---- .../builtin_action_types/pagerduty.tsx | 182 ++++++------------ .../builtin_action_types/server_log.tsx | 63 ++---- .../components/builtin_action_types/slack.tsx | 66 ++----- .../builtin_action_types/webhook.test.tsx | 2 +- .../builtin_action_types/webhook.tsx | 54 +----- 12 files changed, 211 insertions(+), 387 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 16c4b11bb6a6e..d357e40c02934 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15846,7 +15846,6 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "タイミング", "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "タイミング", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle": "メールに送信", - "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.addVariablePopoverButton": "変数を追加", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "サーバーからメールを送信します。", "xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText": "送信元は有効なメールアドレスではありません。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText": "To、Cc、または Bcc のエントリーがありません。 1 つ以上のエントリーが必要です。", @@ -15875,14 +15874,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshTooltip": "このチェックボックスは更新インデックス値を設定します。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.selectMessageText": "データを Elasticsearch にインデックスしてください。", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "PagerDuty に送信", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton1": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton2": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton3": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton4": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton5": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton6": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton7": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariableTitle": "アラート変数を追加", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel": "API URL", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "クラス (任意)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.componentTextFieldLabel": "コンポーネント(任意)", @@ -15906,14 +15897,10 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel": "まとめ", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel": "タイムスタンプ (任意)", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.actionTypeTitle": "サーバーログに送信", - "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.addVariablePopoverButton": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.addVariableTitle": "変数を追加", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logLevelFieldLabel": "レベル", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "メッセージ", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText": "Kibana ログにメッセージを追加します。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "Slack に送信", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.addVariablePopoverButton": "アラート変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.addVariableTitle": "アラート変数を追加", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText": "Web フック URL が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel": "メッセージ", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText": "Slack チャネルにメッセージを送信します。", @@ -15922,8 +15909,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle": "Web フックデータ", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader": "ヘッダーを追加", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton": "追加", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariablePopoverButton": "変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariableTitle": "変数を追加", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyCodeEditorAriaLabel": "コードエディター", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyFieldLabel": "本文", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.deleteHeaderButton": "削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c81fad386ac2f..839c89f3b1cae 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15850,7 +15850,6 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "当", "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "当", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle": "发送到电子邮件", - "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.addVariablePopoverButton": "添加变量", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "从您的服务器发送电子邮件。", "xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText": "发送者电子邮件地址无效。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText": "未输入收件人、抄送、密送。 至少需要输入一个。", @@ -15879,14 +15878,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshTooltip": "此复选框设置刷新索引值。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.selectMessageText": "将数据索引到 Elasticsearch 中。", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "发送到 PagerDuty", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton1": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton2": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton3": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton4": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton5": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton6": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariablePopoverButton7": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariableTitle": "添加告警变量", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel": "API URL", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "类(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.componentTextFieldLabel": "组件(可选)", @@ -15910,14 +15901,10 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel": "摘要", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel": "时间戳(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.actionTypeTitle": "发送到服务器日志", - "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.addVariablePopoverButton": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.addVariableTitle": "添加变量", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logLevelFieldLabel": "级别", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "消息", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText": "将消息添加到 Kibana 日志。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "发送到 Slack", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.addVariablePopoverButton": "添加告警变量", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.addVariableTitle": "添加告警变量", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText": "Webhook URL 必填。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel": "消息", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText": "向 Slack 频道或用户发送消息。", @@ -15926,8 +15913,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle": "Webhook 数据", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader": "添加标头", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton": "添加", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariablePopoverButton": "添加变量", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariableTitle": "添加变量", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyCodeEditorAriaLabel": "代码编辑器", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyFieldLabel": "正文", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.deleteHeaderButton": "删除", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss new file mode 100644 index 0000000000000..996f21c4b6b09 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss @@ -0,0 +1,4 @@ +.messageVariablesPanel { + @include euiYScrollWithShadows; + max-height: $euiSize * 20; +} \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx new file mode 100644 index 0000000000000..ab9b5c2586c17 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; +import './add_message_variables.scss'; + +interface Props { + messageVariables: string[] | undefined; + paramsProperty: string; + onSelectEventHandler: (variable: string) => void; +} + +export const AddMessageVariables: React.FunctionComponent = ({ + messageVariables, + paramsProperty, + onSelectEventHandler, +}) => { + const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); + + const getMessageVariables = () => + messageVariables?.map((variable: string) => ( + { + onSelectEventHandler(variable); + setIsVariablesPopoverOpen(false); + }} + > + {`{{${variable}}}`} + + )); + + const addVariableButtonTitle = i18n.translate( + 'xpack.triggersActionsUI.components.addMessageVariables.addVariableTitle', + { + defaultMessage: 'Add alert variable', + } + ); + + return ( + setIsVariablesPopoverOpen(true)} + iconType="indexOpen" + aria-label={i18n.translate( + 'xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton', + { + defaultMessage: 'Add variable', + } + )} + /> + } + isOpen={isVariablesPopoverOpen} + closePopover={() => setIsVariablesPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx index b4bbb8af36a19..dff697297f3e4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx @@ -16,10 +16,6 @@ import { EuiButtonEmpty, EuiSwitch, EuiFormRow, - EuiContextMenuItem, - EuiButtonIcon, - EuiContextMenuPanel, - EuiPopover, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { @@ -29,6 +25,7 @@ import { ActionParamsProps, } from '../../../types'; import { EmailActionParams, EmailActionConnector } from './types'; +import { AddMessageVariables } from '../add_message_variables'; export function getActionType(): ActionTypeModel { const mailformat = /^[^@\s]+@[^@\s]+$/; @@ -368,25 +365,21 @@ const EmailParamsFields: React.FunctionComponent(false); const [addBCC, setAddBCC] = useState(false); - const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); useEffect(() => { if (!message && defaultMessage && defaultMessage.length > 0) { editAction('message', defaultMessage, index); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const messageVariablesItems = messageVariables?.map((variable: string) => ( - { - editAction('message', (message ?? '').concat(` {{${variable}}}`), index); - setIsVariablesPopoverOpen(false); - }} - > - {`{{${variable}}}`} - - )); + + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction( + paramsProperty, + ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), + index + ); + }; + return ( + onSelectMessageVariable('subject', variable) + } + paramsProperty="subject" + /> + } > setIsVariablesPopoverOpen(true)} - iconType="indexOpen" - aria-label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.addVariablePopoverButton', - { - defaultMessage: 'Add variable', - } - )} - /> + + onSelectMessageVariable('message', variable) } - isOpen={isVariablesPopoverOpen} - closePopover={() => setIsVariablesPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + paramsProperty="message" + /> } > { ).toBe(`{ "test": 123 }`); - expect( - wrapper.find('[data-test-subj="indexDocumentAddVariableButton"]').length > 0 - ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx index 56d9f40e40021..9bd6a39d216e3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx @@ -14,10 +14,6 @@ import { EuiSelect, EuiTitle, EuiIconTip, - EuiPopover, - EuiButtonIcon, - EuiContextMenuPanel, - EuiContextMenuItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -36,6 +32,7 @@ import { getIndexPatterns, } from '../../../common/index_controls'; import { useXJsonMode } from '../../lib/use_x_json_mode'; +import { AddMessageVariables } from '../add_message_variables'; export function getActionType(): ActionTypeModel { return { @@ -282,23 +279,13 @@ const IndexParamsFields: React.FunctionComponent 0 ? documents[0] : null ); - const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); - const messageVariablesItems = messageVariables?.map((variable: string, i: number) => ( - { - const value = (xJson ?? '').concat(` {{${variable}}}`); - setXJson(value); - // Keep the documents in sync with the editor content - onDocumentsChange(convertToJson(value)); - setIsVariablesPopoverOpen(false); - }} - > - {`{{${variable}}}`} - - )); + const onSelectMessageVariable = (variable: string) => { + const value = (xJson ?? '').concat(` {{${variable}}}`); + setXJson(value); + // Keep the documents in sync with the editor content + onDocumentsChange(convertToJson(value)); + }; + function onDocumentsChange(updatedDocuments: string) { try { const documentsJSON = JSON.parse(updatedDocuments); @@ -317,34 +304,11 @@ const IndexParamsFields: React.FunctionComponent setIsVariablesPopoverOpen(true)} - iconType="indexOpen" - title={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.addVariableTitle', - { - defaultMessage: 'Add variable', - } - )} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.addVariablePopoverButton', - { - defaultMessage: 'Add variable', - } - )} - /> - } - isOpen={isVariablesPopoverOpen} - closePopover={() => setIsVariablesPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + onSelectMessageVariable(variable)} + paramsProperty="documents" + /> } > >({ - dedupKey: false, - summary: false, - source: false, - timestamp: false, - component: false, - group: false, - class: false, - }); - // TODO: replace this button with a proper Eui component, when it will be ready - const getMessageVariables = (paramsProperty: string) => - messageVariables?.map((variable: string) => ( - { - editAction( - paramsProperty, - ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), - index - ); - setIsVariablesPopoverOpen({ ...isVariablesPopoverOpen, [paramsProperty]: false }); - }} - > - {`{{${variable}}}`} - - )); - - const addVariableButtonTitle = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.addVariableTitle', - { - defaultMessage: 'Add alert variable', - } - ); - const getAddVariableComponent = (paramsProperty: string, buttonName: string) => { - return ( - - setIsVariablesPopoverOpen({ ...isVariablesPopoverOpen, [paramsProperty]: true }) - } - iconType="indexOpen" - aria-label={buttonName} - /> - } - isOpen={isVariablesPopoverOpen[paramsProperty]} - closePopover={() => - setIsVariablesPopoverOpen({ ...isVariablesPopoverOpen, [paramsProperty]: false }) - } - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction( + paramsProperty, + ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), + index ); }; + return ( @@ -359,15 +305,15 @@ const PagerDutyParamsFields: React.FunctionComponent + onSelectMessageVariable('dedupKey', variable) } - ) - )} + paramsProperty="dedupKey" + /> + } > + onSelectMessageVariable('timestamp', variable) } - ) - )} + paramsProperty="timestamp" + /> + } > + onSelectMessageVariable('component', variable) } - ) - )} + paramsProperty="component" + /> + } > onSelectMessageVariable('group', variable)} + paramsProperty="group" + /> + } > onSelectMessageVariable('source', variable)} + paramsProperty="source" + /> + } > + onSelectMessageVariable('summary', variable) } - ) - )} + paramsProperty="summary" + /> + } > onSelectMessageVariable('class', variable)} + paramsProperty="class" + /> + } > (false); useEffect(() => { editAction('level', 'info', index); @@ -80,18 +72,11 @@ export const ServerLogParamsFields: React.FunctionComponent ( - { - editAction('message', (message ?? '').concat(` {{${variable}}}`), index); - setIsVariablesPopoverOpen(false); - }} - > - {`{{${variable}}}`} - - )); + + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction(paramsProperty, (message ?? '').concat(` {{${variable}}}`), index); + }; + return ( setIsVariablesPopoverOpen(true)} - iconType="indexOpen" - title={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.addVariableTitle', - { - defaultMessage: 'Add variable', - } - )} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.addVariablePopoverButton', - { - defaultMessage: 'Add variable', - } - )} - /> + + onSelectMessageVariable('message', variable) } - isOpen={isVariablesPopoverOpen} - closePopover={() => setIsVariablesPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + paramsProperty="message" + /> } > { const { message } = actionParams; - const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); useEffect(() => { if (!message && defaultMessage && defaultMessage.length > 0) { editAction('message', defaultMessage, index); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const messageVariablesItems = messageVariables?.map((variable: string, i: number) => ( - { - editAction('message', (message ?? '').concat(` {{${variable}}}`), index); - setIsVariablesPopoverOpen(false); - }} - > - {`{{${variable}}}`} - - )); + + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction(paramsProperty, (message ?? '').concat(` {{${variable}}}`), index); + }; + return ( setIsVariablesPopoverOpen(true)} - iconType="indexOpen" - title={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.addVariableTitle', - { - defaultMessage: 'Add alert variable', - } - )} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.addVariablePopoverButton', - { - defaultMessage: 'Add alert variable', - } - )} - /> + + onSelectMessageVariable('message', variable) } - isOpen={isVariablesPopoverOpen} - closePopover={() => setIsVariablesPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + paramsProperty="message" + /> } > { .first() .prop('value') ).toStrictEqual('test message'); - expect(wrapper.find('[data-test-subj="webhookAddVariableButton"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="bodyAddVariableButton"]').length > 0).toBeTruthy(); }); test('params validation fails when body is not valid', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx index f611c3715e56a..daa5a6caeabe9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx @@ -22,9 +22,6 @@ import { EuiCodeEditor, EuiSwitch, EuiButtonEmpty, - EuiContextMenuItem, - EuiPopover, - EuiContextMenuPanel, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { @@ -34,6 +31,7 @@ import { ActionParamsProps, } from '../../../types'; import { WebhookActionParams, WebhookActionConnector } from './types'; +import { AddMessageVariables } from '../add_message_variables'; const HTTP_VERBS = ['post', 'put']; @@ -467,20 +465,9 @@ const WebhookParamsFields: React.FunctionComponent { const { body } = actionParams; - const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); - const messageVariablesItems = messageVariables?.map((variable: string, i: number) => ( - { - editAction('body', (body ?? '').concat(` {{${variable}}}`), index); - setIsVariablesPopoverOpen(false); - }} - > - {`{{${variable}}}`} - - )); + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction(paramsProperty, (body ?? '').concat(` {{${variable}}}`), index); + }; return ( setIsVariablesPopoverOpen(true)} - iconType="indexOpen" - title={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariableTitle', - { - defaultMessage: 'Add variable', - } - )} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addVariablePopoverButton', - { - defaultMessage: 'Add variable', - } - )} - /> - } - isOpen={isVariablesPopoverOpen} - closePopover={() => setIsVariablesPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + onSelectMessageVariable('body', variable)} + paramsProperty="body" + /> } > Date: Thu, 9 Apr 2020 08:22:00 -0700 Subject: [PATCH 56/81] docs: fix rendering of bulleted list (#62855) --- docs/user/alerting/index.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index b4f7e6af3d61c..c7cf1186a44be 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -163,7 +163,8 @@ If you are using an *on-premises* Elastic Stack deployment with <> * <> * <> From a0c247b9cc165638c3c01c87ce02c13ac2a4a698 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Thu, 9 Apr 2020 11:42:10 -0400 Subject: [PATCH 57/81] [EPM] Change PACKAGES_SAVED_OBJECT_TYPE id (#62818) * changed PACKAGES_SAVED_OBJECT_TYPE id from packageName-version to packageName * change references to keys to package and version Co-authored-by: Elastic Machine --- .../server/routes/epm/handlers.ts | 4 +- .../server/services/datasource.ts | 10 ++-- .../services/epm/elasticsearch/ilm/install.ts | 12 +++-- .../elasticsearch/ingest_pipeline/install.ts | 23 +++++---- .../epm/elasticsearch/template/install.ts | 17 +++++-- .../epm/kibana/index_pattern/install.ts | 27 +++++++---- .../server/services/epm/packages/assets.ts | 2 +- .../server/services/epm/packages/get.ts | 40 +++++----------- .../services/epm/packages/get_objects.ts | 40 ---------------- .../server/services/epm/packages/index.ts | 1 - .../server/services/epm/packages/install.ts | 47 ++++++++++++------- .../server/services/epm/packages/remove.ts | 6 ++- .../server/services/epm/registry/index.ts | 25 +++++----- .../ingest_manager/server/services/setup.ts | 3 +- 14 files changed, 118 insertions(+), 139 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index 48f37a4d65ac6..ad16e1dde456b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -102,7 +102,9 @@ export const getInfoHandler: RequestHandler { - const pkgInstall = await findInstalledPackageByName({ - savedObjectsClient: soClient, - pkgName, - }); + const pkgInstall = await getInstallation({ savedObjectsClient: soClient, pkgName }); if (pkgInstall) { const [pkgInfo, defaultOutputId] = await Promise.all([ getPackageInfo({ savedObjectsClient: soClient, - pkgkey: `${pkgInstall.name}-${pkgInstall.version}`, + pkgName: pkgInstall.name, + pkgVersion: pkgInstall.version, }), outputService.getDefaultOutputId(soClient), ]); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts index c56322239f27b..60a85e367079f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts @@ -7,9 +7,15 @@ import { CallESAsCurrentUser, ElasticsearchAssetType } from '../../../../types'; import * as Registry from '../../registry'; -export async function installILMPolicy(pkgkey: string, callCluster: CallESAsCurrentUser) { - const ilmPaths = await Registry.getArchiveInfo(pkgkey, (entry: Registry.ArchiveEntry) => - isILMPolicy(entry) +export async function installILMPolicy( + pkgName: string, + pkgVersion: string, + callCluster: CallESAsCurrentUser +) { + const ilmPaths = await Registry.getArchiveInfo( + pkgName, + pkgVersion, + (entry: Registry.ArchiveEntry) => isILMPolicy(entry) ); if (!ilmPaths.length) return; await Promise.all( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts index 4b65e5554567e..2bbb555ef7393 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -30,11 +30,10 @@ export const installPipelines = async ( if (dataset.ingest_pipeline) { acc.push( installPipelinesForDataset({ - pkgkey: Registry.pkgToPkgKey(registryPackage), dataset, callCluster, - packageName: registryPackage.name, - packageVersion: registryPackage.version, + pkgName: registryPackage.name, + pkgVersion: registryPackage.version, }) ); } @@ -68,19 +67,19 @@ export function rewriteIngestPipeline( export async function installPipelinesForDataset({ callCluster, - pkgkey, + pkgName, + pkgVersion, dataset, - packageName, - packageVersion, }: { callCluster: CallESAsCurrentUser; - pkgkey: string; + pkgName: string; + pkgVersion: string; dataset: Dataset; - packageName: string; - packageVersion: string; }): Promise { - const pipelinePaths = await Registry.getArchiveInfo(pkgkey, (entry: Registry.ArchiveEntry) => - isDatasetPipeline(entry, dataset.path) + const pipelinePaths = await Registry.getArchiveInfo( + pkgName, + pkgVersion, + (entry: Registry.ArchiveEntry) => isDatasetPipeline(entry, dataset.path) ); let pipelines: any[] = []; const substitutions: RewriteSubstitution[] = []; @@ -90,7 +89,7 @@ export async function installPipelinesForDataset({ const nameForInstallation = getPipelineNameForInstallation({ pipelineName: name, dataset, - packageVersion, + packageVersion: pkgVersion, }); const content = Registry.getAsset(path).toString('utf-8'); pipelines.push({ diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index de4ba25590c98..560ddfc1f6885 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -20,11 +20,12 @@ import * as Registry from '../../registry'; export const installTemplates = async ( registryPackage: RegistryPackage, callCluster: CallESAsCurrentUser, - pkgkey: string + pkgName: string, + pkgVersion: string ) => { // install any pre-built index template assets, // atm, this is only the base package's global template - installPreBuiltTemplates(pkgkey, callCluster); + installPreBuiltTemplates(pkgName, pkgVersion, callCluster); // build templates per dataset from yml files const datasets = registryPackage.datasets; @@ -45,9 +46,15 @@ export const installTemplates = async ( }; // this is temporary until we update the registry to use index templates v2 structure -const installPreBuiltTemplates = async (pkgkey: string, callCluster: CallESAsCurrentUser) => { - const templatePaths = await Registry.getArchiveInfo(pkgkey, (entry: Registry.ArchiveEntry) => - isTemplate(entry) +const installPreBuiltTemplates = async ( + pkgName: string, + pkgVersion: string, + callCluster: CallESAsCurrentUser +) => { + const templatePaths = await Registry.getArchiveInfo( + pkgName, + pkgVersion, + (entry: Registry.ArchiveEntry) => isTemplate(entry) ); templatePaths.forEach(async path => { const { file } = Registry.pathParts(path); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index 0657fb7759b49..05e64c6565dc6 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -68,25 +68,32 @@ export enum IndexPatternType { metrics = 'metrics', events = 'events', } - +// TODO: use a function overload and make pkgName and pkgVersion required for install/update +// and not for an update removal. or separate out the functions export async function installIndexPatterns( savedObjectsClient: SavedObjectsClientContract, - pkgkey?: string + pkgName?: string, + pkgVersion?: string ) { // get all user installed packages const installedPackages = await getPackageKeysByStatus( savedObjectsClient, InstallationStatus.installed ); - // add this package to the array if it doesn't already exist - // this should not happen because a user can't "reinstall" a package - // if it does because the install endpoint is called directly, the install continues - if (pkgkey && !installedPackages.includes(pkgkey)) { - installedPackages.push(pkgkey); + if (pkgName && pkgVersion) { + // add this package to the array if it doesn't already exist + const foundPkg = installedPackages.find(pkg => pkg.pkgName === pkgName); + // this may be removed if we add the packged to saved objects before installing index patterns + // otherwise this is a first time install + // TODO: handle update case when versions are different + if (!foundPkg) { + installedPackages.push({ pkgName, pkgVersion }); + } } - // get each package's registry info - const installedPackagesFetchInfoPromise = installedPackages.map(pkg => Registry.fetchInfo(pkg)); + const installedPackagesFetchInfoPromise = installedPackages.map(pkg => + Registry.fetchInfo(pkg.pkgName, pkg.pkgVersion) + ); const installedPackagesInfo = await Promise.all(installedPackagesFetchInfoPromise); // for each index pattern type, create an index pattern @@ -97,7 +104,7 @@ export async function installIndexPatterns( ]; indexPatternTypes.forEach(async indexPatternType => { // if this is an update because a package is being unisntalled (no pkgkey argument passed) and no other packages are installed, remove the index pattern - if (!pkgkey && installedPackages.length === 0) { + if (!pkgName && installedPackages.length === 0) { try { await savedObjectsClient.delete(INDEX_PATTERN_SAVED_OBJECT_TYPE, `${indexPatternType}-*`); } catch (err) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts index d7a5c5569986e..7026d9eae24c3 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts @@ -58,7 +58,7 @@ export async function getAssetsData( ): Promise { // TODO: Needs to be called to fill the cache but should not be required const pkgkey = packageInfo.name + '-' + packageInfo.version; - if (!cacheHas(pkgkey)) await Registry.getArchiveInfo(pkgkey); + if (!cacheHas(pkgkey)) await Registry.getArchiveInfo(packageInfo.name, packageInfo.version); // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); 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 e963ea138dfd5..0e2c2a3d26073 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 @@ -41,7 +41,7 @@ export async function getPackages( .map(item => createInstallableFrom( item, - savedObjectsVisible.find(({ attributes }) => attributes.name === item.name) + savedObjectsVisible.find(({ id }) => id === item.name) ) ) .sort(sortByName); @@ -53,9 +53,9 @@ export async function getPackageKeysByStatus( status: InstallationStatus ) { const allPackages = await getPackages({ savedObjectsClient }); - return allPackages.reduce((acc, pkg) => { + return allPackages.reduce>((acc, pkg) => { if (pkg.status === status) { - acc.push(`${pkg.name}-${pkg.version}`); + acc.push({ pkgName: pkg.name, pkgVersion: pkg.version }); } return acc; }, []); @@ -63,13 +63,14 @@ export async function getPackageKeysByStatus( export async function getPackageInfo(options: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; + pkgName: string; + pkgVersion: string; }): Promise { - const { savedObjectsClient, pkgkey } = options; + const { savedObjectsClient, pkgName, pkgVersion } = options; const [item, savedObject] = await Promise.all([ - Registry.fetchInfo(pkgkey), - getInstallationObject({ savedObjectsClient, pkgkey }), - Registry.getArchiveInfo(pkgkey), + Registry.fetchInfo(pkgName, pkgVersion), + getInstallationObject({ savedObjectsClient, pkgName }), + Registry.getArchiveInfo(pkgName, pkgVersion), ] as const); // adding `as const` due to regression in TS 3.7.2 // see https://github.com/microsoft/TypeScript/issues/34925#issuecomment-550021453 @@ -86,37 +87,22 @@ export async function getPackageInfo(options: { export async function getInstallationObject(options: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; + pkgName: string; }) { - const { savedObjectsClient, pkgkey } = options; + const { savedObjectsClient, pkgName } = options; return savedObjectsClient - .get(PACKAGES_SAVED_OBJECT_TYPE, pkgkey) + .get(PACKAGES_SAVED_OBJECT_TYPE, pkgName) .catch(e => undefined); } export async function getInstallation(options: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; + pkgName: string; }) { const savedObject = await getInstallationObject(options); return savedObject?.attributes; } -export async function findInstalledPackageByName(options: { - savedObjectsClient: SavedObjectsClientContract; - pkgName: string; -}): Promise { - const { savedObjectsClient, pkgName } = options; - - const res = await savedObjectsClient.find({ - type: PACKAGES_SAVED_OBJECT_TYPE, - search: pkgName, - searchFields: ['name'], - }); - if (res.saved_objects.length) return res.saved_objects[0].attributes; - return undefined; -} - function sortByName(a: { name: string }, b: { name: string }) { if (a.name > b.name) { return 1; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts index b924c045870f3..b623295c5e060 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts @@ -11,46 +11,6 @@ import * as Registry from '../registry'; type ArchiveAsset = Pick; type SavedObjectToBe = Required & { type: AssetType }; -export async function getObjects( - pkgkey: string, - filter = (entry: Registry.ArchiveEntry): boolean => true -): Promise { - // Create a Map b/c some values, especially index-patterns, are referenced multiple times - const objects: Map = new Map(); - - // Get paths which match the given filter - const paths = await Registry.getArchiveInfo(pkgkey, filter); - - // Get all objects which matched filter. Add them to the Map - const rootObjects = await Promise.all(paths.map(getObject)); - rootObjects.forEach(obj => objects.set(obj.id, obj)); - - // Each of those objects might have `references` property like [{id, type, name}] - for (const object of rootObjects) { - // For each of those objects, if they have references - for (const reference of object.references) { - // Get the referenced objects. Call same function with a new filter - const referencedObjects = await getObjects(pkgkey, (entry: Registry.ArchiveEntry) => { - // Skip anything we've already stored - if (objects.has(reference.id)) return false; - - // Is the archive entry the reference we want? - const { type, file } = Registry.pathParts(entry.path); - const isType = type === reference.type; - const isJson = file === `${reference.id}.json`; - - return isType && isJson; - }); - - // Add referenced objects to the Map - referencedObjects.forEach(ro => objects.set(ro.id, ro)); - } - } - - // return the array of unique objects - return Array.from(objects.values()); -} - export async function getObject(key: string) { const buffer = Registry.getAsset(key); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index 79259ce79ff41..d49e0e661440f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -21,7 +21,6 @@ export { getPackageInfo, getPackages, SearchParams, - findInstalledPackageByName, } from './get'; export { installKibanaAssets, installPackage, ensureInstalledPackage } from './install'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 82523e37509d1..e250b4f176819 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -16,7 +16,7 @@ import { import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; import { getObject } from './get_objects'; -import { getInstallation, findInstalledPackageByName } from './index'; +import { getInstallation } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { installPipelines } from '../elasticsearch/ingest_pipeline/install'; import { installILMPolicy } from '../elasticsearch/ilm/install'; @@ -63,7 +63,7 @@ export async function ensureInstalledPackage(options: { callCluster: CallESAsCurrentUser; }): Promise { const { savedObjectsClient, pkgName, callCluster } = options; - const installedPackage = await findInstalledPackageByName({ savedObjectsClient, pkgName }); + const installedPackage = await getInstallation({ savedObjectsClient, pkgName }); if (installedPackage) { return installedPackage; } @@ -74,7 +74,7 @@ export async function ensureInstalledPackage(options: { pkgName, callCluster, }); - return await findInstalledPackageByName({ savedObjectsClient, pkgName }); + return await getInstallation({ savedObjectsClient, pkgName }); } catch (err) { throw new Error(err.message); } @@ -86,22 +86,30 @@ export async function installPackage(options: { callCluster: CallESAsCurrentUser; }): Promise { const { savedObjectsClient, pkgkey, callCluster } = options; - const registryPackageInfo = await Registry.fetchInfo(pkgkey); - const { name: pkgName, version: pkgVersion, internal = false } = registryPackageInfo; + // TODO: change epm API to /packageName/version so we don't need to do this + const [pkgName, pkgVersion] = pkgkey.split('-'); + const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); + const { internal = false } = registryPackageInfo; const installKibanaAssetsPromise = installKibanaAssets({ savedObjectsClient, - pkgkey, + pkgName, + pkgVersion, }); const installPipelinePromises = installPipelines(registryPackageInfo, callCluster); - const installTemplatePromises = installTemplates(registryPackageInfo, callCluster, pkgkey); + const installTemplatePromises = installTemplates( + registryPackageInfo, + callCluster, + pkgName, + pkgVersion + ); // index patterns and ilm policies are not currently associated with a particular package // so we do not save them in the package saved object state. at some point ILM policies can be installed/modified // per dataset and we should then save them - await installIndexPatterns(savedObjectsClient, pkgkey); + await installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); // currenly only the base package has an ILM policy - await installILMPolicy(pkgkey, callCluster); + await installILMPolicy(pkgName, pkgVersion, callCluster); const res = await Promise.all([ installKibanaAssetsPromise, @@ -126,14 +134,15 @@ export async function installPackage(options: { // e.g. switch statement with cases for each enum key returning `never` for default case export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; + pkgName: string; + pkgVersion: string; }) { - const { savedObjectsClient, pkgkey } = options; + const { savedObjectsClient, pkgName, pkgVersion } = options; // Only install Kibana assets during package installation. const kibanaAssetTypes = Object.values(KibanaAssetType); const installationPromises = kibanaAssetTypes.map(async assetType => - installKibanaSavedObjects({ savedObjectsClient, pkgkey, assetType }) + installKibanaSavedObjects({ savedObjectsClient, pkgName, pkgVersion, assetType }) ); // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] @@ -149,8 +158,8 @@ export async function saveInstallationReferences(options: { internal: boolean; toSave: AssetReference[]; }) { - const { savedObjectsClient, pkgkey, pkgName, pkgVersion, internal, toSave } = options; - const installation = await getInstallation({ savedObjectsClient, pkgkey }); + const { savedObjectsClient, pkgName, pkgVersion, internal, toSave } = options; + const installation = await getInstallation({ savedObjectsClient, pkgName }); const savedRefs = installation?.installed || []; const mergeRefsReducer = (current: AssetReference[], pending: AssetReference) => { const hasRef = current.find(c => c.id === pending.id && c.type === pending.type); @@ -162,7 +171,7 @@ export async function saveInstallationReferences(options: { await savedObjectsClient.create( PACKAGES_SAVED_OBJECT_TYPE, { installed: toInstall, name: pkgName, version: pkgVersion, internal }, - { id: pkgkey, overwrite: true } + { id: pkgName, overwrite: true } ); return toInstall; @@ -170,16 +179,18 @@ export async function saveInstallationReferences(options: { async function installKibanaSavedObjects({ savedObjectsClient, - pkgkey, + pkgName, + pkgVersion, assetType, }: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; + pkgName: string; + pkgVersion: string; assetType: KibanaAssetType; }) { const isSameType = ({ path }: Registry.ArchiveEntry) => assetType === Registry.pathParts(path).type; - const paths = await Registry.getArchiveInfo(pkgkey, isSameType); + const paths = await Registry.getArchiveInfo(pkgName, pkgVersion, isSameType); const toBeSavedObjects = await Promise.all(paths.map(getObject)); if (toBeSavedObjects.length === 0) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 2e73160453c2b..a30acb97b99cf 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -17,12 +17,14 @@ export async function removeInstallation(options: { callCluster: CallESAsCurrentUser; }): Promise { const { savedObjectsClient, pkgkey, callCluster } = options; - const installation = await getInstallation({ savedObjectsClient, pkgkey }); + // TODO: the epm api should change to /name/version so we don't need to do this + const [pkgName] = pkgkey.split('-'); + const installation = await getInstallation({ savedObjectsClient, pkgName }); const installedObjects = installation?.installed || []; // Delete the manager saved object with references to the asset objects // could also update with [] or some other state - await savedObjectsClient.delete(PACKAGES_SAVED_OBJECT_TYPE, pkgkey); + await savedObjectsClient.delete(PACKAGES_SAVED_OBJECT_TYPE, pkgName); // recreate or delete index patterns when a package is uninstalled await installIndexPatterns(savedObjectsClient); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 36a04b88bba29..a96afc5eb7fa5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -56,10 +56,9 @@ export async function fetchFindLatestPackage( } } -export async function fetchInfo(pkgkey: string): Promise { +export async function fetchInfo(pkgName: string, pkgVersion: string): Promise { const registryUrl = appContextService.getConfig()?.epm.registryUrl; - // change pkg-version to pkg/version - return fetchUrl(`${registryUrl}/package/${pkgkey.replace('-', '/')}`).then(JSON.parse); + return fetchUrl(`${registryUrl}/package/${pkgName}/${pkgVersion}`).then(JSON.parse); } export async function fetchFile(filePath: string): Promise { @@ -73,7 +72,8 @@ export async function fetchCategories(): Promise { } export async function getArchiveInfo( - pkgkey: string, + pkgName: string, + pkgVersion: string, filter = (entry: ArchiveEntry): boolean => true ): Promise { const paths: string[] = []; @@ -87,7 +87,7 @@ export async function getArchiveInfo( } }; - await extract(pkgkey, filter, onEntry); + await extract(pkgName, pkgVersion, filter, onEntry); return paths; } @@ -123,21 +123,22 @@ export function pathParts(path: string): AssetParts { } async function extract( - pkgkey: string, + pkgName: string, + pkgVersion: string, filter = (entry: ArchiveEntry): boolean => true, onEntry: (entry: ArchiveEntry) => void ) { - const archiveBuffer = await getOrFetchArchiveBuffer(pkgkey); + const archiveBuffer = await getOrFetchArchiveBuffer(pkgName, pkgVersion); return untarBuffer(archiveBuffer, filter, onEntry); } -async function getOrFetchArchiveBuffer(pkgkey: string): Promise { +async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { // assume .tar.gz for now. add support for .zip if/when we need it - const key = `${pkgkey}.tar.gz`; + const key = `${pkgName}-${pkgVersion}.tar.gz`; let buffer = cacheGet(key); if (!buffer) { - buffer = await fetchArchiveBuffer(pkgkey); + buffer = await fetchArchiveBuffer(pkgName, pkgVersion); cacheSet(key, buffer); } @@ -148,8 +149,8 @@ async function getOrFetchArchiveBuffer(pkgkey: string): Promise { } } -async function fetchArchiveBuffer(key: string): Promise { - const { download: archivePath } = await fetchInfo(key); +async function fetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { + const { download: archivePath } = await fetchInfo(pkgName, pkgVersion); const registryUrl = appContextService.getConfig()?.epm.registryUrl; return getResponseStream(`${registryUrl}${archivePath}`).then(streamToBuffer); } diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 224355ced7cb1..bbaf083fb8396 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -120,7 +120,8 @@ async function addPackageToConfig( ) { const packageInfo = await getPackageInfo({ savedObjectsClient: soClient, - pkgkey: `${packageToInstall.name}-${packageToInstall.version}`, + pkgName: packageToInstall.name, + pkgVersion: packageToInstall.version, }); await datasourceService.create( soClient, From fddf6cbee5ca1e486f6bc2724d55b2143e93fe74 Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Thu, 9 Apr 2020 08:52:06 -0700 Subject: [PATCH 58/81] [APM] docs: add alerting examples for APM (#62864) --- docs/apm/apm-alerts.asciidoc | 97 +++++++++++++++++++++++++++++ docs/apm/images/apm-alert.png | Bin 0 -> 658181 bytes docs/apm/using-the-apm-ui.asciidoc | 3 + 3 files changed, 100 insertions(+) create mode 100644 docs/apm/apm-alerts.asciidoc create mode 100644 docs/apm/images/apm-alert.png diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc new file mode 100644 index 0000000000000..b8552c007b13d --- /dev/null +++ b/docs/apm/apm-alerts.asciidoc @@ -0,0 +1,97 @@ +[role="xpack"] +[[apm-alerts]] +=== Create an alert + +beta::[] + +The APM app is integrated with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] feature. +It provides a set of built-in **actions** and APM specific threshold **alerts** for you to use, +and allows all alerts to be centrally managed from <>. + +[role="screenshot"] +image::apm/images/apm-alert.png[Create an alert in the APM app] + +There are two different types of threshold alerts: transaction duration, and error rate. +Below, we'll create one of each. + +[float] +[[apm-create-transaction-alert]] +=== Create a transaction duration alert + +This guide creates an alert for the `opbeans-java` service based on the following criteria: + +* Transaction type: `transaction.type:request` +* Average request is above `1500ms` for the last 5 minutes +* Check every 10 minutes, and repeat the alert every 30 minutes +* Send the alert via Slack + +From the APM app, navigate to the `opbeans-java` service and select +**Alerts** > **Create threshold alert** > **Transaction duration**. + +The name of your alert will automatically be set as `Transaction duration | opbeans-java`, +and the alert will be tagged with `apm` and `service.name:opbeans-java`. +Feel free to edit either of these defaults. + +Based on the alert criteria, define the following alert details: + +* **Check every** - `10 minutes` +* **Notify every** - `30 minutes` +* **TYPE** - `request` +* **WHEN** - `avg` +* **IS ABOVE** - `1500ms` +* **FOR THE LAST** - `5 minutes` + +Select an action type. +Multiple action types can be selected, but in this example we want to post to a slack channel. +Select **Slack** > **Create a connector**. +Enter a name for the connector, +and paste the webhook URL. +See Slack's webhook documentation if you need to create one. + +Select **Save**. The alert has been created and is now active! + +[float] +[[apm-create-error-alert]] +=== Create an error rate alert + +This guide creates an alert for the `opbeans-python` service based on the following criteria: + +* Error rate is above 25 for the last minute +* Check every 1 minute, and repeat the alert every 10 minutes +* Send the alert via email to the `opbeans-python` team + +From the APM app, navigate to the `opbeans-python` service and select +**Alerts** > **Create threshold alert** > **Error rate**. + +The name of your alert will automatically be set as `Error rate | opbeans-python`, +and the alert will be tagged with `apm` and `service.name:opbeans-python`. +Feel free to edit either of these defaults. + +Based on the alert criteria, define the following alert details: + +* **Check every** - `1 minute` +* **Notify every** - `10 minutes` +* **IS ABOVE** - `25 errors` +* **FOR THE LAST** - `1 minute` + +Select the **Email** action type and click **Create a connector**. +Fill out the required details: sender, host, port, etc., and click **save**. + +Select **Save**. The alert has been created and is now active! + +[float] +[[apm-alert-manage]] +=== Manage alerts and actions + +From the APM app, select **Alerts** > **View active alerts** to be taken to the Kibana alerts and actions management page. +From this page, you can create, edit, disable, mute, and delete alerts, and create, edit, and disable connectors. + +[float] +[[apm-alert-more-info]] +=== More information + +See {kibana-ref}/alerting-getting-started.html[alerting and actions] for more information. + +NOTE: If you are using an **on-premise** Elastic Stack deployment with security, +TLS must be configured for communication between Elasticsearch and Kibana. +More information is in the alerting {kibana-ref}/alerting-getting-started.html#alerting-setup-prerequisites[prerequisites]. \ No newline at end of file diff --git a/docs/apm/images/apm-alert.png b/docs/apm/images/apm-alert.png new file mode 100644 index 0000000000000000000000000000000000000000..4cee7214637f8e235db2f9ee1201c603cfdb8aa7 GIT binary patch literal 658181 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


#q|^Hy8wAq!HGKrX@3GsH68Pm$O( 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 z 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 Sfju@hR&lzL+{V0hctHXv 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

+ + + +

); }); @@ -170,3 +186,15 @@ export const HostDetailsFlyout = () => { ); }; + +const useHostLogsUrl = (hostId: string): { url: string; appId: string; appPath: string } => { + const { services } = useKibana(); + return useMemo(() => { + const appPath = `/stream?logFilter=(expression:'host.id:${hostId}',kind:kuery)`; + return { + url: `${services.application.getUrlForApp('logs')}${appPath}`, + appId: 'logs', + appPath, + }; + }, [hostId, services.application]); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx index f6dfae99c1b11..c3ff41268e3db 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx @@ -6,40 +6,26 @@ import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { I18nProvider } from '@kbn/i18n/react'; -import { EuiThemeProvider } from '../../../../../../../legacy/common/eui_styled_components'; -import { appStoreFactory } from '../../store'; -import { RouteCapture } from '../route_capture'; -import { createMemoryHistory, MemoryHistory } from 'history'; -import { Router } from 'react-router-dom'; +import { fireEvent } from '@testing-library/react'; import { AppAction } from '../../types'; import { HostList } from './index'; -import { mockHostResultList } from '../../store/hosts/mock_host_result_list'; +import { + mockHostDetailsApiResult, + mockHostResultList, +} from '../../store/hosts/mock_host_result_list'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../mocks'; +import { HostInfo } from '../../../../../common/types'; describe('when on the hosts page', () => { - let render: () => reactTestingLibrary.RenderResult; - let history: MemoryHistory; - let store: ReturnType; + let render: () => ReturnType; + let history: AppContextTestRender['history']; + let store: AppContextTestRender['store']; + let coreStart: AppContextTestRender['coreStart']; beforeEach(async () => { - history = createMemoryHistory(); - store = appStoreFactory(); - render = () => { - return reactTestingLibrary.render( - - - - - - - - - - - - ); - }; + const mockedContext = createAppRootMockRenderer(); + ({ history, store, coreStart } = mockedContext); + render = () => mockedContext.render(); }); it('should show a table', async () => { @@ -56,7 +42,7 @@ describe('when on the hosts page', () => { expect(e).not.toBeNull(); }); }); - describe('when data loads', () => { + describe('when list data loads', () => { beforeEach(() => { reactTestingLibrary.act(() => { const action: AppAction = { @@ -76,6 +62,16 @@ describe('when on the hosts page', () => { describe('when the user clicks the hostname in the table', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { + const hostDetailsApiResponse = mockHostDetailsApiResult(); + + coreStart.http.get.mockReturnValue(Promise.resolve(hostDetailsApiResponse)); + reactTestingLibrary.act(() => { + store.dispatch({ + type: 'serverReturnedHostDetails', + payload: hostDetailsApiResponse, + }); + }); + renderResult = render(); const detailsLink = await renderResult.findByTestId('hostnameCellLink'); if (detailsLink) { @@ -93,19 +89,71 @@ describe('when on the hosts page', () => { }); describe('when there is a selected host in the url', () => { + let hostDetails: HostInfo; beforeEach(() => { + const { + host_status, + metadata: { host, ...details }, + } = mockHostDetailsApiResult(); + hostDetails = { + host_status, + metadata: { + ...details, + host: { + ...host, + id: '1', + }, + }, + }; + + coreStart.http.get.mockReturnValue(Promise.resolve(hostDetails)); + coreStart.application.getUrlForApp.mockReturnValue('/app/logs'); + reactTestingLibrary.act(() => { history.push({ ...history.location, search: '?selected_host=1', }); }); + reactTestingLibrary.act(() => { + store.dispatch({ + type: 'serverReturnedHostDetails', + payload: hostDetails, + }); + }); }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should show the flyout', () => { const renderResult = render(); return renderResult.findByTestId('hostDetailsFlyout').then(flyout => { expect(flyout).not.toBeNull(); }); }); + it('should include the link to logs', async () => { + const renderResult = render(); + const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); + expect(linkToLogs).not.toBeNull(); + expect(linkToLogs.textContent).toEqual('Endpoint Logs'); + expect(linkToLogs.getAttribute('href')).toEqual( + "/app/logs/stream?logFilter=(expression:'host.id:1',kind:kuery)" + ); + }); + describe('when link to logs is clicked', () => { + beforeEach(async () => { + const renderResult = render(); + const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); + reactTestingLibrary.act(() => { + fireEvent.click(linkToLogs); + }); + }); + + it('should navigate to logs without full page refresh', async () => { + // FIXME: this is not working :( + expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1); + }); + }); }); }); From 0dd89e388d3a0d87e7ea07be74b706db234c1c26 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 9 Apr 2020 09:56:11 -0600 Subject: [PATCH 61/81] [Maps] create NOT EXISTS filter for tooltip property with no value (#62849) * [Maps] create NOT EXISTS filter for tooltip property with no value * review feedback --- .../tooltips/es_tooltip_property.test.ts | 105 ++++++++++++++++++ .../layers/tooltips/es_tooltip_property.ts | 19 +++- .../layers/tooltips/join_tooltip_property.ts | 4 +- .../layers/tooltips/tooltip_property.ts | 6 +- 4 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.test.ts diff --git a/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.test.ts b/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.test.ts new file mode 100644 index 0000000000000..2cc9e1513719b --- /dev/null +++ b/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { IFieldType, IndexPattern } from '../../../../../../src/plugins/data/public'; +import { ESTooltipProperty } from './es_tooltip_property'; +import { TooltipProperty } from './tooltip_property'; +import { AbstractField } from '../fields/field'; +import { FIELD_ORIGIN } from '../../../common/constants'; + +class MockField extends AbstractField {} + +const indexPatternField = { + name: 'machine.os', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, +} as IFieldType; + +const featurePropertyField = new MockField({ + fieldName: 'machine.os', + origin: FIELD_ORIGIN.SOURCE, +}); + +const indexPattern = { + id: 'indexPatternId', + fields: { + getByName: (name: string): IFieldType | null => { + return name === 'machine.os' ? indexPatternField : null; + }, + }, + title: 'my index pattern', +} as IndexPattern; + +describe('getESFilters', () => { + test('Should return empty array when field does not exist in index pattern', async () => { + const notFoundFeaturePropertyField = new MockField({ + fieldName: 'field name that is not in index pattern', + origin: FIELD_ORIGIN.SOURCE, + }); + const esTooltipProperty = new ESTooltipProperty( + new TooltipProperty( + notFoundFeaturePropertyField.getName(), + await notFoundFeaturePropertyField.getLabel(), + 'my value' + ), + indexPattern, + notFoundFeaturePropertyField + ); + expect(await esTooltipProperty.getESFilters()).toEqual([]); + }); + + test('Should return phrase filter when field value is provided', async () => { + const esTooltipProperty = new ESTooltipProperty( + new TooltipProperty( + featurePropertyField.getName(), + await featurePropertyField.getLabel(), + 'my value' + ), + indexPattern, + featurePropertyField + ); + expect(await esTooltipProperty.getESFilters()).toEqual([ + { + meta: { + index: 'indexPatternId', + }, + query: { + match_phrase: { + ['machine.os']: 'my value', + }, + }, + }, + ]); + }); + + test('Should return NOT exists filter for null values', async () => { + const esTooltipProperty = new ESTooltipProperty( + new TooltipProperty( + featurePropertyField.getName(), + await featurePropertyField.getLabel(), + undefined + ), + indexPattern, + featurePropertyField + ); + expect(await esTooltipProperty.getESFilters()).toEqual([ + { + meta: { + index: 'indexPatternId', + negate: true, + }, + exists: { + field: 'machine.os', + }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.ts b/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.ts index 5c35009881920..d2fdcfaab476c 100644 --- a/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.ts +++ b/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.ts @@ -7,8 +7,12 @@ import _ from 'lodash'; import { ITooltipProperty } from './tooltip_property'; import { IField } from '../fields/field'; -import { esFilters, IFieldType, IndexPattern } from '../../../../../../src/plugins/data/public'; -import { PhraseFilter } from '../../../../../../src/plugins/data/public'; +import { + esFilters, + Filter, + IFieldType, + IndexPattern, +} from '../../../../../../src/plugins/data/public'; export class ESTooltipProperty implements ITooltipProperty { private readonly _tooltipProperty: ITooltipProperty; @@ -64,12 +68,19 @@ export class ESTooltipProperty implements ITooltipProperty { ); } - async getESFilters(): Promise { + async getESFilters(): Promise { const indexPatternField = this._getIndexPatternField(); if (!indexPatternField) { return []; } - return [esFilters.buildPhraseFilter(indexPatternField, this.getRawValue(), this._indexPattern)]; + const value = this.getRawValue(); + if (value == null) { + const existsFilter = esFilters.buildExistsFilter(indexPatternField, this._indexPattern); + existsFilter.meta.negate = true; + return [existsFilter]; + } else { + return [esFilters.buildPhraseFilter(indexPatternField, value, this._indexPattern)]; + } } } diff --git a/x-pack/plugins/maps/public/layers/tooltips/join_tooltip_property.ts b/x-pack/plugins/maps/public/layers/tooltips/join_tooltip_property.ts index 4af236f6e9e36..cc95c12ef630f 100644 --- a/x-pack/plugins/maps/public/layers/tooltips/join_tooltip_property.ts +++ b/x-pack/plugins/maps/public/layers/tooltips/join_tooltip_property.ts @@ -6,7 +6,7 @@ import { ITooltipProperty } from './tooltip_property'; import { IJoin } from '../joins/join'; -import { PhraseFilter } from '../../../../../../src/plugins/data/public'; +import { Filter } from '../../../../../../src/plugins/data/public'; export class JoinTooltipProperty implements ITooltipProperty { private readonly _tooltipProperty: ITooltipProperty; @@ -37,7 +37,7 @@ export class JoinTooltipProperty implements ITooltipProperty { return this._tooltipProperty.getHtmlDisplayValue(); } - async getESFilters(): Promise { + async getESFilters(): Promise { const esFilters = []; if (this._tooltipProperty.isFilterable()) { esFilters.push(...(await this._tooltipProperty.getESFilters())); diff --git a/x-pack/plugins/maps/public/layers/tooltips/tooltip_property.ts b/x-pack/plugins/maps/public/layers/tooltips/tooltip_property.ts index 7d680dfe9cae0..8da2ed795943b 100644 --- a/x-pack/plugins/maps/public/layers/tooltips/tooltip_property.ts +++ b/x-pack/plugins/maps/public/layers/tooltips/tooltip_property.ts @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { PhraseFilter } from '../../../../../../src/plugins/data/public'; +import { Filter } from '../../../../../../src/plugins/data/public'; import { TooltipFeature } from '../../../../../plugins/maps/common/descriptor_types'; export interface ITooltipProperty { @@ -14,7 +14,7 @@ export interface ITooltipProperty { getHtmlDisplayValue(): string; getRawValue(): string | undefined; isFilterable(): boolean; - getESFilters(): Promise; + getESFilters(): Promise; } export interface LoadFeatureProps { @@ -70,7 +70,7 @@ export class TooltipProperty implements ITooltipProperty { return false; } - async getESFilters(): Promise { + async getESFilters(): Promise { return []; } } From dfea62187f0e1984a50c9cff0c367f31b8728083 Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Thu, 9 Apr 2020 18:56:36 +0300 Subject: [PATCH 62/81] [NP] Inline buildPointSeriesData and buildHierarchicalData dependencies (#61575) * Move buildHierarchicalData to vislib * Move shortened version of buildPointSeriesData to Discover * Move buildPointSeriesData to vis_type_vislib * Convert unit tests to jest * Remove ui/agg_response * Convert point_series files to TS * Update TS in unit tests * Convert buildHierarchicalData to TS * Convert buildPointSeriesData to TS in Discover * Clean TS in Discover * Update TS for buildHierarchicalData * Update buildHierarchicalData unit tests * Clean up TS in point_series * Add unit tests fro response_handler.js * Simplify point_series for Discover * Return array for data * Add check for empty row * Simplify point_series for Discover * Return all points * Specify TS * Refactoring * Simplifying * improve types * Update _get_point.test.ts Co-authored-by: Elastic Machine Co-authored-by: Joe Reuter --- .../kibana/public/discover/kibana_services.ts | 2 - .../np_ready/angular/directives/histogram.tsx | 14 +- .../np_ready/angular/helpers/index.ts} | 0 .../np_ready/angular/helpers/point_series.ts | 111 +++++++ .../np_ready/angular/response_handler.js | 3 +- .../core_plugins/kibana/public/kibana.js | 1 - .../vis_type_vislib/public/legacy_imports.ts | 5 - .../vislib/__tests__/response_handlers.js | 137 --------- .../build_hierarchical_data.test.ts} | 108 ++++--- .../hierarchical/build_hierarchical_data.ts} | 50 +++- .../public/vislib/helpers/index.ts} | 13 +- .../helpers/point_series/_add_to_siri.test.ts | 84 ++++++ .../helpers/point_series/_add_to_siri.ts | 60 ++++ .../point_series/_fake_x_aspect.test.ts} | 15 +- .../helpers/point_series/_fake_x_aspect.ts} | 5 +- .../point_series/_get_aspects.test.ts} | 53 ++-- .../helpers/point_series/_get_aspects.ts} | 20 +- .../helpers/point_series/_get_point.test.ts | 104 +++++++ .../helpers/point_series/_get_point.ts} | 53 +++- .../helpers/point_series/_get_series.test.ts | 281 +++++++++++++++++ .../helpers/point_series/_get_series.ts | 88 ++++++ .../point_series/_init_x_axis.test.ts} | 91 +++--- .../helpers/point_series/_init_x_axis.ts} | 20 +- .../point_series/_init_y_axis.test.ts} | 19 +- .../helpers/point_series/_init_y_axis.ts} | 13 +- .../point_series/_ordered_date_axis.test.ts} | 20 +- .../point_series/_ordered_date_axis.ts} | 10 +- .../vislib/helpers/point_series/index.ts} | 10 +- .../point_series/point_series.test.ts} | 81 ++--- .../helpers/point_series/point_series.ts | 118 ++++++++ .../public/vislib/response_handler.js | 4 +- .../public/vislib/response_handler.test.ts | 130 ++++++++ .../vis_type_vislib/public/vislib/types.ts} | 36 ++- .../point_series/__tests__/_add_to_siri.js | 82 ----- .../point_series/__tests__/_get_point.js | 97 ------ .../point_series/__tests__/_get_series.js | 283 ------------------ .../agg_response/point_series/_get_series.js | 100 ------- .../agg_response/point_series/point_series.js | 42 --- .../dashboard_mode/public/dashboard_viewer.js | 1 - .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- 41 files changed, 1328 insertions(+), 1040 deletions(-) rename src/legacy/{ui/public/agg_response/point_series/index.js => core_plugins/kibana/public/discover/np_ready/angular/helpers/index.ts} (100%) create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/angular/helpers/point_series.ts delete mode 100644 src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/response_handlers.js rename src/legacy/{ui/public/agg_response/hierarchical/build_hierarchical_data.test.js => core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts} (80%) rename src/legacy/{ui/public/agg_response/hierarchical/build_hierarchical_data.js => core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts} (63%) rename src/legacy/{ui/public/agg_response/point_series/__tests__/point_series.js => core_plugins/vis_type_vislib/public/vislib/helpers/index.ts} (71%) create mode 100644 src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts create mode 100644 src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts rename src/legacy/{ui/public/agg_response/point_series/__tests__/_fake_x_aspect.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts} (74%) rename src/legacy/{ui/public/agg_response/point_series/_fake_x_aspect.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts} (88%) rename src/legacy/{ui/public/agg_response/point_series/__tests__/_get_aspects.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts} (53%) rename src/legacy/{ui/public/agg_response/point_series/_get_aspects.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts} (72%) create mode 100644 src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts rename src/legacy/{ui/public/agg_response/point_series/_get_point.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts} (69%) create mode 100644 src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts create mode 100644 src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts rename src/legacy/{ui/public/agg_response/point_series/__tests__/_init_x_axis.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts} (51%) rename src/legacy/{ui/public/agg_response/point_series/_init_x_axis.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts} (73%) rename src/legacy/{ui/public/agg_response/point_series/__tests__/_init_y_axis.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts} (81%) rename src/legacy/{ui/public/agg_response/point_series/_init_y_axis.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts} (82%) rename src/legacy/{ui/public/agg_response/point_series/__tests__/_ordered_date_axis.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts} (76%) rename src/legacy/{ui/public/agg_response/point_series/_ordered_date_axis.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts} (72%) rename src/legacy/{ui/public/agg_response/index.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts} (69%) rename src/legacy/{ui/public/agg_response/point_series/__tests__/_main.js => core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts} (62%) create mode 100644 src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts create mode 100644 src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.test.ts rename src/legacy/{ui/public/agg_response/point_series/_add_to_siri.js => core_plugins/vis_type_vislib/public/vislib/types.ts} (67%) delete mode 100644 src/legacy/ui/public/agg_response/point_series/__tests__/_add_to_siri.js delete mode 100644 src/legacy/ui/public/agg_response/point_series/__tests__/_get_point.js delete mode 100644 src/legacy/ui/public/agg_response/point_series/__tests__/_get_series.js delete mode 100644 src/legacy/ui/public/agg_response/point_series/_get_series.js delete mode 100644 src/legacy/ui/public/agg_response/point_series/point_series.js diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 98679a8f24d16..0a81ca0222b0a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -76,5 +76,3 @@ export { EsQuerySortValue, SortDirection, } from '../../../../../plugins/data/public'; -// @ts-ignore -export { buildPointSeriesData } from 'ui/agg_response/point_series/point_series'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx index f788347ac016c..8c55622e4c604 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx @@ -46,9 +46,10 @@ import { IUiSettingsClient } from 'kibana/public'; import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme'; import { Subscription } from 'rxjs'; import { getServices } from '../../../kibana_services'; +import { Chart as IChart } from '../helpers/point_series'; export interface DiscoverHistogramProps { - chartData: any; + chartData: IChart; timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; } @@ -163,7 +164,7 @@ export class DiscoverHistogram extends Component { - const xAxisFormat = this.props.chartData.xAxisFormat.params.pattern; + const xAxisFormat = this.props.chartData.xAxisFormat.params!.pattern; return moment(val).format(xAxisFormat); }; @@ -208,18 +209,19 @@ export class DiscoverHistogram extends Component domainStart ? domainStart : data[0].x; + const domainMin = data[0]?.x > domainStart ? domainStart : data[0]?.x; const domainMax = domainEnd - xInterval > lastXValue ? domainEnd - xInterval : lastXValue; const xDomain = { diff --git a/src/legacy/ui/public/agg_response/point_series/index.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/helpers/index.ts similarity index 100% rename from src/legacy/ui/public/agg_response/point_series/index.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/helpers/index.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/helpers/point_series.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/helpers/point_series.ts new file mode 100644 index 0000000000000..02dd024b09812 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/helpers/point_series.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 { uniq } from 'lodash'; +import { Duration, Moment } from 'moment'; +import { Unit } from '@elastic/datemath'; + +import { SerializedFieldFormat } from '../../../../../../../../plugins/expressions/common/types'; + +export interface Column { + id: string; + name: string; +} + +export interface Row { + [key: string]: number | 'NaN'; +} + +export interface Table { + columns: Column[]; + rows: Row[]; +} + +interface HistogramParams { + date: true; + interval: Duration; + intervalESValue: number; + intervalESUnit: Unit; + format: string; + bounds: { + min: Moment; + max: Moment; + }; +} +export interface Dimension { + accessor: 0 | 1; + format: SerializedFieldFormat<{ pattern: string }>; +} + +export interface Dimensions { + x: Dimension & { params: HistogramParams }; + y: Dimension; +} + +interface Ordered { + date: true; + interval: Duration; + intervalESUnit: string; + intervalESValue: number; + min: Moment; + max: Moment; +} +export interface Chart { + values: Array<{ + x: number; + y: number; + }>; + xAxisOrderedValues: number[]; + xAxisFormat: Dimension['format']; + xAxisLabel: Column['name']; + yAxisLabel?: Column['name']; + ordered: Ordered; +} + +export const buildPointSeriesData = (table: Table, dimensions: Dimensions) => { + const { x, y } = dimensions; + const xAccessor = table.columns[x.accessor].id; + const yAccessor = table.columns[y.accessor].id; + const chart = {} as Chart; + + chart.xAxisOrderedValues = uniq(table.rows.map(r => r[xAccessor] as number)); + chart.xAxisFormat = x.format; + chart.xAxisLabel = table.columns[x.accessor].name; + + const { intervalESUnit, intervalESValue, interval, bounds } = x.params; + chart.ordered = { + date: true, + interval, + intervalESUnit, + intervalESValue, + min: bounds.min, + max: bounds.max, + }; + + chart.yAxisLabel = table.columns[y.accessor].name; + + chart.values = table.rows + .filter(row => row && row[yAccessor] !== 'NaN') + .map(row => ({ + x: row[xAccessor] as number, + y: row[yAccessor] as number, + })); + + return chart; +}; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/response_handler.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/response_handler.js index 0c19c10841535..04ccb67ec7e25 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/response_handler.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/response_handler.js @@ -17,7 +17,8 @@ * under the License. */ -import { buildPointSeriesData, getServices } from '../../kibana_services'; +import { getServices } from '../../kibana_services'; +import { buildPointSeriesData } from './helpers'; function tableResponseHandler(table, dimensions) { const converted = { tables: [] }; diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index bceb3fa7eef8a..0a026a5e0c310 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -46,7 +46,6 @@ import './discover/legacy'; import './visualize/legacy'; import './management'; import './dev_tools'; -import 'ui/agg_response'; import { showAppRedirectNotification } from '../../../../plugins/kibana_legacy/public'; import 'leaflet'; import { localApplicationService } from './local_application_service'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts index da16a38deba9f..c04ffa506eb04 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts @@ -19,8 +19,3 @@ import { search } from '../../../../plugins/data/public'; export const { tabifyAggResponse, tabifyGetColumns } = search; - -// @ts-ignore -export { buildHierarchicalData } from 'ui/agg_response/hierarchical/build_hierarchical_data'; -// @ts-ignore -export { buildPointSeriesData } from 'ui/agg_response/point_series/point_series'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/response_handlers.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/response_handlers.js deleted file mode 100644 index 3574fb232883d..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/response_handlers.js +++ /dev/null @@ -1,137 +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'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { aggResponseIndex } from 'ui/agg_response'; - -import { vislibSeriesResponseHandler } from '../response_handler'; - -/** - * TODO: Fix these tests if still needed - * - * All these tests were not being run in master or prodiced false positive results - * Fixing them would require changes to the response handler logic. - */ - -describe.skip('Basic Response Handler', function() { - beforeEach(ngMock.module('kibana')); - - it('returns empty object if conversion failed', () => { - const data = vislibSeriesResponseHandler({}); - expect(data).to.not.be.an('undefined'); - expect(data).to.equal({}); - }); - - it('returns empty object if no data was found', () => { - const data = vislibSeriesResponseHandler({ - columns: [{ id: '1', title: '1', aggConfig: {} }], - rows: [], - }); - expect(data).to.not.be.an('undefined'); - expect(data.rows).to.equal([]); - }); -}); - -describe.skip('renderbot#buildChartData', function() { - describe('for hierarchical vis', function() { - it('defers to hierarchical aggResponse converter', function() { - const football = {}; - const stub = sinon.stub(aggResponseIndex, 'hierarchical').returns(football); - expect(vislibSeriesResponseHandler(football)).to.be(football); - expect(stub).to.have.property('callCount', 1); - expect(stub.firstCall.args[1]).to.be(football); - }); - }); - - describe('for point plot', function() { - it('calls tabify to simplify the data into a table', function() { - const football = { tables: [], hits: { total: 1 } }; - const stub = sinon.stub(aggResponseIndex, 'tabify').returns(football); - expect(vislibSeriesResponseHandler(football)).to.eql({ rows: [], hits: 1 }); - expect(stub).to.have.property('callCount', 1); - expect(stub.firstCall.args[1]).to.be(football); - }); - - it('returns a single chart if the tabify response contains only a single table', function() { - const chart = { hits: 1, rows: [], columns: [] }; - const esResp = { hits: { total: 1 } }; - const tabbed = { tables: [{}] }; - - sinon.stub(aggResponseIndex, 'tabify').returns(tabbed); - expect(vislibSeriesResponseHandler(esResp)).to.eql(chart); - }); - - it('converts table groups into rows/columns wrappers for charts', function() { - const converter = sinon.stub().returns('chart'); - const esResp = { hits: { total: 1 } }; - const tables = [{}, {}, {}, {}]; - - sinon.stub(aggResponseIndex, 'tabify').returns({ - tables: [ - { - aggConfig: { params: { row: true } }, - tables: [ - { - aggConfig: { params: { row: false } }, - tables: [tables[0]], - }, - { - aggConfig: { params: { row: false } }, - tables: [tables[1]], - }, - ], - }, - { - aggConfig: { params: { row: true } }, - tables: [ - { - aggConfig: { params: { row: false } }, - tables: [tables[2]], - }, - { - aggConfig: { params: { row: false } }, - tables: [tables[3]], - }, - ], - }, - ], - }); - - const chartData = vislibSeriesResponseHandler(esResp); - - // verify tables were converted - expect(converter).to.have.property('callCount', 4); - expect(converter.args[0][1]).to.be(tables[0]); - expect(converter.args[1][1]).to.be(tables[1]); - expect(converter.args[2][1]).to.be(tables[2]); - expect(converter.args[3][1]).to.be(tables[3]); - - expect(chartData).to.have.property('rows'); - expect(chartData.rows).to.have.length(2); - chartData.rows.forEach(function(row) { - expect(row).to.have.property('columns'); - expect(row.columns).to.eql(['chart', 'chart']); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts similarity index 80% rename from src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts index 21a937bf1fb66..475555f3a15f3 100644 --- a/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts @@ -17,24 +17,26 @@ * under the License. */ -import { buildHierarchicalData } from './build_hierarchical_data'; +import { buildHierarchicalData, Dimensions, Dimension } from './build_hierarchical_data'; +import { Table, TableParent } from '../../types'; -function tableVisResponseHandler(table, dimensions) { - const converted = { +function tableVisResponseHandler(table: Table, dimensions: Dimensions) { + const converted: { + tables: Array; + } = { tables: [], }; const split = dimensions.splitColumn || dimensions.splitRow; if (split) { - converted.direction = dimensions.splitRow ? 'row' : 'column'; const splitColumnIndex = split[0].accessor; const splitColumn = table.columns[splitColumnIndex]; - const splitMap = {}; + const splitMap: { [key: string]: number } = {}; let splitIndex = 0; table.rows.forEach((row, rowIndex) => { - const splitValue = row[splitColumn.id]; + const splitValue = row[splitColumn.id] as string; if (!splitMap.hasOwnProperty(splitValue)) { splitMap[splitValue] = splitIndex++; @@ -46,8 +48,8 @@ function tableVisResponseHandler(table, dimensions) { column: splitColumnIndex, row: rowIndex, table, - tables: [], - }; + tables: [] as Table[], + } as any; tableGroup.tables.push({ $parent: tableGroup, @@ -59,34 +61,30 @@ function tableVisResponseHandler(table, dimensions) { } const tableIndex = splitMap[splitValue]; - converted.tables[tableIndex].tables[0].rows.push(row); + (converted.tables[tableIndex] as TableParent).tables![0].rows.push(row); }); } else { converted.tables.push({ columns: table.columns, rows: table.rows, - }); + } as Table); } return converted; } -jest.mock('ui/new_platform'); -jest.mock('ui/chrome', () => ({ - getUiSettingsClient: jest.fn().mockReturnValue({ - get: jest.fn().mockReturnValue('KQL'), - }), -})); -jest.mock('ui/visualize/loader/pipeline_helpers/utilities', () => ({ - getFormat: jest.fn(() => ({ - convert: jest.fn(v => v), +jest.mock('../../../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: () => ({ + convert: jest.fn(v => JSON.stringify(v)), + }), })), })); describe('buildHierarchicalData convertTable', () => { describe('metric only', () => { - let dimensions; - let table; + let dimensions: Dimensions; + let table: Table; beforeEach(() => { const tabifyResponse = { @@ -94,11 +92,11 @@ describe('buildHierarchicalData convertTable', () => { rows: [{ 'col-0-agg_1': 412032 }], }; dimensions = { - metric: { accessor: 0 }, + metric: { accessor: 0 } as Dimension, }; const tableGroup = tableVisResponseHandler(tabifyResponse, dimensions); - table = tableGroup.tables[0]; + table = tableGroup.tables[0] as Table; }); it('should set the slices with one child to a consistent label', () => { @@ -118,8 +116,8 @@ describe('buildHierarchicalData convertTable', () => { }); describe('threeTermBuckets', () => { - let dimensions; - let tables; + let dimensions: Dimensions; + let tables: TableParent[]; beforeEach(async () => { const tabifyResponse = { @@ -231,60 +229,60 @@ describe('buildHierarchicalData convertTable', () => { ], }; dimensions = { - splitRow: [{ accessor: 0 }], - metric: { accessor: 5 }, - buckets: [{ accessor: 2 }, { accessor: 4 }], + splitRow: [{ accessor: 0 } as Dimension], + metric: { accessor: 5 } as Dimension, + buckets: [{ accessor: 2 }, { accessor: 4 }] as Dimension[], }; const tableGroup = await tableVisResponseHandler(tabifyResponse, dimensions); - tables = tableGroup.tables; + tables = tableGroup.tables as TableParent[]; }); it('should set the correct hits attribute for each of the results', () => { tables.forEach(t => { - const results = buildHierarchicalData(t.tables[0], dimensions); + const results = buildHierarchicalData(t.tables![0], dimensions); expect(results).toHaveProperty('hits'); expect(results.hits).toBe(4); }); }); it('should set the correct names for each of the results', () => { - const results0 = buildHierarchicalData(tables[0].tables[0], dimensions); + const results0 = buildHierarchicalData(tables[0].tables![0], dimensions); expect(results0).toHaveProperty('names'); expect(results0.names).toHaveLength(5); - const results1 = buildHierarchicalData(tables[1].tables[0], dimensions); + const results1 = buildHierarchicalData(tables[1].tables![0], dimensions); expect(results1).toHaveProperty('names'); expect(results1.names).toHaveLength(5); - const results2 = buildHierarchicalData(tables[2].tables[0], dimensions); + const results2 = buildHierarchicalData(tables[2].tables![0], dimensions); expect(results2).toHaveProperty('names'); expect(results2.names).toHaveLength(4); }); it('should set the parent of the first item in the split', () => { - const results0 = buildHierarchicalData(tables[0].tables[0], dimensions); + const results0 = buildHierarchicalData(tables[0].tables![0], dimensions); expect(results0).toHaveProperty('slices'); expect(results0.slices).toHaveProperty('children'); expect(results0.slices.children).toHaveLength(2); - expect(results0.slices.children[0].rawData.table.$parent).toHaveProperty('key', 'png'); + expect(results0.slices.children[0].rawData!.table.$parent).toHaveProperty('key', 'png'); - const results1 = buildHierarchicalData(tables[1].tables[0], dimensions); + const results1 = buildHierarchicalData(tables[1].tables![0], dimensions); expect(results1).toHaveProperty('slices'); expect(results1.slices).toHaveProperty('children'); expect(results1.slices.children).toHaveLength(2); - expect(results1.slices.children[0].rawData.table.$parent).toHaveProperty('key', 'css'); + expect(results1.slices.children[0].rawData!.table.$parent).toHaveProperty('key', 'css'); - const results2 = buildHierarchicalData(tables[2].tables[0], dimensions); + const results2 = buildHierarchicalData(tables[2].tables![0], dimensions); expect(results2).toHaveProperty('slices'); expect(results2.slices).toHaveProperty('children'); expect(results2.slices.children).toHaveLength(2); - expect(results2.slices.children[0].rawData.table.$parent).toHaveProperty('key', 'html'); + expect(results2.slices.children[0].rawData!.table.$parent).toHaveProperty('key', 'html'); }); }); describe('oneHistogramBucket', () => { - let dimensions; - let table; + let dimensions: Dimensions; + let table: Table; beforeEach(async () => { const tabifyResponse = { @@ -302,11 +300,11 @@ describe('buildHierarchicalData convertTable', () => { ], }; dimensions = { - metric: { accessor: 1 }, - buckets: [{ accessor: 0, params: { field: 'bytes', interval: 8192 } }], + metric: { accessor: 1 } as Dimension, + buckets: [{ accessor: 0 } as Dimension], }; const tableGroup = await tableVisResponseHandler(tabifyResponse, dimensions); - table = tableGroup.tables[0]; + table = tableGroup.tables[0] as Table; }); it('should set the hits attribute for the results', () => { @@ -320,8 +318,8 @@ describe('buildHierarchicalData convertTable', () => { }); describe('oneRangeBucket', () => { - let dimensions; - let table; + let dimensions: Dimensions; + let table: Table; beforeEach(async () => { const tabifyResponse = { @@ -335,11 +333,11 @@ describe('buildHierarchicalData convertTable', () => { ], }; dimensions = { - metric: { accessor: 1 }, - buckets: [{ accessor: 0, format: { id: 'range', params: { id: 'agg_2' } } }], + metric: { accessor: 1 } as Dimension, + buckets: [{ accessor: 0, format: { id: 'range', params: { id: 'agg_2' } } } as Dimension], }; const tableGroup = await tableVisResponseHandler(tabifyResponse, dimensions); - table = tableGroup.tables[0]; + table = tableGroup.tables[0] as Table; }); it('should set the hits attribute for the results', () => { @@ -348,13 +346,13 @@ describe('buildHierarchicalData convertTable', () => { expect(results).toHaveProperty('slices'); expect(results.slices).toHaveProperty('children'); expect(results).toHaveProperty('names'); - // expect(results.names).toHaveLength(2); + expect(results.names).toHaveLength(2); }); }); describe('oneFilterBucket', () => { - let dimensions; - let table; + let dimensions: Dimensions; + let table: Table; beforeEach(async () => { const tabifyResponse = { @@ -368,15 +366,15 @@ describe('buildHierarchicalData convertTable', () => { ], }; dimensions = { - metric: { accessor: 1 }, + metric: { accessor: 1 } as Dimension, buckets: [ { accessor: 0, }, - ], + ] as Dimension[], }; const tableGroup = await tableVisResponseHandler(tabifyResponse, dimensions); - table = tableGroup.tables[0]; + table = tableGroup.tables[0] as Table; }); it('should set the hits attribute for the results', () => { diff --git a/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts similarity index 63% rename from src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts index dcc27e956b3f8..2c6d62ed084b5 100644 --- a/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts @@ -18,11 +18,41 @@ */ import { toArray } from 'lodash'; -import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; +import { SerializedFieldFormat } from '../../../../../../../plugins/expressions/common/types'; +import { getFormatService } from '../../../services'; +import { Table } from '../../types'; -export const buildHierarchicalData = (table, { metric, buckets = [] }) => { - let slices; - const names = {}; +export interface Dimension { + accessor: number; + format: { + id?: string; + params?: SerializedFieldFormat; + }; +} + +export interface Dimensions { + metric: Dimension; + buckets?: Dimension[]; + splitRow?: Dimension[]; + splitColumn?: Dimension[]; +} + +interface Slice { + name: string; + size: number; + parent?: Slice; + children?: []; + rawData?: { + table: Table; + row: number; + column: number; + value: string | number | object; + }; +} + +export const buildHierarchicalData = (table: Table, { metric, buckets = [] }: Dimensions) => { + let slices: Slice[]; + const names: { [key: string]: string } = {}; const metricColumn = table.columns[metric.accessor]; const metricFieldFormatter = metric.format; @@ -30,25 +60,25 @@ export const buildHierarchicalData = (table, { metric, buckets = [] }) => { slices = [ { name: metricColumn.name, - size: table.rows[0][metricColumn.id], + size: table.rows[0][metricColumn.id] as number, }, ]; names[metricColumn.name] = metricColumn.name; } else { slices = []; table.rows.forEach((row, rowIndex) => { - let parent; + let parent: Slice; let dataLevel = slices; buckets.forEach(bucket => { const bucketColumn = table.columns[bucket.accessor]; const bucketValueColumn = table.columns[bucket.accessor + 1]; - const bucketFormatter = getFormat(bucket.format); + const bucketFormatter = getFormatService().deserialize(bucket.format); const name = bucketFormatter.convert(row[bucketColumn.id]); - const size = row[bucketValueColumn.id]; + const size = row[bucketValueColumn.id] as number; names[name] = name; - let slice = dataLevel.find(slice => slice.name === name); + let slice = dataLevel.find(dataLevelSlice => dataLevelSlice.name === name); if (!slice) { slice = { name, @@ -66,7 +96,7 @@ export const buildHierarchicalData = (table, { metric, buckets = [] }) => { } parent = slice; - dataLevel = slice.children; + dataLevel = slice.children as []; }); }); } diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/point_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/index.ts similarity index 71% rename from src/legacy/ui/public/agg_response/point_series/__tests__/point_series.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/index.ts index 9c3e1c8180eb5..90924e79f6027 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/point_series.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/index.ts @@ -17,14 +17,5 @@ * under the License. */ -describe('Point Series Agg Response', function() { - require('./_main'); - require('./_add_to_siri'); - require('./_fake_x_aspect'); - require('./_get_aspects'); - require('./_get_point'); - require('./_get_series'); - require('./_init_x_axis'); - require('./_init_y_axis'); - require('./_ordered_date_axis'); -}); +export { buildPointSeriesData } from './point_series'; +export { buildHierarchicalData } from './hierarchical/build_hierarchical_data'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts new file mode 100644 index 0000000000000..e4fdd6bb71c00 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { addToSiri, Serie } from './_add_to_siri'; +import { Point } from './_get_point'; +import { Dimension } from './point_series'; + +describe('addToSiri', function() { + it('creates a new series the first time it sees an id', function() { + const series = new Map(); + const point = {} as Point; + const id = 'id'; + addToSiri(series, point, id, id, { id }); + + const expectedSerie = series.get(id) as Serie; + expect(series.has(id)).toBe(true); + expect(expectedSerie).toEqual(expect.any(Object)); + expect(expectedSerie.label).toBe(id); + expect(expectedSerie.values).toHaveLength(1); + expect(expectedSerie.values[0]).toBe(point); + }); + + it('adds points to existing series if id has been seen', function() { + const series = new Map(); + const id = 'id'; + + const point = {} as Point; + addToSiri(series, point, id, id, { id }); + + const point2 = {} as Point; + addToSiri(series, point2, id, id, { id }); + + expect(series.has(id)).toBe(true); + expect(series.get(id)).toEqual(expect.any(Object)); + expect(series.get(id).label).toBe(id); + expect(series.get(id).values).toHaveLength(2); + expect(series.get(id).values[0]).toBe(point); + expect(series.get(id).values[1]).toBe(point2); + }); + + it('allows overriding the series label', function() { + const series = new Map(); + const id = 'id'; + const label = 'label'; + const point = {} as Point; + addToSiri(series, point, id, label, { id }); + + expect(series.has(id)).toBe(true); + expect(series.get(id)).toEqual(expect.any(Object)); + expect(series.get(id).label).toBe(label); + expect(series.get(id).values).toHaveLength(1); + expect(series.get(id).values[0]).toBe(point); + }); + + it('correctly sets id and rawId', function() { + const series = new Map(); + const id = 'id-id2'; + + const point = {} as Point; + addToSiri(series, point, id, undefined, {} as Dimension['format']); + + expect(series.has(id)).toBe(true); + expect(series.get(id)).toEqual(expect.any(Object)); + expect(series.get(id).label).toBe(id); + expect(series.get(id).rawId).toBe(id); + expect(series.get(id).id).toBe('id2'); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts new file mode 100644 index 0000000000000..5e5185d6c31ab --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts @@ -0,0 +1,60 @@ +/* + * 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 { Point } from './_get_point'; +import { Dimension } from './point_series'; + +export interface Serie { + id: string; + rawId: string; + label: string; + count: number; + values: Point[]; + format: Dimension['format']; + zLabel?: string; + zFormat?: Dimension['format']; +} + +export function addToSiri( + series: Map, + point: Point, + id: string, + yLabel: string | undefined | null, + yFormat: Dimension['format'], + zFormat?: Dimension['format'], + zLabel?: string +) { + id = id == null ? '' : id + ''; + + if (series.has(id)) { + (series.get(id) as Serie).values.push(point); + return; + } + + series.set(id, { + id: id.split('-').pop() as string, + rawId: id, + label: yLabel == null ? id : yLabel, + count: 0, + values: [point], + format: yFormat, + zLabel, + zFormat, + }); +} diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_fake_x_aspect.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts similarity index 74% rename from src/legacy/ui/public/agg_response/point_series/__tests__/_fake_x_aspect.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts index 6c246d7f50897..43d4c3d7ca7c4 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_fake_x_aspect.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts @@ -16,20 +16,17 @@ * specific language governing permissions and limitations * under the License. */ - -import expect from '@kbn/expect'; -import { makeFakeXAspect } from '../_fake_x_aspect'; +import { makeFakeXAspect } from './_fake_x_aspect'; describe('makeFakeXAspect', function() { it('creates an object that looks like an aspect', function() { const aspect = makeFakeXAspect(); - expect(aspect) - .to.have.property('accessor', -1) - .and.have.property('title', 'All docs') - .and.have.property('format') - .and.have.property('params'); + expect(aspect).toHaveProperty('accessor', -1); + expect(aspect).toHaveProperty('title', 'All docs'); + expect(aspect).toHaveProperty('format'); + expect(aspect).toHaveProperty('params'); - expect(aspect.params).to.have.property('defaultValue', '_all'); + expect(aspect.params).toHaveProperty('defaultValue', '_all'); }); }); diff --git a/src/legacy/ui/public/agg_response/point_series/_fake_x_aspect.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts similarity index 88% rename from src/legacy/ui/public/agg_response/point_series/_fake_x_aspect.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts index 254a42baeddb0..1bffa4cceb5b0 100644 --- a/src/legacy/ui/public/agg_response/point_series/_fake_x_aspect.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts @@ -18,16 +18,17 @@ */ import { i18n } from '@kbn/i18n'; +import { Aspect } from './point_series'; export function makeFakeXAspect() { return { accessor: -1, - title: i18n.translate('common.ui.aggResponse.allDocsTitle', { + title: i18n.translate('visTypeVislib.aggResponse.allDocsTitle', { defaultMessage: 'All docs', }), params: { defaultValue: '_all', }, format: {}, - }; + } as Aspect; } diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_get_aspects.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts similarity index 53% rename from src/legacy/ui/public/agg_response/point_series/__tests__/_get_aspects.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts index fab5c2e290e7e..450b283abbed2 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_get_aspects.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts @@ -17,37 +17,37 @@ * under the License. */ -import expect from '@kbn/expect'; -import { getAspects } from '../_get_aspects'; +import { getAspects } from './_get_aspects'; +import { Dimension, Dimensions, Aspect } from './point_series'; +import { Table, Row } from '../../types'; describe('getAspects', function() { - let table; - let dimensions; + let table: Table; + let dimensions: Dimensions; - function validate(aspect, i) { - expect(aspect) - .to.be.an('object') - .and.have.property('accessor', i); + function validate(aspect: Aspect, i: string) { + expect(aspect).toEqual(expect.any(Object)); + expect(aspect).toHaveProperty('accessor', i); } - function init(group, x, y) { + function init(group: number, x: number | null, y: number) { table = { columns: [ - { id: '0', title: 'date' }, // date - { id: '1', title: 'date utc_time' }, // date - { id: '2', title: 'ext' }, // extension - { id: '3', title: 'geo.src' }, // extension - { id: '4', title: 'count' }, // count - { id: '5', title: 'avg bytes' }, // avg + { id: '0', name: 'date' }, // date + { id: '1', name: 'date utc_time' }, // date + { id: '2', name: 'ext' }, // extension + { id: '3', name: 'geo.src' }, // extension + { id: '4', name: 'count' }, // count + { id: '5', name: 'avg bytes' }, // avg ], - rows: [], - }; + rows: [] as Row[], + } as Table; dimensions = { - x: { accessor: x }, - y: { accessor: y }, - series: { accessor: group }, - }; + x: { accessor: x } as Dimension, + y: [{ accessor: y } as Dimension], + series: [{ accessor: group } as Dimension], + } as Dimensions; } it('produces an aspect object for each of the aspect types found in the columns', function() { @@ -55,8 +55,8 @@ describe('getAspects', function() { const aspects = getAspects(table, dimensions); validate(aspects.x[0], '0'); - validate(aspects.series[0], '1'); - validate(aspects.y[0], '2'); + validate(aspects.series![0], '1'); + validate(aspects.y![0], '2'); }); it('creates a fake x aspect if the column does not exist', function() { @@ -64,9 +64,8 @@ describe('getAspects', function() { const aspects = getAspects(table, dimensions); - expect(aspects.x[0]) - .to.be.an('object') - .and.have.property('accessor', -1) - .and.have.property('title'); + expect(aspects.x[0]).toEqual(expect.any(Object)); + expect(aspects.x[0]).toHaveProperty('accessor', -1); + expect(aspects.x[0]).toHaveProperty('title'); }); }); diff --git a/src/legacy/ui/public/agg_response/point_series/_get_aspects.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts similarity index 72% rename from src/legacy/ui/public/agg_response/point_series/_get_aspects.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts index fe74d8566c0e7..29134409ddd5f 100644 --- a/src/legacy/ui/public/agg_response/point_series/_get_aspects.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts @@ -18,20 +18,22 @@ */ import { makeFakeXAspect } from './_fake_x_aspect'; +import { Dimensions, Aspects } from './point_series'; +import { Table } from '../../types'; /** * Identify and group the columns based on the aspect of the pointSeries * they represent. * - * @param {array} columns - the list of columns * @return {object} - an object with a key for each aspect (see map). The values - * may be undefined, a single aspect, or an array of aspects. + * may be undefined or an array of aspects. */ -export function getAspects(table, dimensions) { - const aspects = {}; - Object.keys(dimensions).forEach(name => { - const dimension = Array.isArray(dimensions[name]) ? dimensions[name] : [dimensions[name]]; - dimension.forEach(d => { +export function getAspects(table: Table, dimensions: Dimensions) { + const aspects: Partial = {}; + (Object.keys(dimensions) as Array).forEach(name => { + const dimension = dimensions[name]; + const dimensionList = Array.isArray(dimension) ? dimension : [dimension]; + dimensionList.forEach(d => { if (!d) { return; } @@ -42,7 +44,7 @@ export function getAspects(table, dimensions) { if (!aspects[name]) { aspects[name] = []; } - aspects[name].push({ + aspects[name]!.push({ accessor: column.id, column: d.accessor, title: column.name, @@ -56,5 +58,5 @@ export function getAspects(table, dimensions) { aspects.x = [makeFakeXAspect()]; } - return aspects; + return aspects as Aspects; } diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts new file mode 100644 index 0000000000000..0c79c5b263cea --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { IFieldFormatsRegistry } from '../../../../../../../plugins/data/common'; +import { getPoint } from './_get_point'; +import { setFormatService } from '../../../services'; +import { Aspect } from './point_series'; +import { Table, Row, Column } from '../../types'; + +describe('getPoint', function() { + let deserialize: IFieldFormatsRegistry['deserialize']; + + beforeAll(() => { + deserialize = jest.fn(() => ({ + convert: jest.fn(v => v), + })) as any; + + setFormatService({ + deserialize, + } as any); + }); + + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 1, '1': 2, '2': 3 }, + { '0': 4, '1': 'NaN', '2': 6 }, + ], + } as Table; + + describe('Without series aspect', function() { + let seriesAspect: undefined; + let xAspect: Aspect; + let yAspect: Aspect; + + beforeEach(function() { + xAspect = { accessor: '0' } as Aspect; + yAspect = { accessor: '1', title: 'Y' } as Aspect; + }); + + it('properly unwraps values', function() { + const row = table.rows[0]; + const zAspect = { accessor: '2' } as Aspect; + const point = getPoint(table, xAspect, seriesAspect, row, 0, yAspect, zAspect); + + expect(point).toHaveProperty('x', 1); + expect(point).toHaveProperty('y', 2); + expect(point).toHaveProperty('z', 3); + expect(point).toHaveProperty('series', yAspect.title); + }); + + it('ignores points with a y value of NaN', function() { + const row = table.rows[1]; + const point = getPoint(table, xAspect, seriesAspect, row, 1, yAspect); + expect(point).toBe(void 0); + }); + }); + + describe('With series aspect', function() { + let row: Row; + let xAspect: Aspect; + let yAspect: Aspect; + + beforeEach(function() { + row = table.rows[0]; + xAspect = { accessor: '0' } as Aspect; + yAspect = { accessor: '2' } as Aspect; + }); + + it('properly unwraps values', function() { + const seriesAspect = [{ accessor: '1' } as Aspect]; + const point = getPoint(table, xAspect, seriesAspect, row, 0, yAspect); + + expect(point).toHaveProperty('x', 1); + expect(point).toHaveProperty('series', '2'); + expect(point).toHaveProperty('y', 3); + }); + + it('should call deserialize', function() { + const seriesAspect = [ + { accessor: '1', format: { id: 'number', params: { pattern: '$' } } } as Aspect, + ]; + getPoint(table, xAspect, seriesAspect, row, 0, yAspect); + + expect(deserialize).toHaveBeenCalledWith(seriesAspect[0].format); + }); + }); +}); diff --git a/src/legacy/ui/public/agg_response/point_series/_get_point.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts similarity index 69% rename from src/legacy/ui/public/agg_response/point_series/_get_point.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts index 11e639f3f54a8..3fc13eb0c04b5 100644 --- a/src/legacy/ui/public/agg_response/point_series/_get_point.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts @@ -17,19 +17,55 @@ * under the License. */ -import { getFormat } from '../../visualize/loader/pipeline_helpers/utilities'; +import { getFormatService } from '../../../services'; +import { Aspect } from './point_series'; +import { Table, Row } from '../../types'; -export function getPoint(table, x, series, yScale, row, rowIndex, y, z) { +type RowValue = number | string | object | 'NaN'; +interface Raw { + table: Table; + column: number | undefined; + row: number | undefined; + value?: RowValue; +} +export interface Point { + x: RowValue | '_all'; + y: RowValue; + z?: RowValue; + extraMetrics: []; + seriesRaw?: Raw; + xRaw: Raw; + yRaw: Raw; + zRaw?: Raw; + tableRaw?: { + table: Table; + column: number; + row: number; + value: number; + title: string; + }; + parent: Aspect | null; + series?: string; + seriesId?: string; +} +export function getPoint( + table: Table, + x: Aspect, + series: Aspect[] | undefined, + row: Row, + rowIndex: number, + y: Aspect, + z?: Aspect +): Point | undefined { const xRow = x.accessor === -1 ? '_all' : row[x.accessor]; const yRow = row[y.accessor]; const zRow = z && row[z.accessor]; - const point = { + const point: Point = { x: xRow, y: yRow, z: zRow, extraMetrics: [], - yScale: yScale, seriesRaw: series && { table, column: series[0].column, @@ -71,10 +107,9 @@ export function getPoint(table, x, series, yScale, row, rowIndex, y, z) { } if (series) { - const seriesArray = series.length ? series : [series]; - point.series = seriesArray + point.series = series .map(s => { - const fieldFormatter = getFormat(s.format); + const fieldFormatter = getFormatService().deserialize(s.format); return fieldFormatter.convert(row[s.accessor]); }) .join(' - '); @@ -84,9 +119,5 @@ export function getPoint(table, x, series, yScale, row, rowIndex, y, z) { point.series = y.title; } - if (yScale) { - point.y *= yScale; - } - return point; } diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts new file mode 100644 index 0000000000000..6b94b9de8e15f --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts @@ -0,0 +1,281 @@ +/* + * 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 { getSeries } from './_get_series'; +import { setFormatService } from '../../../services'; +import { Chart, Aspect } from './point_series'; +import { Table, Column } from '../../types'; +import { Serie } from './_add_to_siri'; +import { Point } from './_get_point'; + +describe('getSeries', function() { + beforeAll(() => { + setFormatService({ + deserialize: () => ({ + convert: jest.fn(v => v), + }), + } as any); + }); + + it('produces a single series with points for each row', function() { + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + ], + } as Table; + + const chart = { + aspects: { + x: [{ accessor: '0' }], + y: [{ accessor: '1', title: 'y' }], + z: [{ accessor: '2' }], + }, + } as Chart; + + const series = getSeries(table, chart); + + expect(series).toEqual(expect.any(Array)); + expect(series).toHaveLength(1); + + const siri = series[0]; + + expect(siri).toEqual(expect.any(Object)); + expect(siri).toHaveProperty('label', chart.aspects.y[0].title); + expect(siri).toHaveProperty('values'); + + expect(siri.values).toEqual(expect.any(Array)); + expect(siri.values).toHaveLength(5); + + siri.values.forEach(point => { + expect(point).toHaveProperty('x', 1); + expect(point).toHaveProperty('y', 2); + expect(point).toHaveProperty('z', 3); + }); + }); + + it('adds the seriesId to each point', function() { + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + ], + } as Table; + + const chart = { + aspects: { + x: [{ accessor: '0' }], + y: [ + { accessor: '1', title: '0' }, + { accessor: '2', title: '1' }, + ], + }, + } as Chart; + + const series = getSeries(table, chart); + + series[0].values.forEach(point => { + expect(point).toHaveProperty('seriesId', '1'); + }); + + series[1].values.forEach(point => { + expect(point).toHaveProperty('seriesId', '2'); + }); + }); + + it('produces multiple series if there are multiple y aspects', function() { + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + ], + } as Table; + + const chart = { + aspects: { + x: [{ accessor: '0' }], + y: [ + { accessor: '1', title: '0' }, + { accessor: '2', title: '1' }, + ], + }, + } as Chart; + + const series = getSeries(table, chart); + + expect(series).toEqual(expect.any(Array)); + expect(series).toHaveLength(2); + + series.forEach(function(siri: Serie, i: number) { + expect(siri).toEqual(expect.any(Object)); + expect(siri).toHaveProperty('label', '' + i); + expect(siri).toHaveProperty('values'); + + expect(siri.values).toEqual(expect.any(Array)); + expect(siri.values).toHaveLength(5); + + siri.values.forEach(function(point: Point) { + expect(point).toHaveProperty('x', 1); + expect(point).toHaveProperty('y', i + 2); + }); + }); + }); + + it('produces multiple series if there is a series aspect', function() { + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 0, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 0, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 0, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + ], + } as Table; + + const chart = { + aspects: { + x: [{ accessor: -1 } as Aspect], + series: [{ accessor: '0' }], + y: [{ accessor: '1', title: '0' }], + }, + } as Chart; + + const series = getSeries(table, chart); + + expect(series).toEqual(expect.any(Array)); + expect(series).toHaveLength(2); + + series.forEach(function(siri: Serie, i: number) { + expect(siri).toEqual(expect.any(Object)); + expect(siri).toHaveProperty('label', '' + i); + expect(siri).toHaveProperty('values'); + + expect(siri.values).toEqual(expect.any(Array)); + expect(siri.values).toHaveLength(3); + + siri.values.forEach(function(point: Point) { + expect(point).toHaveProperty('y', 2); + }); + }); + }); + + it('produces multiple series if there is a series aspect and multiple y aspects', function() { + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 0, '1': 3, '2': 4 }, + { '0': 1, '1': 3, '2': 4 }, + { '0': 0, '1': 3, '2': 4 }, + { '0': 1, '1': 3, '2': 4 }, + { '0': 0, '1': 3, '2': 4 }, + { '0': 1, '1': 3, '2': 4 }, + ], + } as Table; + + const chart = { + aspects: { + x: [{ accessor: -1 } as Aspect], + series: [{ accessor: '0' }], + y: [ + { accessor: '1', title: '0' }, + { accessor: '2', title: '1' }, + ], + }, + } as Chart; + + const series = getSeries(table, chart); + + expect(series).toEqual(expect.any(Array)); + expect(series).toHaveLength(4); // two series * two metrics + + checkSiri(series[0], '0: 0', 3); + checkSiri(series[1], '0: 1', 4); + checkSiri(series[2], '1: 0', 3); + checkSiri(series[3], '1: 1', 4); + + function checkSiri(siri: Serie, label: string, y: number) { + expect(siri).toEqual(expect.any(Object)); + expect(siri).toHaveProperty('label', label); + expect(siri).toHaveProperty('values'); + + expect(siri.values).toEqual(expect.any(Array)); + expect(siri.values).toHaveLength(3); + + siri.values.forEach(function(point: Point) { + expect(point).toHaveProperty('y', y); + }); + } + }); + + it('produces a series list in the same order as its corresponding metric column', function() { + const table = { + columns: [{ id: '0' }, { id: '1' }, { id: '3' }] as Column[], + rows: [ + { '0': 0, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 0, '1': 2, '2': 3 }, + { '0': 1, '1': 2, '2': 3 }, + { '0': 0, '1': 2, '2': 3 }, + ], + } as Table; + + const chart = { + aspects: { + x: [{ accessor: -1 } as Aspect], + series: [{ accessor: '0' }], + y: [ + { accessor: '1', title: '0' }, + { accessor: '2', title: '1' }, + ], + }, + } as Chart; + + const series = getSeries(table, chart); + expect(series[0]).toHaveProperty('label', '0: 0'); + expect(series[1]).toHaveProperty('label', '0: 1'); + expect(series[2]).toHaveProperty('label', '1: 0'); + expect(series[3]).toHaveProperty('label', '1: 1'); + + // switch the order of the y columns + chart.aspects.y = chart.aspects.y.reverse(); + chart.aspects.y.forEach(function(y: any, i) { + y.i = i; + }); + + const series2 = getSeries(table, chart); + expect(series2[0]).toHaveProperty('label', '0: 1'); + expect(series2[1]).toHaveProperty('label', '0: 0'); + expect(series2[2]).toHaveProperty('label', '1: 1'); + expect(series2[3]).toHaveProperty('label', '1: 0'); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts new file mode 100644 index 0000000000000..edde5b69af022 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts @@ -0,0 +1,88 @@ +/* + * 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 { partial } from 'lodash'; +import { getPoint } from './_get_point'; +import { addToSiri, Serie } from './_add_to_siri'; +import { Chart } from './point_series'; +import { Table } from '../../types'; + +export function getSeries(table: Table, chart: Chart) { + const aspects = chart.aspects; + const xAspect = aspects.x[0]; + const yAspect = aspects.y[0]; + const zAspect = aspects.z && aspects.z[0]; + const multiY = Array.isArray(aspects.y) && aspects.y.length > 1; + + const partGetPoint = partial(getPoint, table, xAspect, aspects.series); + + const seriesMap = new Map(); + + table.rows.forEach((row, rowIndex) => { + if (!multiY) { + const point = partGetPoint(row, rowIndex, yAspect, zAspect); + if (point) { + const id = `${point.series}-${yAspect.accessor}`; + point.seriesId = id; + addToSiri( + seriesMap, + point, + id, + point.series, + yAspect.format, + zAspect && zAspect.format, + zAspect && zAspect.title + ); + } + return; + } + + aspects.y.forEach(function(y) { + const point = partGetPoint(row, rowIndex, y, zAspect); + if (!point) { + return; + } + + // use the point's y-axis as it's series by default, + // but augment that with series aspect if it's actually + // available + let seriesId = y.accessor; + let seriesLabel = y.title; + + if (aspects.series) { + const prefix = point.series ? point.series + ': ' : ''; + seriesId = prefix + seriesId; + seriesLabel = prefix + seriesLabel; + } + + point.seriesId = seriesId; + addToSiri( + seriesMap, + point, + seriesId as string, + seriesLabel, + y.format, + zAspect && zAspect.format, + zAspect && zAspect.title + ); + }); + }); + + return [...seriesMap.values()]; +} diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts similarity index 51% rename from src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts index a8512edee658b..d3049d7675408 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts @@ -17,14 +17,22 @@ * under the License. */ -import expect from '@kbn/expect'; import moment from 'moment'; -import { initXAxis } from '../_init_x_axis'; -import { makeFakeXAspect } from '../_fake_x_aspect'; +import { initXAxis } from './_init_x_axis'; +import { makeFakeXAspect } from './_fake_x_aspect'; +import { + Aspects, + Chart, + DateHistogramOrdered, + DateHistogramParams, + HistogramOrdered, + HistogramParams, +} from './point_series'; +import { Table, Column } from '../../types'; describe('initXAxis', function() { - let chart; - let table; + let chart: Chart; + let table: Table; beforeEach(function() { chart = { @@ -32,50 +40,48 @@ describe('initXAxis', function() { x: [ { ...makeFakeXAspect(), - accessor: 0, + accessor: '0', title: 'label', }, ], - }, - }; + } as Aspects, + } as Chart; table = { - columns: [{ id: '0' }], + columns: [{ id: '0' } as Column], rows: [{ '0': 'hello' }, { '0': 'world' }, { '0': 'foo' }, { '0': 'bar' }, { '0': 'baz' }], }; }); it('sets the xAxisFormatter if the agg is not ordered', function() { initXAxis(chart, table); - expect(chart) - .to.have.property('xAxisLabel', 'label') - .and.have.property('xAxisFormat', chart.aspects.x[0].format); + expect(chart).toHaveProperty('xAxisLabel', 'label'); + expect(chart).toHaveProperty('xAxisFormat', chart.aspects.x[0].format); }); it('makes the chart ordered if the agg is ordered', function() { - chart.aspects.x[0].params.interval = 10; + (chart.aspects.x[0].params as HistogramParams).interval = 10; initXAxis(chart, table); - expect(chart) - .to.have.property('xAxisLabel', 'label') - .and.have.property('xAxisFormat', chart.aspects.x[0].format) - .and.have.property('ordered'); + expect(chart).toHaveProperty('xAxisLabel', 'label'); + expect(chart).toHaveProperty('xAxisFormat', chart.aspects.x[0].format); + expect(chart).toHaveProperty('ordered'); }); describe('xAxisOrderedValues', function() { it('sets the xAxisOrderedValues property', function() { initXAxis(chart, table); - expect(chart).to.have.property('xAxisOrderedValues'); + expect(chart).toHaveProperty('xAxisOrderedValues'); }); it('returns a list of values, preserving the table order', function() { initXAxis(chart, table); - expect(chart.xAxisOrderedValues).to.eql(['hello', 'world', 'foo', 'bar', 'baz']); + expect(chart.xAxisOrderedValues).toEqual(['hello', 'world', 'foo', 'bar', 'baz']); }); it('only returns unique values', function() { table = { - columns: [{ id: '0' }], + columns: [{ id: '0' } as Column], rows: [ { '0': 'hello' }, { '0': 'world' }, @@ -88,45 +94,46 @@ describe('initXAxis', function() { ], }; initXAxis(chart, table); - expect(chart.xAxisOrderedValues).to.eql(['hello', 'world', 'foo', 'bar', 'baz']); + expect(chart.xAxisOrderedValues).toEqual(['hello', 'world', 'foo', 'bar', 'baz']); }); it('returns the defaultValue if using fake x aspect', function() { chart = { aspects: { x: [makeFakeXAspect()], - }, - }; + } as Aspects, + } as Chart; initXAxis(chart, table); - expect(chart.xAxisOrderedValues).to.eql(['_all']); + expect(chart.xAxisOrderedValues).toEqual(['_all']); }); }); it('reads the date interval param from the x agg', function() { - chart.aspects.x[0].params.interval = 'P1D'; - chart.aspects.x[0].params.intervalESValue = 1; - chart.aspects.x[0].params.intervalESUnit = 'd'; - chart.aspects.x[0].params.date = true; + const dateHistogramParams = chart.aspects.x[0].params as DateHistogramParams; + dateHistogramParams.interval = 'P1D'; + dateHistogramParams.intervalESValue = 1; + dateHistogramParams.intervalESUnit = 'd'; + dateHistogramParams.date = true; initXAxis(chart, table); - expect(chart) - .to.have.property('xAxisLabel', 'label') - .and.have.property('xAxisFormat', chart.aspects.x[0].format) - .and.have.property('ordered'); + expect(chart).toHaveProperty('xAxisLabel', 'label'); + expect(chart).toHaveProperty('xAxisFormat', chart.aspects.x[0].format); + expect(chart).toHaveProperty('ordered'); - expect(moment.isDuration(chart.ordered.interval)).to.be(true); - expect(chart.ordered.interval.toISOString()).to.eql('P1D'); - expect(chart.ordered.intervalESValue).to.be(1); - expect(chart.ordered.intervalESUnit).to.be('d'); + expect(chart.ordered).toEqual(expect.any(Object)); + const { intervalESUnit, intervalESValue, interval } = chart.ordered as DateHistogramOrdered; + expect(moment.isDuration(interval)).toBe(true); + expect(interval.toISOString()).toEqual('P1D'); + expect(intervalESValue).toBe(1); + expect(intervalESUnit).toBe('d'); }); it('reads the numeric interval param from the x agg', function() { - chart.aspects.x[0].params.interval = 0.5; + (chart.aspects.x[0].params as HistogramParams).interval = 0.5; initXAxis(chart, table); - expect(chart) - .to.have.property('xAxisLabel', 'label') - .and.have.property('xAxisFormat', chart.aspects.x[0].format) - .and.have.property('ordered'); + expect(chart).toHaveProperty('xAxisLabel', 'label'); + expect(chart).toHaveProperty('xAxisFormat', chart.aspects.x[0].format); + expect(chart).toHaveProperty('ordered'); - expect(chart.ordered.interval).to.eql(0.5); + expect((chart.ordered as HistogramOrdered).interval).toEqual(0.5); }); }); diff --git a/src/legacy/ui/public/agg_response/point_series/_init_x_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts similarity index 73% rename from src/legacy/ui/public/agg_response/point_series/_init_x_axis.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts index 4a81486783b08..9d16c4857be00 100644 --- a/src/legacy/ui/public/agg_response/point_series/_init_x_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts @@ -19,27 +19,31 @@ import { uniq } from 'lodash'; import moment from 'moment'; +import { Chart } from './point_series'; +import { Table } from '../../types'; -export function initXAxis(chart, table) { +export function initXAxis(chart: Chart, table: Table) { const { format, title, params, accessor } = chart.aspects.x[0]; chart.xAxisOrderedValues = - accessor === -1 ? [params.defaultValue] : uniq(table.rows.map(r => r[accessor])); + accessor === -1 && 'defaultValue' in params + ? [params.defaultValue] + : uniq(table.rows.map(r => r[accessor])); chart.xAxisFormat = format; chart.xAxisLabel = title; - const { interval, date } = params; - if (interval) { - if (date) { + if ('interval' in params) { + const { interval } = params; + if ('date' in params) { const { intervalESUnit, intervalESValue } = params; chart.ordered = { interval: moment.duration(interval), - intervalESUnit: intervalESUnit, - intervalESValue: intervalESValue, + intervalESUnit, + intervalESValue, }; } else { chart.ordered = { - interval, + interval: params.interval, }; } } diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_init_y_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts similarity index 81% rename from src/legacy/ui/public/agg_response/point_series/__tests__/_init_y_axis.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts index 78cd5334e6c86..df84d69c9f849 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_init_y_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts @@ -18,8 +18,8 @@ */ import _ from 'lodash'; -import expect from '@kbn/expect'; -import { initYAxis } from '../_init_y_axis'; +import { initYAxis } from './_init_y_axis'; +import { Chart } from './point_series'; describe('initYAxis', function() { const baseChart = { @@ -34,7 +34,7 @@ describe('initYAxis', function() { }, ], }, - }; + } as Chart; describe('with a single y aspect', function() { const singleYBaseChart = _.cloneDeep(baseChart); @@ -43,13 +43,13 @@ describe('initYAxis', function() { it('sets the yAxisFormatter the the field formats convert fn', function() { const chart = _.cloneDeep(singleYBaseChart); initYAxis(chart); - expect(chart).to.have.property('yAxisFormat'); + expect(chart).toHaveProperty('yAxisFormat'); }); it('sets the yAxisLabel', function() { const chart = _.cloneDeep(singleYBaseChart); initYAxis(chart); - expect(chart).to.have.property('yAxisLabel', 'y1'); + expect(chart).toHaveProperty('yAxisLabel', 'y1'); }); }); @@ -58,16 +58,15 @@ describe('initYAxis', function() { const chart = _.cloneDeep(baseChart); initYAxis(chart); - expect(chart).to.have.property('yAxisFormat'); - expect(chart.yAxisFormat) - .to.be(chart.aspects.y[0].format) - .and.not.be(chart.aspects.y[1].format); + expect(chart).toHaveProperty('yAxisFormat'); + expect(chart.yAxisFormat).toBe(chart.aspects.y[0].format); + expect(chart.yAxisFormat).not.toBe(chart.aspects.y[1].format); }); it('does not set the yAxisLabel, it does not make sense to put multiple labels on the same axis', function() { const chart = _.cloneDeep(baseChart); initYAxis(chart); - expect(chart).to.have.property('yAxisLabel', ''); + expect(chart).toHaveProperty('yAxisLabel', ''); }); }); }); diff --git a/src/legacy/ui/public/agg_response/point_series/_init_y_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts similarity index 82% rename from src/legacy/ui/public/agg_response/point_series/_init_y_axis.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts index 42f5e79a63172..43ba0557949ac 100644 --- a/src/legacy/ui/public/agg_response/point_series/_init_y_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts @@ -17,7 +17,9 @@ * under the License. */ -export function initYAxis(chart) { +import { Chart } from './point_series'; + +export function initYAxis(chart: Chart) { const y = chart.aspects.y; if (Array.isArray(y)) { @@ -28,12 +30,7 @@ export function initYAxis(chart) { const z = chart.aspects.series; if (z) { - if (Array.isArray(z)) { - chart.zAxisFormat = z[0].format; - chart.zAxisLabel = ''; - } else { - chart.zAxisFormat = z.format; - chart.zAxisLabel = z.title; - } + chart.zAxisFormat = z[0].format; + chart.zAxisLabel = ''; } } diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_ordered_date_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts similarity index 76% rename from src/legacy/ui/public/agg_response/point_series/__tests__/_ordered_date_axis.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts index 2e08be16278d5..25e466f21c3e7 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_ordered_date_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts @@ -19,8 +19,8 @@ import moment from 'moment'; import _ from 'lodash'; -import expect from '@kbn/expect'; -import { orderedDateAxis } from '../_ordered_date_axis'; +import { orderedDateAxis } from './_ordered_date_axis'; +import { DateHistogramParams, OrderedChart } from './point_series'; describe('orderedDateAxis', function() { const baseArgs = { @@ -46,7 +46,7 @@ describe('orderedDateAxis', function() { }, ], }, - }, + } as OrderedChart, }; describe('ordered object', function() { @@ -54,24 +54,24 @@ describe('orderedDateAxis', function() { const args = _.cloneDeep(baseArgs); orderedDateAxis(args.chart); - expect(args.chart).to.have.property('ordered'); + expect(args.chart).toHaveProperty('ordered'); - expect(args.chart.ordered).to.have.property('date', true); + expect(args.chart.ordered).toHaveProperty('date', true); }); it('sets the min/max when the buckets are bounded', function() { const args = _.cloneDeep(baseArgs); orderedDateAxis(args.chart); - expect(args.chart.ordered).to.have.property('min'); - expect(args.chart.ordered).to.have.property('max'); + expect(args.chart.ordered).toHaveProperty('min'); + expect(args.chart.ordered).toHaveProperty('max'); }); it('does not set the min/max when the buckets are unbounded', function() { const args = _.cloneDeep(baseArgs); - args.chart.aspects.x[0].params.bounds = null; + (args.chart.aspects.x[0].params as DateHistogramParams).bounds = undefined; orderedDateAxis(args.chart); - expect(args.chart.ordered).to.not.have.property('min'); - expect(args.chart.ordered).to.not.have.property('max'); + expect(args.chart.ordered).not.toHaveProperty('min'); + expect(args.chart.ordered).not.toHaveProperty('max'); }); }); }); diff --git a/src/legacy/ui/public/agg_response/point_series/_ordered_date_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts similarity index 72% rename from src/legacy/ui/public/agg_response/point_series/_ordered_date_axis.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts index a1dd50dc6c71b..193b10a563563 100644 --- a/src/legacy/ui/public/agg_response/point_series/_ordered_date_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts @@ -17,17 +17,17 @@ * under the License. */ -// import moment from 'moment'; +import { OrderedChart } from './point_series'; -export function orderedDateAxis(chart) { +export function orderedDateAxis(chart: OrderedChart) { const x = chart.aspects.x[0]; - const { bounds } = x.params; + const bounds = 'bounds' in x.params ? x.params.bounds : undefined; chart.ordered.date = true; if (bounds) { - chart.ordered.min = isNaN(bounds.min) ? Date.parse(bounds.min) : bounds.min; - chart.ordered.max = isNaN(bounds.max) ? Date.parse(bounds.max) : bounds.max; + chart.ordered.min = typeof bounds.min === 'string' ? Date.parse(bounds.min) : bounds.min; + chart.ordered.max = typeof bounds.max === 'string' ? Date.parse(bounds.max) : bounds.max; } else { chart.ordered.endzones = false; } diff --git a/src/legacy/ui/public/agg_response/index.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts similarity index 69% rename from src/legacy/ui/public/agg_response/index.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts index 982c1c25a8101..9bfba4de966be 100644 --- a/src/legacy/ui/public/agg_response/index.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts @@ -17,12 +17,4 @@ * under the License. */ -import { buildHierarchicalData } from './hierarchical/build_hierarchical_data'; -import { buildPointSeriesData } from './point_series/point_series'; -import { search } from '../../../../plugins/data/public'; - -export const aggResponseIndex = { - hierarchical: buildHierarchicalData, - pointSeries: buildPointSeriesData, - tabify: search.tabifyAggResponse, -}; +export { buildPointSeriesData } from './point_series'; diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_main.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts similarity index 62% rename from src/legacy/ui/public/agg_response/point_series/__tests__/_main.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts index a4c23cb537488..3725bf06660e2 100644 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_main.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts @@ -18,17 +18,25 @@ */ import _ from 'lodash'; -import expect from '@kbn/expect'; -import { buildPointSeriesData } from '../point_series'; +import { buildPointSeriesData, Dimensions } from './point_series'; +import { Table, Column } from '../../types'; +import { setFormatService } from '../../../services'; +import { Serie } from './_add_to_siri'; describe('pointSeriesChartDataFromTable', function() { - this.slow(1000); + beforeAll(() => { + setFormatService({ + deserialize: () => ({ + convert: jest.fn(v => v), + }), + } as any); + }); it('handles a table with just a count', function() { const table = { - columns: [{ id: '0' }], + columns: [{ id: '0' } as Column], rows: [{ '0': 100 }], - }; + } as Table; const chartData = buildPointSeriesData(table, { y: [ { @@ -36,16 +44,15 @@ describe('pointSeriesChartDataFromTable', function() { params: {}, }, ], - }); + } as Dimensions); - expect(chartData).to.be.an('object'); - expect(chartData.series).to.be.an('array'); - expect(chartData.series).to.have.length(1); + expect(chartData).toEqual(expect.any(Object)); + expect(chartData.series).toEqual(expect.any(Array)); + expect(chartData.series).toHaveLength(1); const series = chartData.series[0]; - expect(series.values).to.have.length(1); - expect(series.values[0]) - .to.have.property('x', '_all') - .and.have.property('y', 100); + expect(series.values).toHaveLength(1); + expect(series.values[0]).toHaveProperty('x', '_all'); + expect(series.values[0]).toHaveProperty('y', 100); }); it('handles a table with x and y column', function() { @@ -59,21 +66,21 @@ describe('pointSeriesChartDataFromTable', function() { { '0': 2, '1': 200 }, { '0': 3, '1': 200 }, ], - }; + } as Table; const dimensions = { - x: [{ accessor: 0, params: {} }], + x: { accessor: 0, params: {} }, y: [{ accessor: 1, params: {} }], - }; + } as Dimensions; const chartData = buildPointSeriesData(table, dimensions); - expect(chartData).to.be.an('object'); - expect(chartData.series).to.be.an('array'); - expect(chartData.series).to.have.length(1); + expect(chartData).toEqual(expect.any(Object)); + expect(chartData.series).toEqual(expect.any(Array)); + expect(chartData.series).toHaveLength(1); const series = chartData.series[0]; - expect(series).to.have.property('label', 'Count'); - expect(series.values).to.have.length(3); + expect(series).toHaveProperty('label', 'Count'); + expect(series.values).toHaveLength(3); }); it('handles a table with an x and two y aspects', function() { @@ -84,23 +91,23 @@ describe('pointSeriesChartDataFromTable', function() { { '0': 2, '1': 200, '2': 300 }, { '0': 3, '1': 200, '2': 300 }, ], - }; + } as Table; const dimensions = { - x: [{ accessor: 0, params: {} }], + x: { accessor: 0, params: {} }, y: [ { accessor: 1, params: {} }, { accessor: 2, params: {} }, ], - }; + } as Dimensions; const chartData = buildPointSeriesData(table, dimensions); - expect(chartData).to.be.an('object'); - expect(chartData.series).to.be.an('array'); - expect(chartData.series).to.have.length(2); - chartData.series.forEach(function(siri, i) { - expect(siri).to.have.property('label', `Count-${i}`); - expect(siri.values).to.have.length(3); + expect(chartData).toEqual(expect.any(Object)); + expect(chartData.series).toEqual(expect.any(Array)); + expect(chartData.series).toHaveLength(2); + chartData.series.forEach(function(siri: Serie, i: number) { + expect(siri).toHaveProperty('label', `Count-${i}`); + expect(siri.values).toHaveLength(3); }); }); @@ -121,21 +128,21 @@ describe('pointSeriesChartDataFromTable', function() { }; const dimensions = { - x: [{ accessor: 0, params: {} }], + x: { accessor: 0, params: {} }, series: [{ accessor: 1, params: {} }], y: [ { accessor: 2, params: {} }, { accessor: 3, params: {} }, ], - }; + } as Dimensions; const chartData = buildPointSeriesData(table, dimensions); - expect(chartData).to.be.an('object'); - expect(chartData.series).to.be.an('array'); + expect(chartData).toEqual(expect.any(Object)); + expect(chartData.series).toEqual(expect.any(Array)); // one series for each extension, and then one for each metric inside - expect(chartData.series).to.have.length(4); - chartData.series.forEach(function(siri) { - expect(siri.values).to.have.length(2); + expect(chartData.series).toHaveLength(4); + chartData.series.forEach(function(siri: Serie) { + expect(siri.values).toHaveLength(2); }); }); }); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts new file mode 100644 index 0000000000000..a1681e0d71bd3 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts @@ -0,0 +1,118 @@ +/* + * 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 { Duration } from 'moment'; +import { getSeries } from './_get_series'; +import { getAspects } from './_get_aspects'; +import { initYAxis } from './_init_y_axis'; +import { initXAxis } from './_init_x_axis'; +import { orderedDateAxis } from './_ordered_date_axis'; +import { Serie } from './_add_to_siri'; +import { Column, Table } from '../../types'; + +export interface DateHistogramParams { + date: boolean; + interval: string; + intervalESValue: number; + intervalESUnit: string; + format: string; + bounds?: { + min: string | number; + max: string | number; + }; +} +export interface HistogramParams { + interval: number; +} +export interface FakeParams { + defaultValue: string; +} +export interface Dimension { + accessor: number; + format: { + id?: string; + params?: { pattern?: string; [key: string]: any }; + }; + params: DateHistogramParams | HistogramParams | FakeParams | {}; +} + +export interface Dimensions { + x: Dimension | null; + y: Dimension[]; + z?: Dimension[]; + series?: Dimension | Dimension[]; +} +export interface Aspect { + accessor: Column['id']; + column?: Dimension['accessor']; + title: Column['name']; + format: Dimension['format']; + params: Dimension['params']; +} +export type Aspects = { x: Aspect[]; y: Aspect[] } & { [key in keyof Dimensions]?: Aspect[] }; + +export interface DateHistogramOrdered { + interval: Duration; + intervalESUnit: DateHistogramParams['intervalESUnit']; + intervalESValue: DateHistogramParams['intervalESValue']; +} +export interface HistogramOrdered { + interval: HistogramParams['interval']; +} + +type Ordered = (DateHistogramOrdered | HistogramOrdered) & { + date?: boolean; + min?: number; + max?: number; + endzones?: boolean; +}; + +export interface Chart { + aspects: Aspects; + series: Serie[]; + xAxisOrderedValues?: Array; + xAxisFormat?: Dimension['format']; + xAxisLabel?: Column['name']; + yAxisFormat?: Dimension['format']; + yAxisLabel?: Column['name']; + zAxisFormat?: Dimension['format']; + zAxisLabel?: Column['name']; + ordered?: Ordered; +} + +export type OrderedChart = Chart & { ordered: Ordered }; + +export const buildPointSeriesData = (table: Table, dimensions: Dimensions) => { + const chart = { + aspects: getAspects(table, dimensions), + } as Chart; + + initXAxis(chart, table); + initYAxis(chart); + + if ('date' in chart.aspects.x[0].params) { + // initXAxis will turn `chart` into an `OrderedChart if it is a date axis` + orderedDateAxis(chart as OrderedChart); + } + + chart.series = getSeries(table, chart); + + delete chart.aspects; + return chart; +}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.js index 9ba86c5181a4c..b5f80303b1d74 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.js @@ -17,8 +17,8 @@ * under the License. */ -import { buildHierarchicalData, buildPointSeriesData } from '../legacy_imports'; import { getFormatService } from '../services'; +import { buildHierarchicalData, buildPointSeriesData } from './helpers'; function tableResponseHandler(table, dimensions) { const converted = { tables: [] }; @@ -72,7 +72,7 @@ function tableResponseHandler(table, dimensions) { function convertTableGroup(tableGroup, convertTable) { const tables = tableGroup.tables; - if (!tables.length) return; + if (!tables || !tables.length) return; const firstChild = tables[0]; if (firstChild.columns) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.test.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.test.ts new file mode 100644 index 0000000000000..4a8bebc493235 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { setFormatService } from '../services'; + +jest.mock('./helpers', () => ({ + buildHierarchicalData: jest.fn(() => ({})), + buildPointSeriesData: jest.fn(() => ({})), +})); + +// @ts-ignore +import { vislibSeriesResponseHandler, vislibSlicesResponseHandler } from './response_handler'; +import { buildHierarchicalData, buildPointSeriesData } from './helpers'; +import { Table } from './types'; + +describe('response_handler', () => { + describe('vislibSlicesResponseHandler', () => { + test('should not call buildHierarchicalData when no columns', () => { + vislibSlicesResponseHandler({ rows: [] }, {}); + expect(buildHierarchicalData).not.toHaveBeenCalled(); + }); + + test('should call buildHierarchicalData', () => { + const response = { + rows: [{ 'col-0-1': 1 }], + columns: [{ id: 'col-0-1', name: 'Count' }], + }; + const dimensions = { metric: { accessor: 0 } }; + vislibSlicesResponseHandler(response, dimensions); + + expect(buildHierarchicalData).toHaveBeenCalledWith( + { columns: [...response.columns], rows: [...response.rows] }, + dimensions + ); + }); + }); + + describe('vislibSeriesResponseHandler', () => { + let resp: Table; + let expected: any; + + beforeAll(() => { + setFormatService({ + deserialize: () => ({ + convert: jest.fn(v => v), + }), + } as any); + }); + + beforeAll(() => { + resp = { + rows: [ + { 'col-0-3': 158599872, 'col-1-1': 1 }, + { 'col-0-3': 158599893, 'col-1-1': 2 }, + { 'col-0-3': 158599908, 'col-1-1': 1 }, + ], + columns: [ + { id: 'col-0-3', name: 'timestamp per 30 seconds' }, + { id: 'col-1-1', name: 'Count' }, + ], + } as Table; + + const colId = resp.columns[0].id; + expected = [ + { label: `${resp.rows[0][colId]}: ${resp.columns[0].name}` }, + { label: `${resp.rows[1][colId]}: ${resp.columns[0].name}` }, + { label: `${resp.rows[2][colId]}: ${resp.columns[0].name}` }, + ]; + }); + + test('should not call buildPointSeriesData when no columns', () => { + vislibSeriesResponseHandler({ rows: [] }, {}); + expect(buildPointSeriesData).not.toHaveBeenCalled(); + }); + + test('should call buildPointSeriesData', () => { + const response = { + rows: [{ 'col-0-1': 1 }], + columns: [{ id: 'col-0-1', name: 'Count' }], + }; + const dimensions = { x: null, y: { accessor: 0 } }; + vislibSeriesResponseHandler(response, dimensions); + + expect(buildPointSeriesData).toHaveBeenCalledWith( + { columns: [...response.columns], rows: [...response.rows] }, + dimensions + ); + }); + + test('should split columns', () => { + const dimensions = { + x: null, + y: [{ accessor: 1 }], + splitColumn: [{ accessor: 0 }], + }; + + const convertedResp = vislibSlicesResponseHandler(resp, dimensions); + expect(convertedResp.columns).toHaveLength(resp.rows.length); + expect(convertedResp.columns).toEqual(expected); + }); + + test('should split rows', () => { + const dimensions = { + x: null, + y: [{ accessor: 1 }], + splitRow: [{ accessor: 0 }], + }; + + const convertedResp = vislibSlicesResponseHandler(resp, dimensions); + expect(convertedResp.rows).toHaveLength(resp.rows.length); + expect(convertedResp.rows).toEqual(expected); + }); + }); +}); diff --git a/src/legacy/ui/public/agg_response/point_series/_add_to_siri.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/types.ts similarity index 67% rename from src/legacy/ui/public/agg_response/point_series/_add_to_siri.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/types.ts index 9a0fcbc7b267c..ad59603663b84 100644 --- a/src/legacy/ui/public/agg_response/point_series/_add_to_siri.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/types.ts @@ -17,22 +17,26 @@ * under the License. */ -export function addToSiri(series, point, id, yLabel, yFormat, zFormat, zLabel) { - id = id == null ? '' : id + ''; +export interface Column { + // -1 value can be in a fake X aspect + id: string | -1; + name: string; +} - if (series.has(id)) { - series.get(id).values.push(point); - return; - } +export interface Row { + [key: string]: number | string | object; +} - series.set(id, { - id: id.split('-').pop(), - rawId: id, - label: yLabel == null ? id : yLabel, - count: 0, - values: [point], - format: yFormat, - zLabel, - zFormat, - }); +export interface TableParent { + table: Table; + tables?: Table[]; + column: number; + row: number; + key: number; + name: string; +} +export interface Table { + columns: Column[]; + rows: Row[]; + $parent?: TableParent; } diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_add_to_siri.js b/src/legacy/ui/public/agg_response/point_series/__tests__/_add_to_siri.js deleted file mode 100644 index 43a10ebbfb12e..0000000000000 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_add_to_siri.js +++ /dev/null @@ -1,82 +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 expect from '@kbn/expect'; -import { addToSiri } from '../_add_to_siri'; - -describe('addToSiri', function() { - it('creates a new series the first time it sees an id', function() { - const series = new Map(); - const point = {}; - const id = 'id'; - addToSiri(series, point, id, id, { id: id }); - - expect(series.has(id)).to.be(true); - expect(series.get(id)).to.be.an('object'); - expect(series.get(id).label).to.be(id); - expect(series.get(id).values).to.have.length(1); - expect(series.get(id).values[0]).to.be(point); - }); - - it('adds points to existing series if id has been seen', function() { - const series = new Map(); - const id = 'id'; - - const point = {}; - addToSiri(series, point, id, id, { id: id }); - - const point2 = {}; - addToSiri(series, point2, id, id, { id: id }); - - expect(series.has(id)).to.be(true); - expect(series.get(id)).to.be.an('object'); - expect(series.get(id).label).to.be(id); - expect(series.get(id).values).to.have.length(2); - expect(series.get(id).values[0]).to.be(point); - expect(series.get(id).values[1]).to.be(point2); - }); - - it('allows overriding the series label', function() { - const series = new Map(); - const id = 'id'; - const label = 'label'; - const point = {}; - addToSiri(series, point, id, label, { id: id }); - - expect(series.has(id)).to.be(true); - expect(series.get(id)).to.be.an('object'); - expect(series.get(id).label).to.be(label); - expect(series.get(id).values).to.have.length(1); - expect(series.get(id).values[0]).to.be(point); - }); - - it('correctly sets id and rawId', function() { - const series = new Map(); - const id = 'id-id2'; - - const point = {}; - addToSiri(series, point, id); - - expect(series.has(id)).to.be(true); - expect(series.get(id)).to.be.an('object'); - expect(series.get(id).label).to.be(id); - expect(series.get(id).rawId).to.be(id); - expect(series.get(id).id).to.be('id2'); - }); -}); diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_get_point.js b/src/legacy/ui/public/agg_response/point_series/__tests__/_get_point.js deleted file mode 100644 index 0eb2c608d6d6c..0000000000000 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_get_point.js +++ /dev/null @@ -1,97 +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 expect from '@kbn/expect'; -import { getPoint } from '../_get_point'; - -describe('getPoint', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 1, '1': 2, '2': 3 }, - { '0': 4, '1': 'NaN', '2': 6 }, - ], - }; - - describe('Without series aspect', function() { - let seriesAspect; - let xAspect; - let yAspect; - let yScale; - - beforeEach(function() { - seriesAspect = null; - xAspect = { accessor: 0 }; - yAspect = { accessor: 1, title: 'Y' }; - yScale = 5; - }); - - it('properly unwraps and scales values', function() { - const row = table.rows[0]; - const zAspect = { accessor: 2 }; - const point = getPoint(table, xAspect, seriesAspect, yScale, row, 0, yAspect, zAspect); - - expect(point) - .to.have.property('x', 1) - .and.have.property('y', 10) - .and.have.property('z', 3) - .and.have.property('series', yAspect.title); - }); - - it('ignores points with a y value of NaN', function() { - const row = table.rows[1]; - const point = getPoint(table, xAspect, seriesAspect, yScale, row, 1, yAspect); - expect(point).to.be(void 0); - }); - }); - - describe('With series aspect', function() { - let row; - let xAspect; - let yAspect; - let yScale; - - beforeEach(function() { - row = table.rows[0]; - xAspect = { accessor: 0 }; - yAspect = { accessor: 2 }; - yScale = null; - }); - - it('properly unwraps and scales values', function() { - const seriesAspect = [{ accessor: 1 }]; - const point = getPoint(table, xAspect, seriesAspect, yScale, row, 0, yAspect); - - expect(point) - .to.have.property('x', 1) - .and.have.property('series', '2') - .and.have.property('y', 3); - }); - - it('properly formats series values', function() { - const seriesAspect = [{ accessor: 1, format: { id: 'number', params: { pattern: '$' } } }]; - const point = getPoint(table, xAspect, seriesAspect, yScale, row, 0, yAspect); - - expect(point) - .to.have.property('x', 1) - .and.have.property('series', '$2') - .and.have.property('y', 3); - }); - }); -}); diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_get_series.js b/src/legacy/ui/public/agg_response/point_series/__tests__/_get_series.js deleted file mode 100644 index 1727994976383..0000000000000 --- a/src/legacy/ui/public/agg_response/point_series/__tests__/_get_series.js +++ /dev/null @@ -1,283 +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 _ from 'lodash'; -import expect from '@kbn/expect'; -import { getSeries } from '../_get_series'; - -describe('getSeries', function() { - it('produces a single series with points for each row', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - ], - }; - - const chart = { - aspects: { - x: [{ accessor: 0 }], - y: [{ accessor: 1, title: 'y' }], - z: { accessor: 2 }, - }, - }; - - const series = getSeries(table, chart); - - expect(series) - .to.be.an('array') - .and.to.have.length(1); - - const siri = series[0]; - expect(siri) - .to.be.an('object') - .and.have.property('label', chart.aspects.y.title) - .and.have.property('values'); - - expect(siri.values) - .to.be.an('array') - .and.have.length(5); - - siri.values.forEach(function(point) { - expect(point) - .to.have.property('x', 1) - .and.property('y', 2) - .and.property('z', 3); - }); - }); - - it('adds the seriesId to each point', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - ], - }; - - const chart = { - aspects: { - x: [{ accessor: 0 }], - y: [ - { accessor: 1, title: '0' }, - { accessor: 2, title: '1' }, - ], - }, - }; - - const series = getSeries(table, chart); - - series[0].values.forEach(function(point) { - expect(point).to.have.property('seriesId', 1); - }); - - series[1].values.forEach(function(point) { - expect(point).to.have.property('seriesId', 2); - }); - }); - - it('produces multiple series if there are multiple y aspects', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - ], - }; - - const chart = { - aspects: { - x: [{ accessor: 0 }], - y: [ - { accessor: 1, title: '0' }, - { accessor: 2, title: '1' }, - ], - }, - }; - - const series = getSeries(table, chart); - - expect(series) - .to.be.an('array') - .and.to.have.length(2); - - series.forEach(function(siri, i) { - expect(siri) - .to.be.an('object') - .and.have.property('label', '' + i) - .and.have.property('values'); - - expect(siri.values) - .to.be.an('array') - .and.have.length(5); - - siri.values.forEach(function(point) { - expect(point) - .to.have.property('x', 1) - .and.property('y', i + 2); - }); - }); - }); - - it('produces multiple series if there is a series aspect', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 0, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 0, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 0, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - ], - }; - - const chart = { - aspects: { - x: [{ accessor: -1 }], - series: [{ accessor: 0, fieldFormatter: _.identity }], - y: [{ accessor: 1, title: '0' }], - }, - }; - - const series = getSeries(table, chart); - - expect(series) - .to.be.an('array') - .and.to.have.length(2); - - series.forEach(function(siri, i) { - expect(siri) - .to.be.an('object') - .and.have.property('label', '' + i) - .and.have.property('values'); - - expect(siri.values) - .to.be.an('array') - .and.have.length(3); - - siri.values.forEach(function(point) { - expect(point).to.have.property('y', 2); - }); - }); - }); - - it('produces multiple series if there is a series aspect and multiple y aspects', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 0, '1': 3, '2': 4 }, - { '0': 1, '1': 3, '2': 4 }, - { '0': 0, '1': 3, '2': 4 }, - { '0': 1, '1': 3, '2': 4 }, - { '0': 0, '1': 3, '2': 4 }, - { '0': 1, '1': 3, '2': 4 }, - ], - }; - - const chart = { - aspects: { - x: [{ accessor: -1 }], - series: [{ accessor: 0, fieldFormatter: _.identity }], - y: [ - { accessor: 1, title: '0' }, - { accessor: 2, title: '1' }, - ], - }, - }; - - const series = getSeries(table, chart); - - expect(series) - .to.be.an('array') - .and.to.have.length(4); // two series * two metrics - - checkSiri(series[0], '0: 0', 3); - checkSiri(series[1], '0: 1', 4); - checkSiri(series[2], '1: 0', 3); - checkSiri(series[3], '1: 1', 4); - - function checkSiri(siri, label, y) { - expect(siri) - .to.be.an('object') - .and.have.property('label', label) - .and.have.property('values'); - - expect(siri.values) - .to.be.an('array') - .and.have.length(3); - - siri.values.forEach(function(point) { - expect(point).to.have.property('y', y); - }); - } - }); - - it('produces a series list in the same order as its corresponding metric column', function() { - const table = { - columns: [{ id: '0' }, { id: '1' }, { id: '3' }], - rows: [ - { '0': 0, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 0, '1': 2, '2': 3 }, - { '0': 1, '1': 2, '2': 3 }, - { '0': 0, '1': 2, '2': 3 }, - ], - }; - - const chart = { - aspects: { - x: [{ accessor: -1 }], - series: [{ accessor: 0, fieldFormatter: _.identity }], - y: [ - { accessor: 1, title: '0' }, - { accessor: 2, title: '1' }, - ], - }, - }; - - const series = getSeries(table, chart); - expect(series[0]).to.have.property('label', '0: 0'); - expect(series[1]).to.have.property('label', '0: 1'); - expect(series[2]).to.have.property('label', '1: 0'); - expect(series[3]).to.have.property('label', '1: 1'); - - // switch the order of the y columns - chart.aspects.y = chart.aspects.y.reverse(); - chart.aspects.y.forEach(function(y, i) { - y.i = i; - }); - - const series2 = getSeries(table, chart); - expect(series2[0]).to.have.property('label', '0: 1'); - expect(series2[1]).to.have.property('label', '0: 0'); - expect(series2[2]).to.have.property('label', '1: 1'); - expect(series2[3]).to.have.property('label', '1: 0'); - }); -}); diff --git a/src/legacy/ui/public/agg_response/point_series/_get_series.js b/src/legacy/ui/public/agg_response/point_series/_get_series.js deleted file mode 100644 index 73c1735191abc..0000000000000 --- a/src/legacy/ui/public/agg_response/point_series/_get_series.js +++ /dev/null @@ -1,100 +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 _ from 'lodash'; -import { getPoint } from './_get_point'; -import { addToSiri } from './_add_to_siri'; - -export function getSeries(table, chart) { - const aspects = chart.aspects; - const xAspect = aspects.x[0]; - const yAspect = aspects.y[0]; - const zAspect = aspects.z && aspects.z.length ? aspects.z[0] : aspects.z; - const multiY = Array.isArray(aspects.y) && aspects.y.length > 1; - const yScale = chart.yScale; - - const partGetPoint = _.partial(getPoint, table, xAspect, aspects.series, yScale); - - let series = _(table.rows) - .transform(function(series, row, rowIndex) { - if (!multiY) { - const point = partGetPoint(row, rowIndex, yAspect, zAspect); - if (point) { - const id = `${point.series}-${yAspect.accessor}`; - point.seriesId = id; - addToSiri( - series, - point, - id, - point.series, - yAspect.format, - zAspect && zAspect.format, - zAspect && zAspect.title - ); - } - return; - } - - aspects.y.forEach(function(y) { - const point = partGetPoint(row, rowIndex, y, zAspect); - if (!point) return; - - // use the point's y-axis as it's series by default, - // but augment that with series aspect if it's actually - // available - let seriesId = y.accessor; - let seriesLabel = y.title; - - if (aspects.series) { - const prefix = point.series ? point.series + ': ' : ''; - seriesId = prefix + seriesId; - seriesLabel = prefix + seriesLabel; - } - - point.seriesId = seriesId; - addToSiri( - series, - point, - seriesId, - seriesLabel, - y.format, - zAspect && zAspect.format, - zAspect && zAspect.title - ); - }); - }, new Map()) - .thru(series => [...series.values()]) - .value(); - - if (multiY) { - series = _.sortBy(series, function(siri) { - const firstVal = siri.values[0]; - let y; - - if (firstVal) { - y = _.find(aspects.y, function(y) { - return y.accessor === firstVal.accessor; - }); - } - - return y ? y.i : series.length; - }); - } - return series; -} diff --git a/src/legacy/ui/public/agg_response/point_series/point_series.js b/src/legacy/ui/public/agg_response/point_series/point_series.js deleted file mode 100644 index 8489f7bc2ca45..0000000000000 --- a/src/legacy/ui/public/agg_response/point_series/point_series.js +++ /dev/null @@ -1,42 +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 { getSeries } from './_get_series'; -import { getAspects } from './_get_aspects'; -import { initYAxis } from './_init_y_axis'; -import { initXAxis } from './_init_x_axis'; -import { orderedDateAxis } from './_ordered_date_axis'; - -export const buildPointSeriesData = (table, dimensions) => { - const chart = { - aspects: getAspects(table, dimensions), - }; - - initXAxis(chart, table); - initYAxis(chart); - - if (chart.aspects.x[0].params.date) { - orderedDateAxis(chart); - } - - chart.series = getSeries(table, chart); - - delete chart.aspects; - return chart; -}; diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index 905c88a6d18a0..532c49803e7b0 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -27,7 +27,6 @@ import 'uiExports/search'; import 'uiExports/shareContextMenuExtensions'; import _ from 'lodash'; import 'ui/autoload/all'; -import 'ui/agg_response'; import 'leaflet'; import { npStart } from 'ui/new_platform'; import { localApplicationService } from 'plugins/kibana/local_application_service'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d357e40c02934..705a4577cbd07 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -130,7 +130,6 @@ "charts.colormaps.greysText": "グレー", "charts.colormaps.redsText": "赤", "charts.colormaps.yellowToRedText": "黄色から赤", - "common.ui.aggResponse.allDocsTitle": "すべてのドキュメント", "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "エラー", "common.ui.errorAutoCreateIndex.errorDescription": "Elasticsearch クラスターの {autoCreateIndexActionConfig} 設定が原因で、Kibana が保存されたオブジェクトを格納するインデックスを自動的に作成できないようです。Kibana は、保存されたオブジェクトインデックスが適切なマッピング/スキーマを使用し Kibana から Elasticsearch へのポーリングの回数を減らすための最適な手段であるため、この Elasticsearch の機能を使用します。", "common.ui.errorAutoCreateIndex.errorDisclaimer": "申し訳ございませんが、この問題が解決されるまで Kibana で何も保存することができません。", @@ -3809,6 +3808,7 @@ "visTypeVega.visualization.renderErrorTitle": "Vega エラー", "visTypeVega.visualization.unableToFindDefaultIndexErrorMessage": "デフォルトのインデックスが見つかりません", "visTypeVega.visualization.unableToRenderWithoutDataWarningMessage": "データなしにはレンダリングできません", + "visTypeVislib.aggResponse.allDocsTitle": "すべてのドキュメント", "visTypeVislib.area.areaDescription": "折れ線グラフの下の数量を強調します。", "visTypeVislib.area.areaTitle": "エリア", "visTypeVislib.area.countText": "カウント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 839c89f3b1cae..50b807a4934ed 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -130,7 +130,6 @@ "charts.colormaps.greysText": "灰色", "charts.colormaps.redsText": "红色", "charts.colormaps.yellowToRedText": "黄到红", - "common.ui.aggResponse.allDocsTitle": "所有文档", "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "错误", "common.ui.errorAutoCreateIndex.errorDescription": "似乎 Elasticsearch 集群的 {autoCreateIndexActionConfig} 设置使 Kibana 无法自动创建用于存储已保存对象的索引。Kibana 将使用此 Elasticsearch 功能,因为这是确保已保存对象索引使用正确映射/架构的最好方式,而且其允许 Kibana 较少地轮询 Elasticsearch。", "common.ui.errorAutoCreateIndex.errorDisclaimer": "但是,只有解决了此问题后,您才能在 Kibana 保存内容。", @@ -3810,6 +3809,7 @@ "visTypeVega.visualization.renderErrorTitle": "Vega 错误", "visTypeVega.visualization.unableToFindDefaultIndexErrorMessage": "找不到默认索引", "visTypeVega.visualization.unableToRenderWithoutDataWarningMessage": "没有数据时无法渲染", + "visTypeVislib.aggResponse.allDocsTitle": "所有文档", "visTypeVislib.area.areaDescription": "突出折线图下方的数量", "visTypeVislib.area.areaTitle": "面积图", "visTypeVislib.area.countText": "计数", From b73fe279d6609162530619b4582f7f1da35a88c6 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 9 Apr 2020 09:44:33 -0700 Subject: [PATCH 63/81] [EPM] Update UI copy to use `integration` (#63077) * Update epm copy to say Integrations * Update copy in create data source flow * Update copy in data sources table * Fix missed copies * Remove unused translation keys (they were renamed & strings were changed) --- .../ingest_manager/layouts/default.tsx | 4 ++-- .../components/layout.tsx | 2 +- .../components/navigation.tsx | 2 +- .../create_datasource_page/index.tsx | 2 +- .../step_select_package.tsx | 8 ++++---- .../datasources/datasources_table.tsx | 2 +- .../epm/components/package_list_grid.tsx | 2 +- .../sections/epm/screens/detail/header.tsx | 8 +++++++- .../sections/epm/screens/home/header.tsx | 4 ++-- .../sections/epm/screens/home/index.tsx | 18 +++++++++--------- .../translations/translations/ja-JP.json | 5 ----- .../translations/translations/zh-CN.json | 5 ----- 12 files changed, 29 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 8ec2d2ec03b35..26f2c85a291a3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -55,8 +55,8 @@ export const DefaultLayout: React.FunctionComponent = ({ section, childre disabled={!epm?.enabled} > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx index 8bb7b2553c1b1..dd242f366e8c0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx @@ -88,7 +88,7 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/navigation.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/navigation.tsx index 099a7a83caa10..7dae981e65c30 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/navigation.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/navigation.tsx @@ -27,7 +27,7 @@ export const CreateDatasourceStepsNavigation: React.FunctionComponent<{ from === 'config' ? { title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectPackageLabel', { - defaultMessage: 'Select package', + defaultMessage: 'Select integration', }), isSelected: currentStep === 'selectPackage', isComplete: diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 7815ab9cd1d6e..461bb750ca6f5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -221,7 +221,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { {from === 'config' ? ( ) : ( } error={packagesError} @@ -114,7 +114,7 @@ export const StepSelectPackage: React.FunctionComponent<{

@@ -149,7 +149,7 @@ export const StepSelectPackage: React.FunctionComponent<{ placeholder: i18n.translate( 'xpack.ingestManager.createDatasource.stepSelectPackage.filterPackagesInputPlaceholder', { - defaultMessage: 'Search for packages', + defaultMessage: 'Search for integrations', } ), }} @@ -179,7 +179,7 @@ export const StepSelectPackage: React.FunctionComponent<{ title={ } error={selectedPkgError} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx index 87155afdc21be..1eee9f6b0c346 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx @@ -138,7 +138,7 @@ export const DatasourcesTable: React.FunctionComponent = ({ name: i18n.translate( 'xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle', { - defaultMessage: 'Package', + defaultMessage: 'Integration', } ), render(packageTitle: string, datasource: InMemoryDatasource) { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx index 2ca49298decf9..818b365d5be12 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx @@ -62,7 +62,7 @@ export function PackageListGrid({ query={searchTerm} box={{ placeholder: i18n.translate('xpack.ingestManager.epmList.searchPackagesPlaceholder', { - defaultMessage: 'Search for a package', + defaultMessage: 'Search for integrations', }), incremental: true, }} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx index a7204dd722603..d83910f29f1a7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx @@ -5,6 +5,7 @@ */ import React, { Fragment } from 'react'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiTitle, IconType, EuiButton } from '@elastic/eui'; import { PackageInfo } from '../../../../types'; @@ -41,7 +42,12 @@ export function Header(props: HeaderProps) { return ( - + {iconType ? ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx index 4230775c04e00..4d6c02eeef8b4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx @@ -17,7 +17,7 @@ export const HeroCopy = memo(() => {

@@ -27,7 +27,7 @@ export const HeroCopy = memo(() => {

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx index 5f215b7788259..bf785147502b5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx @@ -35,16 +35,16 @@ export function EPMHomePage() { ([ { id: 'all_packages', - name: i18n.translate('xpack.ingestManager.epmList.allPackagesTabText', { - defaultMessage: 'All packages', + name: i18n.translate('xpack.ingestManager.epmList.allTabText', { + defaultMessage: 'All integrations', }), href: ALL_PACKAGES_URI, isSelected: tabId !== 'installed', }, { id: 'installed_packages', - name: i18n.translate('xpack.ingestManager.epmList.installedPackagesTabText', { - defaultMessage: 'Installed packages', + name: i18n.translate('xpack.ingestManager.epmList.installedTabText', { + defaultMessage: 'Installed integrations', }), href: INSTALLED_PACKAGES_URI, isSelected: tabId === 'installed', @@ -72,14 +72,14 @@ function InstalledPackages() { ? allPackages.response.filter(pkg => pkg.status === 'installed') : []; - const title = i18n.translate('xpack.ingestManager.epmList.installedPackagesTitle', { - defaultMessage: 'Installed packages', + const title = i18n.translate('xpack.ingestManager.epmList.installedTitle', { + defaultMessage: 'Installed integrations', }); const categories = [ { id: '', - title: i18n.translate('xpack.ingestManager.epmList.allPackagesFilterLinkText', { + title: i18n.translate('xpack.ingestManager.epmList.allFilterLinkText', { defaultMessage: 'All', }), count: packages.length, @@ -120,8 +120,8 @@ function AvailablePackages() { const packages = categoryPackagesRes && categoryPackagesRes.response ? categoryPackagesRes.response : []; - const title = i18n.translate('xpack.ingestManager.epmList.allPackagesTitle', { - defaultMessage: 'All packages', + const title = i18n.translate('xpack.ingestManager.epmList.allTitle', { + defaultMessage: 'All integrations', }); const categories = [ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 705a4577cbd07..687834a683c4d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8389,7 +8389,6 @@ "xpack.ingestManager.appNavigation.configurationsLinkText": "構成", "xpack.ingestManager.appNavigation.fleetLinkText": "フリート", "xpack.ingestManager.appNavigation.overviewLinkText": "概要", - "xpack.ingestManager.appNavigation.packagesLinkText": "パッケージ", "xpack.ingestManager.appTitle": "Ingest Manager", "xpack.ingestManager.configDetails.addDatasourceButtonText": "データソースを作成", "xpack.ingestManager.configDetails.configDetailsTitle": "構成「{id}」", @@ -8515,10 +8514,6 @@ "xpack.ingestManager.epm.pageSubtitle": "人気のアプリやサービスのパッケージを参照する", "xpack.ingestManager.epm.pageTitle": "Elastic Package Manager", "xpack.ingestManager.epmList.allPackagesFilterLinkText": "すべて", - "xpack.ingestManager.epmList.allPackagesTabText": "すべてのパッケージ", - "xpack.ingestManager.epmList.allPackagesTitle": "すべてのパッケージ", - "xpack.ingestManager.epmList.installedPackagesTabText": "パッケージをインストールしました", - "xpack.ingestManager.epmList.installedPackagesTitle": "パッケージをインストールしました", "xpack.ingestManager.epmList.noPackagesFoundPlaceholder": "パッケージが見つかりません", "xpack.ingestManager.epmList.searchPackagesPlaceholder": "パッケージを検索", "xpack.ingestManager.epmList.updatesAvailableFilterLinkText": "更新が可能です", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 50b807a4934ed..58905787da8d5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8392,7 +8392,6 @@ "xpack.ingestManager.appNavigation.configurationsLinkText": "配置", "xpack.ingestManager.appNavigation.fleetLinkText": "Fleet", "xpack.ingestManager.appNavigation.overviewLinkText": "概览", - "xpack.ingestManager.appNavigation.packagesLinkText": "软件包", "xpack.ingestManager.appTitle": "Ingest Manager", "xpack.ingestManager.configDetails.addDatasourceButtonText": "创建数据源", "xpack.ingestManager.configDetails.configDetailsTitle": "配置“{id}”", @@ -8518,10 +8517,6 @@ "xpack.ingestManager.epm.pageSubtitle": "浏览热门应用和服务的软件。", "xpack.ingestManager.epm.pageTitle": "Elastic Package Manager", "xpack.ingestManager.epmList.allPackagesFilterLinkText": "全部", - "xpack.ingestManager.epmList.allPackagesTabText": "所有软件包", - "xpack.ingestManager.epmList.allPackagesTitle": "所有软件包", - "xpack.ingestManager.epmList.installedPackagesTabText": "已安装软件包", - "xpack.ingestManager.epmList.installedPackagesTitle": "已安装软件包", "xpack.ingestManager.epmList.noPackagesFoundPlaceholder": "未找到任何软件包", "xpack.ingestManager.epmList.searchPackagesPlaceholder": "搜索软件包", "xpack.ingestManager.epmList.updatesAvailableFilterLinkText": "有可用更新", From 59c044ff00d88d63c7bf30685a6f1371c7164f8c Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Thu, 9 Apr 2020 12:42:30 -0500 Subject: [PATCH 64/81] [DOCS] Removed references to right (#62508) --- docs/apm/spans.asciidoc | 2 +- docs/apm/transactions.asciidoc | 1 + docs/canvas/canvas-share-workpad.asciidoc | 4 ++-- docs/canvas/canvas-tutorial.asciidoc | 6 +++--- docs/canvas/canvas-workpad.asciidoc | 2 +- .../alerting/alert-management.asciidoc | 14 ++++++------- .../alerting/connector-management.asciidoc | 8 ++++---- docs/management/index-patterns.asciidoc | 1 - docs/management/managing-fields.asciidoc | 2 +- .../create_and_manage_rollups.asciidoc | 4 ++-- docs/maps/geojson-upload.asciidoc | 3 +-- .../indexing-geojson-data-tutorial.asciidoc | 4 ++-- docs/maps/maps-getting-started.asciidoc | 5 ++--- docs/maps/search.asciidoc | 2 +- docs/user/alerting/defining-alerts.asciidoc | 20 +++++++++---------- docs/user/dashboard.asciidoc | 2 +- docs/user/discover.asciidoc | 2 +- docs/user/graph/getting-started.asciidoc | 4 ++-- docs/user/monitoring/beats-details.asciidoc | 4 ++-- docs/visualize/lens.asciidoc | 12 +++++------ 20 files changed, 50 insertions(+), 52 deletions(-) diff --git a/docs/apm/spans.asciidoc b/docs/apm/spans.asciidoc index b09de576f2d4a..ef21e1c5333e0 100644 --- a/docs/apm/spans.asciidoc +++ b/docs/apm/spans.asciidoc @@ -34,4 +34,4 @@ which indicates the next transaction in the trace. These transactions can be expanded and viewed in detail by clicking on them. After exploring these traces, -you can return to the full trace by clicking *View full trace* in the upper right hand corner of the page. +you can return to the full trace by clicking *View full trace*. diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 5c92afa55109d..1eb037009efff 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -105,6 +105,7 @@ image::apm/images/apm-transaction-duration-dist.png[Example view of transactions This graph shows a typical distribution, and indicates most of our requests were served quickly - awesome! It's the requests on the right, the ones taking longer than average, that we probably want to focus on. + When you select one of these buckets, you're presented with up to ten trace samples. Each sample has a span timeline waterfall that shows what a typical request in that bucket was doing. diff --git a/docs/canvas/canvas-share-workpad.asciidoc b/docs/canvas/canvas-share-workpad.asciidoc index ee29926914ad6..5cae3fcc7b531 100644 --- a/docs/canvas/canvas-share-workpad.asciidoc +++ b/docs/canvas/canvas-share-workpad.asciidoc @@ -76,7 +76,7 @@ After you've added the workpad to your website, you can change the autoplay and To change the autoplay settings: -. In the lower right corner of the shareable workpad, click the settings icon. +. Click the settings icon. . Click *Auto Play*, then change the settings. + @@ -85,7 +85,7 @@ image::images/canvas_share_autoplay_480.gif[Autoplay settings] To change the toolbar settings: -. In the lower right corner, click the settings icon. +. Click the settings icon. . Click *Toolbar*, then change the settings. + diff --git a/docs/canvas/canvas-tutorial.asciidoc b/docs/canvas/canvas-tutorial.asciidoc index b6d684bdf5dde..a38ab4a69598e 100644 --- a/docs/canvas/canvas-tutorial.asciidoc +++ b/docs/canvas/canvas-tutorial.asciidoc @@ -18,7 +18,7 @@ Your first step to working with Canvas is to create a workpad. . Click *Create workpad*. -. To add a *Name* for your workpad, use the editor on the right. For example, `My Canvas Workpad`. +. To add a *Name* for your workpad, use the editor. For example, `My Canvas Workpad`. [float] === Customize your workpad with images @@ -29,7 +29,7 @@ To customize your workpad to look the way you want, add your own images. + The default Elastic logo image appears on your page. -. To replace the Elastic logo with your own image, select the image, then use the editor on the right. +. To replace the Elastic logo with your own image, select the image, then use the editor. . To move the image, click and drag it to your preferred location. @@ -73,7 +73,7 @@ You'll notice that the error is gone, but the number could use some formatting. . To format the number, use the Canvas expression language. -.. In the lower right corner, click *Expression editor*. +.. Click *Expression editor*. + You're now looking at the raw data syntax that Canvas uses to display the element. diff --git a/docs/canvas/canvas-workpad.asciidoc b/docs/canvas/canvas-workpad.asciidoc index c5c163441439c..42eedf55c404d 100644 --- a/docs/canvas/canvas-workpad.asciidoc +++ b/docs/canvas/canvas-workpad.asciidoc @@ -124,7 +124,7 @@ Organize your ideas onto separate pages by adding more pages. . Click *Page 1*, then click *+*. -. On the *Page* editor panel on the right, select the page transition from the *Transition* dropdown. +. On the *Page* editor panel, select the page transition from the *Transition* dropdown. + [role="screenshot"] image::images/canvas-add-pages.gif[Add pages] diff --git a/docs/management/alerting/alert-management.asciidoc b/docs/management/alerting/alert-management.asciidoc index caf260937b7be..73cf40c4d7c40 100644 --- a/docs/management/alerting/alert-management.asciidoc +++ b/docs/management/alerting/alert-management.asciidoc @@ -18,9 +18,9 @@ For more information on alerting concepts and the types of alerts and actions av [float] ==== Finding alerts -The *Alerts* tab lists all alerts in the current space, including summary information about their execution frequency, tags, and type. +The *Alerts* tab lists all alerts in the current space, including summary information about their execution frequency, tags, and type. -The *search bar* can be used to quickly find alerts by name or tag. +The *search bar* can be used to quickly find alerts by name or tag. [role="screenshot"] image::images/alerts-filter-by-search.png[Filtering the alerts list using the search bar] @@ -30,7 +30,7 @@ The *type* dropdown lets you filter to a subset of alert types. [role="screenshot"] image::images/alerts-filter-by-type.png[Filtering the alerts list by types of alert] -The *Action type* dropdown lets you filter by the type of action used in the alert. +The *Action type* dropdown lets you filter by the type of action used in the alert. [role="screenshot"] image::images/alerts-filter-by-action-type.png[Filtering the alert list by type of action] @@ -39,16 +39,16 @@ image::images/alerts-filter-by-action-type.png[Filtering the alert list by type [[create-edit-alerts]] ==== Creating and editing alerts -Many alerts must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic alert types can be created in the *Alerts* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting an alert type and configuring it's properties. Refer to <> for details on what types of alerts are available and how to configure them. +Many alerts must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic alert types can be created in the *Alerts* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting an alert type and configuring it's properties. Refer to <> for details on what types of alerts are available and how to configure them. -After an alert is created, you can re-open the flyout and change an alerts properties by clicking the *Edit* button shown on each row of the alert listing. +After an alert is created, you can re-open the flyout and change an alerts properties by clicking the *Edit* button shown on each row of the alert listing. [float] [[controlling-alerts]] ==== Controlling alerts -The alert listing allows you to quickly mute/unmute, disable/enable, and delete individual alerts by clicking the action button at the right of each row. +The alert listing allows you to quickly mute/unmute, disable/enable, and delete individual alerts by clicking the action button. [role="screenshot"] image:management/alerting/images/individual-mute-disable.png[The actions button allows an individual alert to be muted, disabled, or deleted] @@ -56,4 +56,4 @@ image:management/alerting/images/individual-mute-disable.png[The actions button These operations can also be performed in bulk by multi-selecting alerts and clicking the *Manage alerts* button: [role="screenshot"] -image:management/alerting/images/bulk-mute-disable.png[The Manage alerts button lets you mute/unmute, enable/disable, and delete in bulk] \ No newline at end of file +image:management/alerting/images/bulk-mute-disable.png[The Manage alerts button lets you mute/unmute, enable/disable, and delete in bulk] diff --git a/docs/management/alerting/connector-management.asciidoc b/docs/management/alerting/connector-management.asciidoc index 1002a372f9460..46e106e6e9648 100644 --- a/docs/management/alerting/connector-management.asciidoc +++ b/docs/management/alerting/connector-management.asciidoc @@ -15,7 +15,7 @@ image::images/connector-listing.png[Example connector listing in the Alerts and [float] ==== Connector list -The *Connectors* tab lists all connectors in the current space. The *search bar* can be used to find specific connectors by name and/or type. +The *Connectors* tab lists all connectors in the current space. The *search bar* can be used to find specific connectors by name and/or type. [role="screenshot"] image::images/connector-filter-by-search.png[Filtering the connector list using the search bar] @@ -26,12 +26,12 @@ The *type* dropdown also lets you filter to a subset of action types. [role="screenshot"] image::images/connector-filter-by-type.png[Filtering the connector list by types of actions] -The *Actions* column indicates the number of actions that reference the connector. This count helps you confirm a connector is unused before you delete it, and tells you how many actions will be affected when a connector is modified. +The *Actions* column indicates the number of actions that reference the connector. This count helps you confirm a connector is unused before you delete it, and tells you how many actions will be affected when a connector is modified. [role="screenshot"] image::images/connector-action-count.png[Filtering the connector list by types of actions] -You can delete individual connectors using the trash icon on the right of each row. Connectors can also be deleted in bulk by multi-selecting them and clicking the *Delete* button to the left of the search box. +You can delete individual connectors using the trash icon. Connectors can also be deleted in bulk by multi-selecting them and clicking the *Delete* button to the left of the search box. [role="screenshot"] image::images/connector-delete.png[Deleting connectors individually or in bulk] @@ -44,4 +44,4 @@ When this happens the action will fail to execute, and appear as errors in the { ==== Creating a new connector -New connectors can be created by clicking the *Create connector* button, which will guide you to select the type of connector and configure it's properties. Refer to <> for the types of connectors available and how to configure them. Once you create a connector it will be made available to you anytime you set up an action in the current space. \ No newline at end of file +New connectors can be created by clicking the *Create connector* button, which will guide you to select the type of connector and configure it's properties. Refer to <> for the types of connectors available and how to configure them. Once you create a connector it will be made available to you anytime you set up an action in the current space. diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index 45f8bd13a5c54..bb16faab7fe5a 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -38,7 +38,6 @@ image:management/index-patterns/images/rollup-index-pattern.png["Menu with rollu Just start typing in the *Index pattern* field, and {kib} looks for the names of {es} indices that match your input. Make sure that the name of the index pattern is unique. -To include system indices in your search, toggle the switch in the upper right. [role="screenshot"] image:management/index-patterns/images/create-index-pattern.png["Create index pattern"] diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index 1a1bcec10ab50..9682d918aabe4 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -25,7 +25,7 @@ the *Index patterns* overview. [role="screenshot"] image::management/index-patterns/images/new-index-pattern.png["Index files and data types"] -Use the icons in the upper right to perform the following actions: +Use the icons to perform the following actions: * [[set-default-pattern]]*Set the default index pattern.* {kib} uses a badge to make users aware of which index pattern is the default. The first pattern diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 6a56970687fd6..da2e190847fdb 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -42,8 +42,8 @@ image::images/management_create_rollup_job.png[][Wizard that walks you through c === Start, stop, and delete rollup jobs Once you’ve saved a rollup job, you’ll see it the *Rollup Jobs* overview page, -where you can drill down for further investigation. The *Manage* menu in -the lower right enables you to start, stop, and delete the rollup job. +where you can drill down for further investigation. The *Manage* menu enables +you to start, stop, and delete the rollup job. You must first stop a rollup job before deleting it. [role="screenshot"] diff --git a/docs/maps/geojson-upload.asciidoc b/docs/maps/geojson-upload.asciidoc index ad20264f56138..7e2cdddfd30ef 100644 --- a/docs/maps/geojson-upload.asciidoc +++ b/docs/maps/geojson-upload.asciidoc @@ -37,7 +37,6 @@ the Elasticsearch responses are shown on the *Layer add panel* and the indexed d appears on the map. The geospatial data on the map should be identical to the locally-previewed data, but now it's indexed data from Elasticsearch. -. To continue adding data to the map, click *Add layer* in the lower -right-hand corner. +. To continue adding data to the map, click *Add layer*. . In *Layer settings*, adjust any settings or <> as needed. . Click *Save & close*. diff --git a/docs/maps/indexing-geojson-data-tutorial.asciidoc b/docs/maps/indexing-geojson-data-tutorial.asciidoc index a94e5757d5dfa..bf846a2b80e03 100644 --- a/docs/maps/indexing-geojson-data-tutorial.asciidoc +++ b/docs/maps/indexing-geojson-data-tutorial.asciidoc @@ -55,14 +55,14 @@ auto-populate *Index type* with either {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape] and *Index name* with ``. -. Click *Import file* in the lower right. +. Click *Import file*. + You'll see activity as the GeoJSON Upload utility creates a new index and index pattern for the data set. When the process is complete, you should receive messages that the creation of the new index and index pattern were successful. -. Click *Add layer* in the bottom right. +. Click *Add layer*. . In *Layer settings*, adjust settings and <> as needed. . Click *Save & close*. diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index b13eeebe56fd8..6495b8a057cf6 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -80,7 +80,7 @@ To symbolize countries by web traffic, you'll need to augment the world country To do this, you'll create a <> to link the vector source *World Countries* to the {es} index `kibana_sample_data_logs` on the shared key iso2 = geo.src. -. Click plus image:maps/images/gs_plus_icon.png[] to the right of *Term Joins* label. +. Click plus image:maps/images/gs_plus_icon.png[] next to the *Term Joins* label. . Click *Join --select--* . Set *Left field* to *ISO 3166-1 alpha-2 code*. . Set *Right source* to *kibana_sample_data_logs*. @@ -238,7 +238,7 @@ The *machine.os.keyword: osx* filter appears in the dashboard query bar. + . Click the *x* to remove the *machine.os.keyword: osx* filter. . In the map, click in the United States vector. -. Click plus image:maps/images/gs_plus_icon.png[] to the right of *iso2* row in the tooltip. +. Click plus image:maps/images/gs_plus_icon.png[] next to the *iso2* row in the tooltip. + Both the visualizations and the map are filtered to only show documents where *geo.src* is *US*. The *geo.src: US* filter appears in the dashboard query bar. @@ -247,4 +247,3 @@ Your dashboard should look like this: + [role="screenshot"] image::maps/images/gs_dashboard_with_terms_filter.png[] - diff --git a/docs/maps/search.asciidoc b/docs/maps/search.asciidoc index 8a93352798d2c..a461ab6fbb3a6 100644 --- a/docs/maps/search.asciidoc +++ b/docs/maps/search.asciidoc @@ -4,7 +4,7 @@ **Elastic Maps** embeds the search bar for real-time search. Only layers requesting data from {es} are filtered when you submit a search request. -Layers narrowed by the search context contain the filter icon image:maps/images/filter_icon.png[] to the right of layer name in the legend. +Layers narrowed by the search context contain the filter icon image:maps/images/filter_icon.png[] next to the layer name in the legend. You can create a layer that requests data from {es} from the following: diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 89c4c88708d58..f05afac34e595 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -2,7 +2,7 @@ [[defining-alerts]] == Defining alerts -{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. +{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. [float] === Alert flyout @@ -25,20 +25,20 @@ All alert share the following four properties in common: image::images/alert-flyout-general-details.png[All alerts have name, tags, check every, and re-notify every properties in common] Name:: The name of the alert. While this name does not have to be unique, the name can be referenced in actions and also appears in the searchable alert listing in the management UI. A distinctive name can help identify and find an alert. -Tags:: A list of tag names that can be applied to an alert. Tags can help you organize and find alerts, because tags appear in the alert listing in the management UI which is searchable by tag. +Tags:: A list of tag names that can be applied to an alert. Tags can help you organize and find alerts, because tags appear in the alert listing in the management UI which is searchable by tag. Check every:: This value determines how frequently the alert conditions below are checked. Note that the timing of background alert checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. -Re-notify every:: This value limits how often actions are repeated when an alert instance remains active across alert checks. See <> for more information. +Re-notify every:: This value limits how often actions are repeated when an alert instance remains active across alert checks. See <> for more information. [float] [[defining-alerts-type-conditions]] === Alert type and conditions -Depending upon the {kib} app and context, you may be prompted to choose the type of alert you wish to create. Some apps will pre-select the type of alert for you. +Depending upon the {kib} app and context, you may be prompted to choose the type of alert you wish to create. Some apps will pre-select the type of alert for you. [role="screenshot"] image::images/alert-flyout-alert-type-selection.png[Choosing the type of alert to create] -Each alert type provides its own way of defining the conditions to detect, but an expression formed by a series of clauses is a common pattern. Each clause has a UI control that allows you to define the clause. For example, in an index threshold alert the `WHEN` clause allows you to select an aggregation operation to apply to a numeric field. +Each alert type provides its own way of defining the conditions to detect, but an expression formed by a series of clauses is a common pattern. Each clause has a UI control that allows you to define the clause. For example, in an index threshold alert the `WHEN` clause allows you to select an aggregation operation to apply to a numeric field. [role="screenshot"] image::images/alert-flyout-alert-conditions.png[UI for defining alert conditions on an index threshold alert] @@ -52,19 +52,19 @@ To add an action to an alert, you first select the type of action: [role="screenshot"] image::images/alert-flyout-action-type-selection.png[UI for selecting an action type] -Each action must specify a <> instance. If no connectors exist for that action type, click "Add new" to create one. +Each action must specify a <> instance. If no connectors exist for that action type, click "Add new" to create one. -Each action type exposes different properties. For example an email action allows you to set the recipients, the subject, and a message body in markdown format. See <> for details on the types of actions provided by {kib} and their properties. +Each action type exposes different properties. For example an email action allows you to set the recipients, the subject, and a message body in markdown format. See <> for details on the types of actions provided by {kib} and their properties. [role="screenshot"] image::images/alert-flyout-action-details.png[UI for defining an email action] -Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass alert values at the time a condition is detected to an action. Available variables differ by alert type, and a list can be accessed using the "add variable" button at the right of the text box. +Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass alert values at the time a condition is detected to an action. Available variables differ by alert type, and a list can be accessed using the "add variable" button. [role="screenshot"] image::images/alert-flyout-action-variables.png[Passing alert values to an action] -You can attach more than one action. Clicking the "Add action" button will prompt you to select another alert type and repeat the above steps again. +You can attach more than one action. Clicking the "Add action" button will prompt you to select another alert type and repeat the above steps again. [role="screenshot"] image::images/alert-flyout-add-action.png[You can add multiple actions on an alert] @@ -77,4 +77,4 @@ Actions are not required on alerts. In some cases you may want to run an alert w [float] === Managing alerts -To modify an alert after it was created, including muting or disabling it, use the <>. \ No newline at end of file +To modify an alert after it was created, including muting or disabling it, use the <>. diff --git a/docs/user/dashboard.asciidoc b/docs/user/dashboard.asciidoc index a17e46c5b3542..ab529a533d5e3 100644 --- a/docs/user/dashboard.asciidoc +++ b/docs/user/dashboard.asciidoc @@ -90,7 +90,7 @@ In *Edit* mode, you can move, resize, customize, and delete panels to suit your * To move a panel, click and hold the panel header and drag to the new location. [[resizing-containers]] -* To resize a panel, click the resize control on the lower right and drag +* To resize a panel, click the resize control and drag to the new dimensions. * To toggle the use of margins and panel titles, use the *Options* menu. diff --git a/docs/user/discover.asciidoc b/docs/user/discover.asciidoc index 4222ba40debb7..2547b38a22616 100644 --- a/docs/user/discover.asciidoc +++ b/docs/user/discover.asciidoc @@ -33,7 +33,7 @@ which has a pre-built index pattern. By default, *Discover* shows data for the last 15 minutes. If you have a time-based index, and no data displays, -you might need to increase the time range. Using the <> in the upper right, +you might need to increase the time range. Using the <>, you can specify a common or recently-used time range, a relative time from now, or an absolute time range. diff --git a/docs/user/graph/getting-started.asciidoc b/docs/user/graph/getting-started.asciidoc index 1749678ace9e3..a155017f1bb22 100644 --- a/docs/user/graph/getting-started.asciidoc +++ b/docs/user/graph/getting-started.asciidoc @@ -38,7 +38,7 @@ image::user/graph/images/graph-url-connections.png["URL connections"] [role="screenshot"] image::user/graph/images/graph-link-summary.png["Link summary"] -. Use the control bar on the right to explore +. Use the control bar to explore additional connections: + * To display additional vertices that connect to your graph, click the expand icon @@ -70,7 +70,7 @@ select *Edit settings*. To change the color and label of selected vertices, click the style icon image:user/graph/images/graph-style-button.png[Style] -in the control bar on the right. +in the control bar. [float] diff --git a/docs/user/monitoring/beats-details.asciidoc b/docs/user/monitoring/beats-details.asciidoc index 0b2be4dd9e3d9..f4ecb2a74d91e 100644 --- a/docs/user/monitoring/beats-details.asciidoc +++ b/docs/user/monitoring/beats-details.asciidoc @@ -14,8 +14,8 @@ image::user/monitoring/images/monitoring-beats.jpg["Monitoring Beats",link="imag To view an overview of the Beats data in the cluster, click *Overview*. The overview page has a section for activity in the last day, which is a real-time sample of data. The summary bar and charts follow the typical paradigm -of data in the Monitoring UI, which is bound to the span of the time filter in -the top right corner of the page. This overview page can therefore show +of data in the Monitoring UI, which is bound to the span of the time filter. +This overview page can therefore show up-to-date or historical information. To view a listing of the individual Beat instances in the cluster, click *Beats*. diff --git a/docs/visualize/lens.asciidoc b/docs/visualize/lens.asciidoc index 35570ea7ca1dc..b181763c0d0d0 100644 --- a/docs/visualize/lens.asciidoc +++ b/docs/visualize/lens.asciidoc @@ -38,7 +38,7 @@ you'll see two places highlighted in green: * The visualization builder pane -* The *X-axis* or *Y-axis* fields in the right column +* The *X-axis* or *Y-axis* fields You can incorporate many fields into your visualization, and Lens uses heuristics to decide how to apply each one to the visualization. @@ -89,8 +89,8 @@ You can switch between suggestions without losing your previous state: [role="screenshot"] image::images/lens_suggestions.gif[] -If you want to switch to a chart type that is not suggested, click the chart type in the -top right, then select a chart type. When there is an exclamation point (!) +If you want to switch to a chart type that is not suggested, click the chart type, +then select a chart type. When there is an exclamation point (!) next to a chart type, Lens is unable to transfer your current data, but still allows you to make the change. @@ -106,7 +106,7 @@ If there is a match, Lens displays the new data. All fields that do not match th . Change the data field options, such as the aggregation or label. -.. Click *Drop a field here* or the field name in the right column. +.. Click *Drop a field here* or the field name in the column. .. Change the options that appear depending on the type of field. @@ -168,7 +168,7 @@ image::images/lens_tutorial_2.png[Lens tutorial] Customize your visualization to look exactly how you want. -. In the right column, click *Average of taxful_total_price*. +. Click *Average of taxful_total_price*. .. Change the *Label* to `Sales`, or a name that you prefer for the data. @@ -180,7 +180,7 @@ six available categories. . Look at the suggestions. None of them show an area chart, but for sales data, a stacked area chart might make sense. To switch the chart type: -.. Click *Stacked bar chart* in the right column. +.. Click *Stacked bar chart* in the column. .. Click *Stacked area*. + From bc3f38288337220fba91c32de3ed5a14e9c219cc Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 9 Apr 2020 10:47:42 -0700 Subject: [PATCH 65/81] skip flaky suite (#62927) --- x-pack/test/functional/apps/canvas/custom_elements.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/canvas/custom_elements.ts b/x-pack/test/functional/apps/canvas/custom_elements.ts index 6c34556840960..de3976509be1f 100644 --- a/x-pack/test/functional/apps/canvas/custom_elements.ts +++ b/x-pack/test/functional/apps/canvas/custom_elements.ts @@ -19,7 +19,8 @@ export default function canvasCustomElementTest({ const PageObjects = getPageObjects(['canvas', 'common']); const find = getService('find'); - describe('custom elements', function() { + // FLAKY: https://github.com/elastic/kibana/issues/62927 + describe.skip('custom elements', function() { this.tags('skipFirefox'); before(async () => { From 2574d0f8055981e4a44416dcfea2a9b9b64c5d9b Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Thu, 9 Apr 2020 11:01:25 -0700 Subject: [PATCH 66/81] Adds a new config flag to encode with BOM for our CSVs (#63006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds a new config flag to encode with BOM for our CSVs * Push out bom-chars to it's own constant * Getting those snapshots back into shape 💪 Co-authored-by: Elastic Machine --- .../__snapshots__/index.test.js.snap | 4 ++ .../plugins/reporting/common/constants.ts | 1 + x-pack/legacy/plugins/reporting/config.ts | 1 + .../csv/server/execute_job.test.js | 45 +++++++++++++++++++ .../export_types/csv/server/execute_job.ts | 6 ++- .../plugins/reporting/server/config/index.ts | 1 + 6 files changed, 56 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/reporting/__snapshots__/index.test.js.snap b/x-pack/legacy/plugins/reporting/__snapshots__/index.test.js.snap index 757677f1d4f82..3ae3079da136b 100644 --- a/x-pack/legacy/plugins/reporting/__snapshots__/index.test.js.snap +++ b/x-pack/legacy/plugins/reporting/__snapshots__/index.test.js.snap @@ -66,6 +66,7 @@ Object { "duration": "30s", "size": 500, }, + "useByteOrderMarkEncoding": false, }, "enabled": true, "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -162,6 +163,7 @@ Object { "duration": "30s", "size": 500, }, + "useByteOrderMarkEncoding": false, }, "enabled": true, "index": ".reporting", @@ -257,6 +259,7 @@ Object { "duration": "30s", "size": 500, }, + "useByteOrderMarkEncoding": false, }, "enabled": true, "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -353,6 +356,7 @@ Object { "duration": "30s", "size": 500, }, + "useByteOrderMarkEncoding": false, }, "enabled": true, "index": ".reporting", diff --git a/x-pack/legacy/plugins/reporting/common/constants.ts b/x-pack/legacy/plugins/reporting/common/constants.ts index 8f7a06ba9f8e9..e3d6a4274e7df 100644 --- a/x-pack/legacy/plugins/reporting/common/constants.ts +++ b/x-pack/legacy/plugins/reporting/common/constants.ts @@ -19,6 +19,7 @@ export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv export const CONTENT_TYPE_CSV = 'text/csv'; export const CSV_REPORTING_ACTION = 'downloadCsvReport'; +export const CSV_BOM_CHARS = '\ufeff'; export const WHITELISTED_JOB_CONTENT_TYPES = [ 'application/json', diff --git a/x-pack/legacy/plugins/reporting/config.ts b/x-pack/legacy/plugins/reporting/config.ts index 211fa70301bbf..5eceb84c83e43 100644 --- a/x-pack/legacy/plugins/reporting/config.ts +++ b/x-pack/legacy/plugins/reporting/config.ts @@ -135,6 +135,7 @@ export async function config(Joi: any) { .default(), }).default(), csv: Joi.object({ + useByteOrderMarkEncoding: Joi.boolean().default(false), checkForFormulas: Joi.boolean().default(true), enablePanelActionDownload: Joi.boolean().default(true), maxSizeBytes: Joi.number() diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js index 93dbe598b367c..4870e1e35cdaf 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js @@ -13,6 +13,7 @@ import { createMockReportingCore } from '../../../test_helpers'; import { LevelLogger } from '../../../server/lib/level_logger'; import { setFieldFormats } from '../../../server/services'; import { executeJobFactory } from './execute_job'; +import { CSV_BOM_CHARS } from '../../../common/constants'; const delay = ms => new Promise(resolve => setTimeout(() => resolve(), ms)); @@ -374,6 +375,50 @@ describe('CSV Execute Job', function() { }); }); + describe('Byte order mark encoding', () => { + it('encodes CSVs with BOM', async () => { + configGetStub.withArgs('csv', 'useByteOrderMarkEncoding').returns(true); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: 'one', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = { + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }; + const { content } = await executeJob('job123', jobParams, cancellationToken); + + expect(content).toEqual(`${CSV_BOM_CHARS}one,two\none,bar\n`); + }); + + it('encodes CSVs without BOM', async () => { + configGetStub.withArgs('csv', 'useByteOrderMarkEncoding').returns(false); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: 'one', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = { + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }; + const { content } = await executeJob('job123', jobParams, cancellationToken); + + expect(content).toEqual('one,two\none,bar\n'); + }); + }); + describe('Elasticsearch call errors', function() { it('should reject Promise if search call errors out', async function() { callAsCurrentUserStub.rejects(new Error()); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index 3a282eb0b2974..376a398da274f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import Hapi from 'hapi'; import { IUiSettingsClient, KibanaRequest } from '../../../../../../../src/core/server'; -import { CSV_JOB_TYPE } from '../../../common/constants'; +import { CSV_JOB_TYPE, CSV_BOM_CHARS } from '../../../common/constants'; import { ReportingCore } from '../../../server/core'; import { cryptoFactory } from '../../../server/lib'; import { getFieldFormats } from '../../../server/services'; @@ -121,6 +121,8 @@ export const executeJobFactory: ExecuteJobFactory Date: Thu, 9 Apr 2020 21:26:13 +0300 Subject: [PATCH 67/81] [SIEM][CASE] Test configuration API and hooks (#62803) * Test API * Test useConnectors * Test useConfigure * Fixes --- .../case/configure/__mocks__/api.ts | 31 ++ .../containers/case/configure/api.test.ts | 115 +++++++ .../public/containers/case/configure/mock.ts | 99 ++++++ .../case/configure/use_configure.test.tsx | 299 ++++++++++++++++++ .../case/configure/use_configure.tsx | 2 +- .../case/configure/use_connectors.test.tsx | 95 ++++++ 6 files changed, 640 insertions(+), 1 deletion(-) create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/configure/__mocks__/api.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/__mocks__/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/__mocks__/api.ts new file mode 100644 index 0000000000000..03f7d241e5dff --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/__mocks__/api.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CasesConfigurePatch, + CasesConfigureRequest, + Connector, +} from '../../../../../../../../plugins/case/common/api'; + +import { ApiProps } from '../../types'; +import { CaseConfigure } from '../types'; +import { connectorsMock, caseConfigurationCamelCaseResponseMock } from '../mock'; + +export const fetchConnectors = async ({ signal }: ApiProps): Promise => + Promise.resolve(connectorsMock); + +export const getCaseConfigure = async ({ signal }: ApiProps): Promise => + Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const postCaseConfigure = async ( + caseConfiguration: CasesConfigureRequest, + signal: AbortSignal +): Promise => Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const patchCaseConfigure = async ( + caseConfiguration: CasesConfigurePatch, + signal: AbortSignal +): Promise => Promise.resolve(caseConfigurationCamelCaseResponseMock); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts new file mode 100644 index 0000000000000..ef0e51fb1c24d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaServices } from '../../../lib/kibana'; +import { fetchConnectors, getCaseConfigure, postCaseConfigure, patchCaseConfigure } from './api'; +import { + connectorsMock, + caseConfigurationMock, + caseConfigurationResposeMock, + caseConfigurationCamelCaseResponseMock, +} from './mock'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../../../lib/kibana'); + +const fetchMock = jest.fn(); +mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + +describe('Case Configuration API', () => { + describe('fetch connectors', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(connectorsMock); + }); + + test('check url, method, signal', async () => { + await fetchConnectors({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure/connectors/_find', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await fetchConnectors({ signal: abortCtrl.signal }); + expect(resp).toEqual(connectorsMock); + }); + }); + + describe('fetch configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, method, signal', async () => { + await getCaseConfigure({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCaseConfigure({ signal: abortCtrl.signal }); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + + test('return null on empty response', async () => { + fetchMock.mockResolvedValue({}); + const resp = await getCaseConfigure({ signal: abortCtrl.signal }); + expect(resp).toBe(null); + }); + }); + + describe('create configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, body, method, signal', async () => { + await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + body: + '{"connector_id":"123","connector_name":"My Connector","closure_type":"close-by-user"}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + }); + + describe('update configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, body, method, signal', async () => { + await patchCaseConfigure({ connector_id: '456', version: 'WzHJ12' }, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + body: '{"connector_id":"456","version":"WzHJ12"}', + method: 'PATCH', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchCaseConfigure( + { connector_id: '456', version: 'WzHJ12' }, + abortCtrl.signal + ); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts new file mode 100644 index 0000000000000..d2491b39fdf56 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts @@ -0,0 +1,99 @@ +/* + * 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 { + Connector, + CasesConfigureResponse, + CasesConfigureRequest, +} from '../../../../../../../plugins/case/common/api'; +import { CaseConfigure } from './types'; + +export const connectorsMock: Connector[] = [ + { + id: '123', + actionTypeId: '.servicenow', + name: 'My Connector', + config: { + apiUrl: 'https://instance1.service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'append', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + isPreconfigured: true, + }, + { + id: '456', + actionTypeId: '.servicenow', + name: 'My Connector 2', + config: { + apiUrl: 'https://instance2.service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + isPreconfigured: true, + }, +]; + +export const caseConfigurationResposeMock: CasesConfigureResponse = { + created_at: '2020-04-06T13:03:18.657Z', + created_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + connector_id: '123', + connector_name: 'My Connector', + closure_type: 'close-by-user', + updated_at: '2020-04-06T14:03:18.657Z', + updated_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + version: 'WzHJ12', +}; + +export const caseConfigurationMock: CasesConfigureRequest = { + connector_id: '123', + connector_name: 'My Connector', + closure_type: 'close-by-user', +}; + +export const caseConfigurationCamelCaseResponseMock: CaseConfigure = { + createdAt: '2020-04-06T13:03:18.657Z', + createdBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, + connectorId: '123', + connectorName: 'My Connector', + closureType: 'close-by-user', + updatedAt: '2020-04-06T14:03:18.657Z', + updatedBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, + version: 'WzHJ12', +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx new file mode 100644 index 0000000000000..3ee16e19eaf9f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx @@ -0,0 +1,299 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useCaseConfigure, ReturnUseCaseConfigure, PersistCaseConfigure } from './use_configure'; +import { caseConfigurationCamelCaseResponseMock } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +const configuration: PersistCaseConfigure = { + connectorId: '456', + connectorName: 'My Connector 2', + closureType: 'close-by-pushing', +}; + +describe('useConfigure', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + const args = { + setConnector: jest.fn(), + setClosureType: jest.fn(), + setCurrentConfiguration: jest.fn(), + }; + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: true, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('fetch case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('fetch case configuration - setConnector', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(args.setConnector).toHaveBeenCalledWith('123', 'My Connector'); + }); + }); + + test('fetch case configuration - setClosureType', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(args.setClosureType).toHaveBeenCalledWith('close-by-user'); + }); + }); + + test('fetch case configuration - setCurrentConfiguration', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(args.setCurrentConfiguration).toHaveBeenCalledWith({ + connectorId: '123', + closureType: 'close-by-user', + }); + }); + }); + + test('fetch case configuration - only setConnector', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure({ setConnector: jest.fn() }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('refetch case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCaseConfigure(); + expect(spyOnGetCaseConfigure).toHaveBeenCalledTimes(2); + }); + }); + + test('set isLoading to true when fetching case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCaseConfigure(); + + expect(result.current.loading).toBe(true); + }); + }); + + test('persist case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + expect(result.current).toEqual({ + loading: false, + persistLoading: true, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('save case configuration - postCaseConfigure', async () => { + // When there is no version, a configuration is created. Otherwise is updated. + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + version: '', + }) + ); + + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + await waitForNextUpdate(); + + expect(args.setConnector).toHaveBeenNthCalledWith(2, '456'); + expect(args.setClosureType).toHaveBeenNthCalledWith(2, 'close-by-pushing'); + expect(args.setCurrentConfiguration).toHaveBeenNthCalledWith(2, { + connectorId: '456', + closureType: 'close-by-pushing', + }); + }); + }); + + test('save case configuration - patchCaseConfigure', async () => { + const spyOnPatchCaseConfigure = jest.spyOn(api, 'patchCaseConfigure'); + spyOnPatchCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + await waitForNextUpdate(); + + expect(args.setConnector).toHaveBeenNthCalledWith(2, '456'); + expect(args.setClosureType).toHaveBeenNthCalledWith(2, 'close-by-pushing'); + expect(args.setCurrentConfiguration).toHaveBeenNthCalledWith(2, { + connectorId: '456', + closureType: 'close-by-pushing', + }); + }); + }); + + test('save case configuration - only setConnector', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure({ setConnector: jest.fn() }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('unhappy path - fetch case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('unhappy path - persist case configuration', async () => { + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx index 19d80bba1e0f8..7f57149d4e56d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx @@ -12,7 +12,7 @@ import * as i18n from './translations'; import { ClosureType } from './types'; import { CurrentConfiguration } from '../../../pages/case/components/configure_cases/reducer'; -interface PersistCaseConfigure { +export interface PersistCaseConfigure { connectorId: string; connectorName: string; closureType: ClosureType; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx new file mode 100644 index 0000000000000..0d6b6acfd9065 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx @@ -0,0 +1,95 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useConnectors, ReturnConnectors } from './use_connectors'; +import { connectorsMock } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useConnectors', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useConnectors() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: true, + connectors: [], + refetchConnectors: result.current.refetchConnectors, + }); + }); + }); + + test('fetch connectors', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + connectors: connectorsMock, + refetchConnectors: result.current.refetchConnectors, + }); + }); + }); + + test('refetch connectors', async () => { + const spyOnfetchConnectors = jest.spyOn(api, 'fetchConnectors'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchConnectors(); + expect(spyOnfetchConnectors).toHaveBeenCalledTimes(2); + }); + }); + + test('set isLoading to true when refetching connectors', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchConnectors(); + + expect(result.current.loading).toBe(true); + }); + }); + + test('unhappy path', async () => { + const spyOnfetchConnectors = jest.spyOn(api, 'fetchConnectors'); + spyOnfetchConnectors.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + connectors: [], + refetchConnectors: result.current.refetchConnectors, + }); + }); + }); +}); From 9fd63a7daad6f3f13585d2d6397402654c278914 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 9 Apr 2020 11:31:19 -0700 Subject: [PATCH 68/81] [DOCS] Adds docs for Painless Lab (#62997) * [DOCS] Adds docs for Painless Lab * [DOCS] Incorporated review comments --- .../painlesslab/images/painless-lab.png | Bin 0 -> 220876 bytes docs/dev-tools/painlesslab/index.asciidoc | 17 +++++++++++ docs/user/dev-tools.asciidoc | 28 ++++++++++++++---- 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 docs/dev-tools/painlesslab/images/painless-lab.png create mode 100644 docs/dev-tools/painlesslab/index.asciidoc diff --git a/docs/dev-tools/painlesslab/images/painless-lab.png b/docs/dev-tools/painlesslab/images/painless-lab.png new file mode 100644 index 0000000000000000000000000000000000000000..f65257852792e03eae3d2e111da11d81f6df630f GIT binary patch literal 220876 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%0DJ;r1F_I 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

}xIV{+K-l~dNlQoghqoZX| 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, +process {ref}/docs-reindex.html[reindexed data], define complex +<>, +and work with data in other contexts. + +To get started, go to *Dev Tools > Painless Lab*. + +image::dev-tools/painlesslab/images/painless-lab.png[Painless Lab] diff --git a/docs/user/dev-tools.asciidoc b/docs/user/dev-tools.asciidoc index 77a781fd069e4..0ee7fbc741e00 100644 --- a/docs/user/dev-tools.asciidoc +++ b/docs/user/dev-tools.asciidoc @@ -4,13 +4,29 @@ [partintro] -- -The *Dev Tools* page contains development tools that you can use to interact -with your data in Kibana. +*Dev Tools* contains tools that you can use to interact +with your data. -* <> -* <> -* <> +[cols="2"] +|=== +a| <> + +| Interact with the REST API of Elasticsearch, including sending requests +and viewing API documentation. + +a| <> + +| Inspect and analyze your search queries. + +a| <> + +| Build and debug grok patterns before you use them in your data processing pipelines. + +a| <> + +| beta:[] Test and debug Painless scripts in real-time. +|=== -- @@ -19,3 +35,5 @@ include::{kib-repo-dir}/dev-tools/console/console.asciidoc[] include::{kib-repo-dir}/dev-tools/searchprofiler/index.asciidoc[] include::{kib-repo-dir}/dev-tools/grokdebugger/index.asciidoc[] + +include::{kib-repo-dir}/dev-tools/painlesslab/index.asciidoc[] From 087df824523d007409c68e7d3ba32d824cce07ee Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 9 Apr 2020 11:48:56 -0700 Subject: [PATCH 69/81] [TSVB] Add Positive Rate to Aggregations (#59843) * [TSVB] Add Rate to Aggregations * Fixing i18n labels * Changing from rate to growth_rate; adding message to aggregation form; * Change units to scale; change free text to combobox * Fixing placeholder * Fixing i18n label * Changing from Growth Rate to Positive Rate Co-authored-by: Elastic Machine --- .../public/components/aggs/agg_select.js | 6 + .../public/components/aggs/positive_rate.js | 191 ++++++++++++++++++ .../public/components/lib/agg_to_component.js | 2 + .../vis_type_timeseries/common/agg_lookup.js | 3 + .../common/calculate_label.js | 6 + .../request_processors/series/index.js | 2 + .../series/positive_rate.js | 68 +++++++ .../series/positive_rate.test.js | 95 +++++++++ .../request_processors/table/index.js | 2 + .../request_processors/table/positive_rate.js | 35 ++++ 10 files changed, 410 insertions(+) create mode 100644 src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_rate.js create mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js create mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js create mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js index f93dee14d0eed..8607ff184dfaa 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js @@ -49,6 +49,12 @@ const metricAggs = [ }), value: 'filter_ratio', }, + { + label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.positiveRateLabel', { + defaultMessage: 'Positive Rate', + }), + value: 'positive_rate', + }, { label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.maxLabel', { defaultMessage: 'Max', diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_rate.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_rate.js new file mode 100644 index 0000000000000..39558fa3a9224 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_rate.js @@ -0,0 +1,191 @@ +/* + * 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 PropTypes from 'prop-types'; +import React from 'react'; +import { AggSelect } from './agg_select'; +import { FieldSelect } from './field_select'; +import { AggRow } from './agg_row'; +import { createChangeHandler } from '../lib/create_change_handler'; +import { createSelectHandler } from '../lib/create_select_handler'; +import { + htmlIdGenerator, + EuiFlexGroup, + EuiFlexItem, + EuiFormLabel, + EuiFormRow, + EuiSpacer, + EuiText, + EuiLink, + EuiComboBox, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; + +const UNIT_OPTIONS = [ + { + label: i18n.translate('visTypeTimeseries.units.auto', { defaultMessage: 'auto' }), + value: '', + }, + { + label: i18n.translate('visTypeTimeseries.units.perMillisecond', { + defaultMessage: 'per millisecond', + }), + value: '1ms', + }, + { + label: i18n.translate('visTypeTimeseries.units.perSecond', { defaultMessage: 'per second' }), + value: '1s', + }, + { + label: i18n.translate('visTypeTimeseries.units.perMinute', { defaultMessage: 'per minute' }), + value: '1m', + }, + { + label: i18n.translate('visTypeTimeseries.units.perHour', { defaultMessage: 'per hour' }), + value: '1h', + }, + { + label: i18n.translate('visTypeTimeseries.units.perDay', { defaultMessage: 'per day' }), + value: '1d', + }, +]; + +export const PositiveRateAgg = props => { + const defaults = { unit: '' }; + const model = { ...defaults, ...props.model }; + + const handleChange = createChangeHandler(props.onChange, model); + const handleSelectChange = createSelectHandler(handleChange); + + const htmlId = htmlIdGenerator(); + const indexPattern = + (props.series.override_index_pattern && props.series.series_index_pattern) || + props.panel.index_pattern; + + const selectedUnitOptions = UNIT_OPTIONS.filter(o => o.value === model.unit); + + return ( + + + + + + + + + + + + } + fullWidth + > + + + + + + } + fullWidth + > + + + + + + +

+ + + + ), + }} + /> +

+ + + ); +}; + +PositiveRateAgg.propTypes = { + disableDelete: PropTypes.bool, + fields: PropTypes.object, + model: PropTypes.object, + onAdd: PropTypes.func, + onChange: PropTypes.func, + onDelete: PropTypes.func, + panel: PropTypes.object, + series: PropTypes.object, + siblings: PropTypes.array, +}; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js index ca40d60f20848..a53192afafdcc 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js @@ -33,6 +33,7 @@ import { PercentileRankAgg } from '../aggs/percentile_rank'; import { Static } from '../aggs/static'; import { MathAgg } from '../aggs/math'; import { TopHitAgg } from '../aggs/top_hit'; +import { PositiveRateAgg } from '../aggs/positive_rate'; export const aggToComponent = { count: StandardAgg, @@ -65,4 +66,5 @@ export const aggToComponent = { static: Static, math: MathAgg, top_hit: TopHitAgg, + positive_rate: PositiveRateAgg, }; diff --git a/src/plugins/vis_type_timeseries/common/agg_lookup.js b/src/plugins/vis_type_timeseries/common/agg_lookup.js index 4dfdc83dcfabb..432da03e3d45d 100644 --- a/src/plugins/vis_type_timeseries/common/agg_lookup.js +++ b/src/plugins/vis_type_timeseries/common/agg_lookup.js @@ -97,6 +97,9 @@ export const lookup = { defaultMessage: 'Static Value', }), top_hit: i18n.translate('visTypeTimeseries.aggLookup.topHitLabel', { defaultMessage: 'Top Hit' }), + positive_rate: i18n.translate('visTypeTimeseries.aggLookup.positiveRateLabel', { + defaultMessage: 'Positive Rate', + }), }; const pipeline = [ diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.js b/src/plugins/vis_type_timeseries/common/calculate_label.js index 756d6e57a83e8..71aa0aed7dc11 100644 --- a/src/plugins/vis_type_timeseries/common/calculate_label.js +++ b/src/plugins/vis_type_timeseries/common/calculate_label.js @@ -70,6 +70,12 @@ export function calculateLabel(metric, metrics) { defaultMessage: 'Filter Ratio', }); } + if (metric.type === 'positive_rate') { + return i18n.translate('visTypeTimeseries.calculateLabel.positiveRateLabel', { + defaultMessage: 'Positive Rate of {field}', + values: { field: metric.field }, + }); + } if (metric.type === 'static') { return i18n.translate('visTypeTimeseries.calculateLabel.staticValueLabel', { defaultMessage: 'Static Value of {metricValue}', diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js index 4b0b8f33716a2..c727a3131f5df 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js @@ -26,6 +26,7 @@ import { dateHistogram } from './date_histogram'; import { metricBuckets } from './metric_buckets'; import { siblingBuckets } from './sibling_buckets'; import { ratios as filterRatios } from './filter_ratios'; +import { positiveRate } from './positive_rate'; import { normalizeQuery } from './normalize_query'; export const processors = [ @@ -38,5 +39,6 @@ export const processors = [ metricBuckets, siblingBuckets, filterRatios, + positiveRate, normalizeQuery, ]; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js new file mode 100644 index 0000000000000..1ff548cc19e02 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js @@ -0,0 +1,68 @@ +/* + * 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 { getBucketSize } from '../../helpers/get_bucket_size'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; +import { bucketTransform } from '../../helpers/bucket_transform'; +import { set } from 'lodash'; + +export const filter = metric => metric.type === 'positive_rate'; + +export const createPositiveRate = (doc, intervalString, aggRoot) => metric => { + const maxFn = bucketTransform.max; + const derivativeFn = bucketTransform.derivative; + const positiveOnlyFn = bucketTransform.positive_only; + + const maxMetric = { id: `${metric.id}-positive-rate-max`, type: 'max', field: metric.field }; + const derivativeMetric = { + id: `${metric.id}-positive-rate-derivative`, + type: 'derivative', + field: `${metric.id}-positive-rate-max`, + unit: metric.unit, + }; + const positiveOnlyMetric = { + id: metric.id, + type: 'positive_only', + field: `${metric.id}-positive-rate-derivative`, + }; + + const fakeSeriesMetrics = [maxMetric, derivativeMetric, positiveOnlyMetric]; + + const maxBucket = maxFn(maxMetric, fakeSeriesMetrics, intervalString); + const derivativeBucket = derivativeFn(derivativeMetric, fakeSeriesMetrics, intervalString); + const positiveOnlyBucket = positiveOnlyFn(positiveOnlyMetric, fakeSeriesMetrics, intervalString); + + set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-positive-rate-max`, maxBucket); + set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-positive-rate-derivative`, derivativeBucket); + set(doc, `${aggRoot}.timeseries.aggs.${metric.id}`, positiveOnlyBucket); +}; + +export function positiveRate(req, panel, series, esQueryConfig, indexPatternObject, capabilities) { + return next => doc => { + const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject); + const { intervalString } = getBucketSize(req, interval, capabilities); + if (series.metrics.some(filter)) { + series.metrics + .filter(filter) + .forEach(createPositiveRate(doc, intervalString, `aggs.${series.id}.aggs`)); + return next(doc); + } + return next(doc); + }; +} diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js new file mode 100644 index 0000000000000..946884c05c722 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { positiveRate } from './positive_rate'; +describe('positiveRate(req, panel, series)', () => { + let panel; + let series; + let req; + beforeEach(() => { + panel = { + time_field: 'timestamp', + }; + series = { + id: 'test', + split_mode: 'terms', + terms_size: 10, + terms_field: 'host', + metrics: [ + { + id: 'metric-1', + type: 'positive_rate', + field: 'system.network.out.bytes', + unit: '1s', + }, + ], + }; + req = { + payload: { + timerange: { + min: '2017-01-01T00:00:00Z', + max: '2017-01-01T01:00:00Z', + }, + }, + }; + }); + + test('calls next when finished', () => { + const next = jest.fn(); + positiveRate(req, panel, series)(next)({}); + expect(next.mock.calls.length).toEqual(1); + }); + + test('returns positive rate aggs', () => { + const next = doc => doc; + const doc = positiveRate(req, panel, series)(next)({}); + expect(doc).toEqual({ + aggs: { + test: { + aggs: { + timeseries: { + aggs: { + 'metric-1-positive-rate-max': { + max: { field: 'system.network.out.bytes' }, + }, + 'metric-1-positive-rate-derivative': { + derivative: { + buckets_path: 'metric-1-positive-rate-max', + gap_policy: 'skip', + unit: '1s', + }, + }, + 'metric-1': { + bucket_script: { + buckets_path: { value: 'metric-1-positive-rate-derivative[normalized_value]' }, + script: { + source: 'params.value > 0.0 ? params.value : 0.0', + lang: 'painless', + }, + gap_policy: 'skip', + }, + }, + }, + }, + }, + }, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js index a62533ae7a37c..5864d2538005d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js @@ -26,6 +26,7 @@ import { metricBuckets } from './metric_buckets'; import { siblingBuckets } from './sibling_buckets'; import { ratios as filterRatios } from './filter_ratios'; import { normalizeQuery } from './normalize_query'; +import { positiveRate } from './positive_rate'; export const processors = [ query, @@ -36,5 +37,6 @@ export const processors = [ metricBuckets, siblingBuckets, filterRatios, + positiveRate, normalizeQuery, ]; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js new file mode 100644 index 0000000000000..da4b834822d70 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js @@ -0,0 +1,35 @@ +/* + * 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 { getBucketSize } from '../../helpers/get_bucket_size'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; +import { calculateAggRoot } from './calculate_agg_root'; +import { createPositiveRate, filter } from '../series/positive_rate'; + +export function positiveRate(req, panel, esQueryConfig, indexPatternObject) { + return next => doc => { + const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); + const { intervalString } = getBucketSize(req, interval); + panel.series.forEach(column => { + const aggRoot = calculateAggRoot(doc, column); + column.metrics.filter(filter).forEach(createPositiveRate(doc, intervalString, aggRoot)); + }); + return next(doc); + }; +} From b8738b0eebf7a7ad708b5f498dcc51f6a7fb2c87 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 9 Apr 2020 15:05:09 -0400 Subject: [PATCH 70/81] Ensure that discover data exists for home/_navigation test so that the test suite can run in isolation (#62516) --- test/functional/apps/home/_navigation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index efc0dad394464..2c927e9a2f4c7 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -52,6 +52,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { describe('Kibana browser back navigation should work', function describeIndexTests() { before(async () => { + await esArchiver.loadIfNeeded('discover'); await esArchiver.loadIfNeeded('logstash_functional'); if (browser.isInternetExplorer) { await kibanaServer.uiSettings.replace({ 'state:storeInSessionStorage': false }); From e61680571acce5b6bb8be8d9f6e574864c49f290 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 9 Apr 2020 15:05:21 -0400 Subject: [PATCH 71/81] Ensure alerting action exists in connectors test setup (#62511) --- .../apps/triggers_actions_ui/connectors.ts | 8 ++++++++ 1 file changed, 8 insertions(+) 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 c2013ba3502e2..b5bcd33c3b9ab 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 @@ -13,12 +13,20 @@ function generateUniqueKey() { } export default ({ getPageObjects, getService }: FtrProviderContext) => { + const alerting = getService('alerting'); const testSubjects = getService('testSubjects'); const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const find = getService('find'); describe('Connectors', function() { before(async () => { + await alerting.actions.createAction({ + name: `server-log-${Date.now()}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }); + await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('connectorsTab'); }); From 783e3c17a9ed32d6a5aff39a83bdfa38aa152bf1 Mon Sep 17 00:00:00 2001 From: Tim Schnell Date: Thu, 9 Apr 2020 14:32:24 -0500 Subject: [PATCH 72/81] ignore some things for code coverage (#62701) Co-authored-by: Elastic Machine --- x-pack/legacy/plugins/canvas/scripts/jest.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/canvas/scripts/jest.js b/x-pack/legacy/plugins/canvas/scripts/jest.js index cce1b8d355846..133f775c7192f 100644 --- a/x-pack/legacy/plugins/canvas/scripts/jest.js +++ b/x-pack/legacy/plugins/canvas/scripts/jest.js @@ -36,6 +36,14 @@ run( `!${path}/**/__tests__/**/*`, '--collectCoverageFrom', // Ignore coverage on example files `!${path}/**/__examples__/**/*`, + '--collectCoverageFrom', // Ignore flot files + `!${path}/**/flot-charts/**`, + '--collectCoverageFrom', // Ignore coverage files + `!${path}/**/coverage/**`, + '--collectCoverageFrom', // Ignore scripts + `!${path}/**/scripts/**`, + '--collectCoverageFrom', // Ignore mock files + `!${path}/**/mocks/**`, '--collectCoverageFrom', // Include JS files `${path}/**/*.js`, '--collectCoverageFrom', // Include TS/X files @@ -76,7 +84,7 @@ run( --all Runs all tests and snapshots. Slower. --storybook Runs Storybook Snapshot tests only. --update Updates Storybook Snapshot tests. - --path Runs any tests at a given path. + --path Runs any tests at a given path. --coverage Collect coverage statistics. `, }, From 834306458ac5178cbe26a0f962c764a0451568e2 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 9 Apr 2020 13:52:02 -0600 Subject: [PATCH 73/81] Fixes a needed import that was coming from APM but now isolates things better for optimization import builds ## Summary Adds a single line to work with ts config optimizers. What is happening is that this is relying on an import from here: ``` x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx ``` when really it should be isolated and imported within this file. Testing is just to go to siem and run these commands: ```ts cd /projects/kibana node x-pack/legacy/plugins/siem/scripts/optimize_tsconfig.js node scripts/type_check.js --project x-pack/tsconfig.json ``` Ensure you don't see any errors. --- .../public/application/components/health_check.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index 5156a6146f3a1..9c51139993b3f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -10,6 +10,7 @@ import { HealthCheck } from './health_check'; import { act } from 'react-dom/test-utils'; import { httpServiceMock } from '../../../../../../src/core/public/mocks'; +import '@testing-library/jest-dom/extend-expect'; const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' }; From bc8c4a754db653762fe50a7bfc9bee7f9006e20c Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 9 Apr 2020 15:43:07 -0600 Subject: [PATCH 74/81] [File upload] Change 'file_upload' id and refs to 'fileUpload' (#63000) --- x-pack/plugins/file_upload/kibana.json | 3 +-- x-pack/plugins/maps/public/plugin.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/file_upload/kibana.json b/x-pack/plugins/file_upload/kibana.json index 3fda32fb6ebe5..7676a01d0b0f9 100644 --- a/x-pack/plugins/file_upload/kibana.json +++ b/x-pack/plugins/file_upload/kibana.json @@ -1,8 +1,7 @@ { - "id": "file_upload", + "id": "fileUpload", "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": ["xpack", "file_upload"], "server": true, "ui": true, "requiredPlugins": ["data", "usageCollection"] diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 9437c2512ded4..14487b615e759 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -43,9 +43,9 @@ export const bindSetupCoreAndPlugins = (core: CoreSetup, plugins: any) => { }; export const bindStartCoreAndPlugins = (core: CoreStart, plugins: any) => { - const { file_upload, data, inspector } = plugins; + const { fileUpload, data, inspector } = plugins; setInspector(inspector); - setFileUpload(file_upload); + setFileUpload(fileUpload); setIndexPatternSelect(data.ui.IndexPatternSelect); setTimeFilter(data.query.timefilter.timefilter); setIndexPatternService(data.indexPatterns); From 93b34632c08884e7850114f1fe21adfb47471c22 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Thu, 9 Apr 2020 23:53:47 +0200 Subject: [PATCH 75/81] [TSVB] Fix wrongly display stacked as percentage charts (#62654) Update to elastic-charts 18.2.2 with the valid 0% rendering --- package.json | 2 +- packages/kbn-ui-shared-deps/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index bd0fec3a5c116..ea930d07e7b43 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "@babel/core": "^7.9.0", "@babel/register": "^7.9.0", "@elastic/apm-rum": "^4.6.0", - "@elastic/charts": "^18.1.1", + "@elastic/charts": "18.2.2", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", "@elastic/eui": "21.0.1", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index e2823f23d0431..7c5d6a62a11ca 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --watch" }, "dependencies": { - "@elastic/charts": "^18.1.1", + "@elastic/charts": "18.2.2", "@elastic/eui": "21.0.1", "@kbn/i18n": "1.0.0", "abortcontroller-polyfill": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index 3f04b2d26a013..20e33fdefc996 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1197,10 +1197,10 @@ dependencies: "@elastic/apm-rum-core" "^4.7.0" -"@elastic/charts@^18.1.1": - version "18.2.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.2.0.tgz#e141151b4d7ecc71c9f6f235f8ce141665c67195" - integrity sha512-OWsARaHI/4Ict/GkeKIO3a+e2c86esGw3FtSGRLPFVgzpwBXdjvjYyraGntKOIVs/NAGNVWYj5XoRRb5C6cMlQ== +"@elastic/charts@18.2.2": + version "18.2.2" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.2.2.tgz#f59d6ee597d553d193314d8598561c65da787e8d" + integrity sha512-ss8AqLj9wHa2C+9ULUKbXw8ZCQmEjLuaVU5AkqE2j3hOVtAN75HO2p7nMIsxcSldfmqy+4jSptybJLNAfizegQ== dependencies: classnames "^2.2.6" d3-array "^1.2.4" From 982c0da78e67694e0c2e05e8e2e7431ab9bda880 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Thu, 9 Apr 2020 16:51:22 -0700 Subject: [PATCH 76/81] Move ILM out of legacy (#61915) * Rename IndexMgmtSetup to IndexManagementPluginSetup. * Remove unused fetch index template route and related tests. * Remove unnecessary custom styles. --- .github/CODEOWNERS | 2 +- x-pack/.i18nrc.json | 2 +- x-pack/index.js | 2 - .../np_ready/extend_index_management.ts | 4 +- .../public/np_ready/plugin.ts | 4 +- .../server/np_ready/plugin.ts | 4 +- .../index_lifecycle_management/index.ts | 62 -------- .../index_lifecycle_management/plugin.ts | 54 ------- .../public/legacy.ts | 109 ------------- .../_index_lifecycle_management.scss | 17 -- .../public/np_ready/application/index.scss | 3 - .../public/np_ready/application/index.tsx | 63 -------- .../np_ready/application/services/api.js | 81 ---------- .../public/np_ready/plugin.tsx | 58 ------- .../call_with_request_factory.ts | 19 --- .../lib/call_with_request_factory/index.ts | 7 - .../check_license/__tests__/check_license.js | 145 ------------------ .../server/lib/check_license/check_license.ts | 66 -------- .../__tests__/wrap_custom_error.js | 21 --- .../error_wrappers/__tests__/wrap_es_error.js | 38 ----- .../__tests__/wrap_unknown_error.js | 19 --- .../server/lib/error_wrappers/index.ts | 9 -- .../lib/error_wrappers/wrap_custom_error.ts | 18 --- .../lib/error_wrappers/wrap_es_error.ts | 29 ---- .../lib/error_wrappers/wrap_unknown_error.ts | 17 -- .../server/lib/is_es_error/index.ts | 7 - .../__tests__/license_pre_routing_factory.js | 69 --------- .../license_pre_routing_factory.ts | 29 ---- .../lib/register_license_checker/index.ts | 7 - .../register_license_checker.ts | 23 --- .../server/routes/api/index/index.ts | 7 - .../api/index/register_add_policy_route.ts | 58 ------- .../routes/api/index/register_remove_route.ts | 51 ------ .../routes/api/index/register_retry_route.ts | 51 ------ .../server/routes/api/nodes/constants.ts | 15 -- .../server/routes/api/nodes/index.ts | 7 - .../api/nodes/register_details_route.ts | 61 -------- .../routes/api/nodes/register_list_route.ts | 63 -------- .../server/routes/api/policies/index.ts | 7 - .../api/policies/register_create_route.ts | 52 ------- .../api/policies/register_delete_route.ts | 46 ------ .../api/policies/register_fetch_route.ts | 84 ---------- .../server/routes/api/templates/index.ts | 7 - .../templates/register_add_policy_route.ts | 67 -------- .../api/templates/register_get_route.ts | 48 ------ .../index_lifecycle_management/shim.ts | 17 -- .../legacy/plugins/index_management/index.ts | 1 + x-pack/legacy/plugins/rollup/public/plugin.ts | 4 +- x-pack/legacy/plugins/rollup/server/plugin.ts | 4 +- .../extend_index_management.test.js.snap | 0 .../__snapshots__/policy_table.test.js.snap | 0 .../__jest__/components/edit_policy.test.js | 20 ++- .../__jest__/components/policy_table.test.js | 16 +- .../__jest__/extend_index_management.test.js | 17 +- .../common/constants/index.ts | 6 + .../index_lifecycle_management/kibana.json | 16 ++ .../public}/application/app.tsx | 2 +- .../public}/application/constants/index.ts | 0 .../application/constants/ui_metric.ts | 0 .../public/application/index.tsx | 27 ++++ .../sections/components/active_badge.js | 0 .../application/sections/components/index.js | 0 .../sections/components/learn_more_link.js | 0 .../sections/components/optional_label.js | 0 .../components/phase_error_message.js | 0 .../cold_phase/cold_phase.container.js | 0 .../components/cold_phase/cold_phase.js | 0 .../components/cold_phase/index.js | 0 .../delete_phase/delete_phase.container.js | 0 .../components/delete_phase/delete_phase.js | 0 .../components/delete_phase/index.js | 0 .../hot_phase/hot_phase.container.js | 0 .../components/hot_phase/hot_phase.js | 0 .../edit_policy/components/hot_phase/index.js | 0 .../edit_policy/components/min_age_input.js | 0 .../components/node_allocation/index.js | 0 .../node_allocation.container.js | 0 .../node_allocation/node_allocation.js | 0 .../components/node_attrs_details/index.js | 0 .../node_attrs_details.container.js | 0 .../node_attrs_details/node_attrs_details.js | 0 .../components/policy_json_flyout.js | 0 .../components/set_priority_input.js | 0 .../components/warm_phase/index.js | 0 .../warm_phase/warm_phase.container.js | 0 .../components/warm_phase/warm_phase.js | 0 .../edit_policy/edit_policy.container.js | 0 .../sections/edit_policy/edit_policy.js | 0 .../sections/edit_policy/form_errors.js | 0 .../sections/edit_policy/index.d.ts | 0 .../application/sections/edit_policy/index.js | 0 .../no_match/components/no_match/index.js | 0 .../no_match/components/no_match/no_match.js | 0 .../policy_table/components/no_match/index.js | 0 .../add_policy_to_template_confirm_modal.js | 0 .../components/policy_table/confirm_delete.js | 0 .../components/policy_table/index.js | 0 .../policy_table/policy_table.container.js | 0 .../components/policy_table/policy_table.js | 8 +- .../sections/policy_table/index.d.ts | 0 .../sections/policy_table/index.js | 0 .../public/application/services/api.js | 72 +++++++++ .../application/services/api_errors.js | 0 .../application/services/documentation.ts | 0 .../application/services/filter_items.js | 0 .../application/services/find_errors.js | 0 .../services/flatten_panel_tree.js | 0 .../public}/application/services/http.ts | 14 +- .../public}/application/services/index.js | 0 .../application/services/navigation.ts | 12 +- .../application/services/notification.ts | 0 .../application/services/sort_table.js | 0 .../application/services/ui_metric.test.js | 0 .../public}/application/services/ui_metric.ts | 11 +- .../application/store/actions/general.js | 0 .../application/store/actions/index.js | 0 .../application/store/actions/lifecycle.js | 0 .../application/store/actions/nodes.js | 0 .../application/store/actions/policies.js | 0 .../application/store/defaults/cold_phase.js | 0 .../store/defaults/delete_phase.js | 0 .../application/store/defaults/hot_phase.js | 0 .../application/store/defaults/index.d.ts | 0 .../application/store/defaults/index.js | 0 .../application/store/defaults/warm_phase.js | 0 .../public}/application/store/index.d.ts | 0 .../public}/application/store/index.js | 0 .../application/store/reducers/general.js | 0 .../application/store/reducers/index.js | 0 .../application/store/reducers/nodes.js | 0 .../application/store/reducers/policies.js | 0 .../application/store/selectors/general.js | 0 .../application/store/selectors/index.js | 0 .../application/store/selectors/lifecycle.js | 0 .../application/store/selectors/nodes.js | 0 .../application/store/selectors/policies.js | 0 .../public}/application/store/store.js | 0 .../components/add_lifecycle_confirm_modal.js | 6 +- .../components/index_lifecycle_summary.js | 0 .../remove_lifecycle_confirm_modal.js | 4 +- .../extend_index_management/index.d.ts | 0 .../public}/extend_index_management/index.js | 50 +----- .../public}/index.ts | 8 +- .../public/plugin.tsx | 69 +++++++++ .../public/types.ts | 21 +++ .../server/config.ts | 18 +++ .../server/index.ts | 19 +++ .../server/lib}/is_es_error.ts | 0 .../server/plugin.ts | 86 +++++++++++ .../server/routes/api/index/index.ts} | 9 +- .../api/index/register_add_policy_route.ts | 66 ++++++++ .../routes/api/index/register_remove_route.ts | 54 +++++++ .../routes/api/index/register_retry_route.ts | 54 +++++++ .../server/routes/api/nodes/index.ts} | 7 +- .../api/nodes/register_details_route.ts | 64 ++++++++ .../routes/api/nodes/register_list_route.ts | 69 +++++++++ .../server/routes/api/policies/index.ts} | 9 +- .../api/policies/register_create_route.ts | 145 ++++++++++++++++++ .../api/policies/register_delete_route.ts | 50 ++++++ .../api/policies/register_fetch_route.ts | 90 +++++++++++ .../server/routes/api/templates/index.ts} | 9 +- .../templates/register_add_policy_route.ts | 81 ++++++++++ .../api/templates/register_fetch_route.ts | 51 +++--- .../server/routes/index.ts | 19 +++ .../server/services/add_base_path.ts} | 4 +- .../server/services}/index.ts | 3 +- .../server/services/license.ts | 82 ++++++++++ .../server/types.ts | 27 ++++ .../index_actions_context_menu.js | 15 +- .../plugins/index_management/public/index.ts | 4 +- .../plugins/index_management/public/plugin.ts | 4 +- .../plugins/index_management/server/index.ts | 2 +- .../plugins/index_management/server/plugin.ts | 6 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - .../templates.helpers.js | 3 - .../index_lifecycle_management/templates.js | 19 +-- 177 files changed, 1264 insertions(+), 1828 deletions(-) delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/index.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/plugin.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/public/legacy.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/_index_lifecycle_management.scss delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.scss delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.tsx delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/api.js delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/plugin.tsx delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/call_with_request_factory.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/index.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/__tests__/check_license.js delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_custom_error.js delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_es_error.js delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_unknown_error.js delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/index.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_custom_error.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_es_error.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_unknown_error.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/is_es_error/index.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/index.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/register_license_checker.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/index.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/constants.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/index.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/index.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/index.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_get_route.ts delete mode 100644 x-pack/legacy/plugins/index_lifecycle_management/shim.ts rename x-pack/{legacy => }/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap (100%) rename x-pack/{legacy => }/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap (100%) rename x-pack/{legacy => }/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js (96%) rename x-pack/{legacy => }/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js (91%) rename x-pack/{legacy => }/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js (93%) rename x-pack/{legacy => }/plugins/index_lifecycle_management/common/constants/index.ts (72%) create mode 100644 x-pack/plugins/index_lifecycle_management/kibana.json rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/app.tsx (94%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/constants/index.ts (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/constants/ui_metric.ts (100%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/index.tsx rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/components/active_badge.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/components/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/components/learn_more_link.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/components/optional_label.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/components/phase_error_message.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/cold_phase/cold_phase.container.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/cold_phase/cold_phase.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/cold_phase/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/delete_phase/delete_phase.container.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/delete_phase/delete_phase.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/delete_phase/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/hot_phase/hot_phase.container.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/hot_phase/hot_phase.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/hot_phase/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/min_age_input.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/node_allocation/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/node_allocation/node_allocation.container.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/node_allocation/node_allocation.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/node_attrs_details/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.container.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/policy_json_flyout.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/set_priority_input.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/warm_phase/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/warm_phase/warm_phase.container.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/components/warm_phase/warm_phase.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/edit_policy.container.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/edit_policy.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/form_errors.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/index.d.ts (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/edit_policy/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/policy_table/components/no_match/components/no_match/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/policy_table/components/no_match/components/no_match/no_match.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/policy_table/components/no_match/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/policy_table/components/policy_table/confirm_delete.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/policy_table/components/policy_table/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/policy_table/components/policy_table/policy_table.container.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/policy_table/components/policy_table/policy_table.js (98%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/policy_table/index.d.ts (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/sections/policy_table/index.js (100%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/api.js rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/services/api_errors.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/services/documentation.ts (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/services/filter_items.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/services/find_errors.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/services/flatten_panel_tree.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/services/http.ts (50%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/services/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/services/navigation.ts (59%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/services/notification.ts (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/services/sort_table.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/services/ui_metric.test.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/services/ui_metric.ts (85%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/actions/general.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/actions/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/actions/lifecycle.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/actions/nodes.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/actions/policies.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/defaults/cold_phase.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/defaults/delete_phase.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/defaults/hot_phase.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/defaults/index.d.ts (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/defaults/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/defaults/warm_phase.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/index.d.ts (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/reducers/general.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/reducers/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/reducers/nodes.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/reducers/policies.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/selectors/general.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/selectors/index.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/selectors/lifecycle.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/selectors/nodes.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/selectors/policies.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/application/store/store.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/extend_index_management/components/add_lifecycle_confirm_modal.js (97%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/extend_index_management/components/index_lifecycle_summary.js (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/extend_index_management/components/remove_lifecycle_confirm_modal.js (96%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/extend_index_management/index.d.ts (100%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/extend_index_management/index.js (80%) rename x-pack/{legacy/plugins/index_lifecycle_management/public/np_ready => plugins/index_lifecycle_management/public}/index.ts (58%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/plugin.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/types.ts create mode 100644 x-pack/plugins/index_lifecycle_management/server/config.ts create mode 100644 x-pack/plugins/index_lifecycle_management/server/index.ts rename x-pack/{legacy/plugins/index_lifecycle_management/server/lib/is_es_error => plugins/index_lifecycle_management/server/lib}/is_es_error.ts (100%) create mode 100644 x-pack/plugins/index_lifecycle_management/server/plugin.ts rename x-pack/{legacy/plugins/index_lifecycle_management/server/routes/api/index/register_index_routes.ts => plugins/index_lifecycle_management/server/routes/api/index/index.ts} (65%) create mode 100644 x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts create mode 100644 x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts create mode 100644 x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts rename x-pack/{legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_nodes_routes.ts => plugins/index_lifecycle_management/server/routes/api/nodes/index.ts} (65%) create mode 100644 x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts create mode 100644 x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts rename x-pack/{legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_policies_routes.ts => plugins/index_lifecycle_management/server/routes/api/policies/index.ts} (64%) create mode 100644 x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts create mode 100644 x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts create mode 100644 x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts rename x-pack/{legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_templates_routes.ts => plugins/index_lifecycle_management/server/routes/api/templates/index.ts} (64%) create mode 100644 x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts rename x-pack/{legacy => }/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts (63%) create mode 100644 x-pack/plugins/index_lifecycle_management/server/routes/index.ts rename x-pack/{legacy/plugins/index_lifecycle_management/server/lib/check_license/index.ts => plugins/index_lifecycle_management/server/services/add_base_path.ts} (64%) rename x-pack/{legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory => plugins/index_lifecycle_management/server/services}/index.ts (74%) create mode 100644 x-pack/plugins/index_lifecycle_management/server/services/license.ts create mode 100644 x-pack/plugins/index_lifecycle_management/server/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e707250ff3261..267f3dde0b66f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -180,7 +180,7 @@ /src/plugins/console/ @elastic/es-ui /src/plugins/es_ui_shared/ @elastic/es-ui /x-pack/legacy/plugins/cross_cluster_replication/ @elastic/es-ui -/x-pack/legacy/plugins/index_lifecycle_management/ @elastic/es-ui +/x-pack/plugins/index_lifecycle_management/ @elastic/es-ui /x-pack/legacy/plugins/index_management/ @elastic/es-ui /x-pack/legacy/plugins/license_management/ @elastic/es-ui /x-pack/legacy/plugins/rollup/ @elastic/es-ui diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index ae8d61769b14c..bbbcc062786b0 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -18,7 +18,7 @@ "xpack.graph": ["legacy/plugins/graph", "plugins/graph"], "xpack.grokDebugger": "plugins/grokdebugger", "xpack.idxMgmt": "plugins/index_management", - "xpack.indexLifecycleMgmt": "legacy/plugins/index_lifecycle_management", + "xpack.indexLifecycleMgmt": "plugins/index_lifecycle_management", "xpack.infra": "plugins/infra", "xpack.ingestManager": "plugins/ingest_manager", "xpack.lens": "legacy/plugins/lens", diff --git a/x-pack/index.js b/x-pack/index.js index 6fab13d726fa6..3126dc17a7107 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -16,7 +16,6 @@ import { beats } from './legacy/plugins/beats_management'; import { apm } from './legacy/plugins/apm'; import { maps } from './legacy/plugins/maps'; import { indexManagement } from './legacy/plugins/index_management'; -import { indexLifecycleManagement } from './legacy/plugins/index_lifecycle_management'; import { spaces } from './legacy/plugins/spaces'; import { canvas } from './legacy/plugins/canvas'; import { infra } from './legacy/plugins/infra'; @@ -50,7 +49,6 @@ module.exports = function(kibana) { maps(kibana), canvas(kibana), indexManagement(kibana), - indexLifecycleManagement(kibana), infra(kibana), taskManager(kibana), rollup(kibana), diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts index 01c6250383fb8..4ffe0db4e3c4e 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { IndexMgmtSetup } from '../../../../../plugins/index_management/public'; +import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/public'; const propertyPath = 'isFollowerIndex'; @@ -21,7 +21,7 @@ const followerBadgeExtension = { filterExpression: 'isFollowerIndex:true', }; -export const extendIndexManagement = (indexManagement?: IndexMgmtSetup) => { +export const extendIndexManagement = (indexManagement?: IndexManagementPluginSetup) => { if (indexManagement) { indexManagement.extensionsService.addBadge(followerBadgeExtension); } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts index f7651cbb210a7..46259c698b282 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts @@ -11,7 +11,7 @@ import { DocLinksStart, } from 'src/core/public'; -import { IndexMgmtSetup } from '../../../../../plugins/index_management/public'; +import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/public'; // @ts-ignore; import { setHttpClient } from './app/services/api'; @@ -21,7 +21,7 @@ import { setNotifications } from './app/services/notifications'; import { extendIndexManagement } from './extend_index_management'; interface PluginDependencies { - indexManagement: IndexMgmtSetup; + indexManagement: IndexManagementPluginSetup; __LEGACY: { chrome: any; MANAGEMENT_BREADCRUMB: ChromeBreadcrumb; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts index 1012c07af3d2a..829de10ad0177 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts @@ -6,7 +6,7 @@ import { Plugin, PluginInitializerContext, CoreSetup } from 'src/core/server'; -import { IndexMgmtSetup } from '../../../../../plugins/index_management/server'; +import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/server'; // @ts-ignore import { registerLicenseChecker } from './lib/register_license_checker'; @@ -15,7 +15,7 @@ import { registerRoutes } from './routes/register_routes'; import { ccrDataEnricher } from './cross_cluster_replication_data'; interface PluginDependencies { - indexManagement: IndexMgmtSetup; + indexManagement: IndexManagementPluginSetup; __LEGACY: { server: any; ccrUIEnabled: boolean; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/index.ts deleted file mode 100644 index 9b14b7143bf44..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/index.ts +++ /dev/null @@ -1,62 +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 { Legacy } from 'kibana'; -import { resolve } from 'path'; -import { PLUGIN } from './common/constants'; -import { Plugin as IndexLifecycleManagementPlugin } from './plugin'; -import { createShim } from './shim'; - -export function indexLifecycleManagement(kibana: any) { - return new kibana.Plugin({ - id: PLUGIN.ID, - configPrefix: 'xpack.ilm', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main', 'index_management'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/np_ready/application/index.scss'), - managementSections: ['plugins/index_lifecycle_management/legacy'], - injectDefaultVars(server: Legacy.Server) { - const config = server.config(); - return { - ilmUiEnabled: config.get('xpack.ilm.ui.enabled'), - }; - }, - }, - config: (Joi: any) => { - return Joi.object({ - // display menu item - ui: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - - // enable plugin - enabled: Joi.boolean().default(true), - - filteredNodeAttributes: Joi.array() - .items(Joi.string()) - .default([]), - }).default(); - }, - isEnabled(config: any) { - return ( - config.get('xpack.ilm.enabled') && - config.has('xpack.index_management.enabled') && - config.get('xpack.index_management.enabled') - ); - }, - init(server: Legacy.Server) { - const core = server.newPlatform.setup.core; - const plugins = {}; - const __LEGACY = createShim(server); - - const indexLifecycleManagementPlugin = new IndexLifecycleManagementPlugin(); - - // Set up plugin. - indexLifecycleManagementPlugin.setup(core, plugins, __LEGACY); - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/plugin.ts b/x-pack/legacy/plugins/index_lifecycle_management/plugin.ts deleted file mode 100644 index 38d1bea45ce07..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/plugin.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 { CoreSetup } from 'kibana/server'; -import { LegacySetup } from './shim'; -import { registerLicenseChecker } from './server/lib/register_license_checker'; -import { registerIndexRoutes } from './server/routes/api/index'; -import { registerNodesRoutes } from './server/routes/api/nodes'; -import { registerPoliciesRoutes } from './server/routes/api/policies'; -import { registerTemplatesRoutes } from './server/routes/api/templates'; - -const indexLifecycleDataEnricher = async (indicesList: any, callWithRequest: any) => { - if (!indicesList || !indicesList.length) { - return; - } - const params = { - path: '/*/_ilm/explain', - method: 'GET', - }; - const { indices: ilmIndicesData } = await callWithRequest('transport.request', params); - return indicesList.map((index: any): any => { - return { - ...index, - ilm: { ...(ilmIndicesData[index.name] || {}) }, - }; - }); -}; - -export class Plugin { - public setup(core: CoreSetup, plugins: any, __LEGACY: LegacySetup): void { - const { server } = __LEGACY; - - registerLicenseChecker(server); - - // Register routes. - registerIndexRoutes(server); - registerNodesRoutes(server); - registerPoliciesRoutes(server); - registerTemplatesRoutes(server); - - const serverPlugins = server.newPlatform.setup.plugins as any; - - if ( - server.config().get('xpack.ilm.ui.enabled') && - serverPlugins.indexManagement && - serverPlugins.indexManagement.indexDataEnricher - ) { - serverPlugins.indexManagement.indexDataEnricher.add(indexLifecycleDataEnricher); - } - } -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/legacy.ts b/x-pack/legacy/plugins/index_lifecycle_management/public/legacy.ts deleted file mode 100644 index 006e5f6098f2b..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/legacy.ts +++ /dev/null @@ -1,109 +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 { App } from 'src/core/public'; - -/* Legacy Imports */ -import { npSetup, npStart } from 'ui/new_platform'; -import chrome from 'ui/chrome'; -import routes from 'ui/routes'; -import { management } from 'ui/management'; -import { createUiStatsReporter } from '../../../../../src/legacy/core_plugins/ui_metric/public'; - -import { PLUGIN, BASE_PATH } from '../common/constants'; -import { createPlugin } from './np_ready'; -import { addAllExtensions } from './np_ready/extend_index_management'; - -if (chrome.getInjected('ilmUiEnabled')) { - // We have to initialize this outside of the NP lifecycle, otherwise these extensions won't - // be available in Index Management unless the user visits ILM first. - if ((npSetup.plugins as any).indexManagement) { - addAllExtensions((npSetup.plugins as any).indexManagement.extensionsService); - } - - // This method handles the cleanup needed when route is scope is destroyed. It also prevents Angular - // from destroying scope when route changes and both old route and new route are this same route. - const manageAngularLifecycle = ($scope: any, $route: any, unmount: () => void) => { - const lastRoute = $route.current; - const deregister = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - // if templates are the same we are on the same route - if (lastRoute.$$route.template === currentRoute.$$route.template) { - // this prevents angular from destroying scope - $route.current = lastRoute; - } - }); - $scope.$on('$destroy', () => { - if (deregister) { - deregister(); - } - unmount(); - }); - }; - - // Once this app no longer depends upon Angular's routing (e.g. for the "redirect" service), we can - // use the Management plugin's API to register this app within the Elasticsearch section. - const esSection = management.getSection('elasticsearch'); - esSection.register('index_lifecycle_policies', { - visible: true, - display: PLUGIN.TITLE, - order: 2, - url: `#${BASE_PATH}policies`, - }); - - const REACT_ROOT_ID = 'indexLifecycleManagementReactRoot'; - - const template = ` -
- - `; - - routes.when(`${BASE_PATH}:view?/:action?/:id?`, { - template, - controllerAs: 'indexLifecycleManagement', - controller: class IndexLifecycleManagementController { - constructor($scope: any, $route: any, kbnUrl: any, $rootScope: any) { - $scope.$$postDigest(() => { - const element = document.getElementById(REACT_ROOT_ID)!; - const { core } = npSetup; - - const coreDependencies = { - ...core, - application: { - ...core.application, - async register(app: App) { - const unmountApp = await app.mount({ ...npStart } as any, { - element, - appBasePath: '', - onAppLeave: () => undefined, - // TODO: adapt to use Core's ScopedHistory - history: {} as any, - }); - manageAngularLifecycle($scope, $route, unmountApp as any); - }, - }, - }; - - // The Plugin interface won't allow us to pass __LEGACY as a third argument, so we'll just - // sneak it inside of the plugins argument for now. - const pluginDependencies = { - __LEGACY: { - redirect: (path: string) => { - $scope.$evalAsync(() => { - kbnUrl.redirect(path); - }); - }, - createUiStatsReporter, - }, - }; - - const plugin = createPlugin({} as any); - plugin.setup(coreDependencies, pluginDependencies); - }); - } - } as any, - } as any); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/_index_lifecycle_management.scss b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/_index_lifecycle_management.scss deleted file mode 100644 index 96c6d1a938c61..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/_index_lifecycle_management.scss +++ /dev/null @@ -1,17 +0,0 @@ -.policyTable__horizontalScrollContainer { - overflow-x: auto; - max-width: 100%; -} - -.policyTable__horizontalScroll { - min-width: 800px; - width: 100%; -} - -.policyTable__link { - font-weight: 400; -} - -.ilmEditPolicyPageContent { - max-width: 1200px !important; -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.scss b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.scss deleted file mode 100644 index 53e90e2aae35b..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; -@import 'index_lifecycle_management'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.tsx b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.tsx deleted file mode 100644 index b87a633d65c9c..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.tsx +++ /dev/null @@ -1,63 +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 from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { Provider } from 'react-redux'; -import { DocLinksStart, ToastsSetup, HttpSetup, FatalErrorsSetup } from 'src/core/public'; - -import { App } from './app'; -import { indexLifecycleManagementStore } from './store'; -import { init as initHttp } from './services/http'; -import { init as initNavigation } from './services/navigation'; -import { init as initDocumentation } from './services/documentation'; -import { init as initUiMetric } from './services/ui_metric'; -import { init as initNotification } from './services/notification'; - -export interface LegacySetup { - redirect: any; - createUiStatsReporter: any; -} - -interface AppDependencies { - legacy: LegacySetup; - I18nContext: any; - http: HttpSetup; - toasts: ToastsSetup; - fatalErrors: FatalErrorsSetup; - docLinks: DocLinksStart; - element: HTMLElement; -} - -export const renderApp = (appDependencies: AppDependencies) => { - const { - legacy: { redirect, createUiStatsReporter }, - I18nContext, - http, - toasts, - fatalErrors, - docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, - element, - } = appDependencies; - - // Initialize services - initHttp(http); - initNavigation(redirect); - initDocumentation(`${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`); - initUiMetric(createUiStatsReporter); - initNotification(toasts, fatalErrors); - - render( - - - - - , - element - ); - - return () => unmountComponentAtNode(element); -}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/api.js b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/api.js deleted file mode 100644 index f13bbcb6162b8..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/api.js +++ /dev/null @@ -1,81 +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 { - UIM_POLICY_DELETE, - UIM_POLICY_ATTACH_INDEX, - UIM_POLICY_ATTACH_INDEX_TEMPLATE, - UIM_POLICY_DETACH_INDEX, - UIM_INDEX_RETRY_STEP, -} from '../constants'; - -import { trackUiMetric } from './ui_metric'; -import { sendGet, sendPost, sendDelete } from './http'; - -// The extend_index_management module that we support an injected httpClient here. - -export async function loadNodes(httpClient) { - return await sendGet(`nodes/list`, httpClient); -} - -export async function loadNodeDetails(selectedNodeAttrs, httpClient) { - return await sendGet(`nodes/${selectedNodeAttrs}/details`, httpClient); -} - -export async function loadIndexTemplates(httpClient) { - return await sendGet(`templates`, httpClient); -} - -export async function loadIndexTemplate(templateName, httpClient) { - if (!templateName) { - return {}; - } - return await sendGet(`templates/${templateName}`, httpClient); -} - -export async function loadPolicies(withIndices, httpClient) { - const query = withIndices ? '?withIndices=true' : ''; - return await sendGet('policies', query, httpClient); -} - -export async function savePolicy(policy, httpClient) { - return await sendPost(`policies`, policy, httpClient); -} - -export async function deletePolicy(policyName, httpClient) { - const response = await sendDelete(`policies/${encodeURIComponent(policyName)}`, httpClient); - // Only track successful actions. - trackUiMetric('count', UIM_POLICY_DELETE); - return response; -} - -export const retryLifecycleForIndex = async (indexNames, httpClient) => { - const response = await sendPost(`index/retry`, { indexNames }, httpClient); - // Only track successful actions. - trackUiMetric('count', UIM_INDEX_RETRY_STEP); - return response; -}; - -export const removeLifecycleForIndex = async (indexNames, httpClient) => { - const response = await sendPost(`index/remove`, { indexNames }, httpClient); - // Only track successful actions. - trackUiMetric('count', UIM_POLICY_DETACH_INDEX); - return response; -}; - -export const addLifecyclePolicyToIndex = async (body, httpClient) => { - const response = await sendPost(`index/add`, body, httpClient); - // Only track successful actions. - trackUiMetric('count', UIM_POLICY_ATTACH_INDEX); - return response; -}; - -export const addLifecyclePolicyToTemplate = async (body, httpClient) => { - const response = await sendPost(`template`, body, httpClient); - // Only track successful actions. - trackUiMetric('count', UIM_POLICY_ATTACH_INDEX_TEMPLATE); - return response; -}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/plugin.tsx b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/plugin.tsx deleted file mode 100644 index e2897f09fa892..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/plugin.tsx +++ /dev/null @@ -1,58 +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 { CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { PLUGIN } from '../../common/constants'; -import { LegacySetup } from './application'; - -interface PluginsSetup { - __LEGACY: LegacySetup; -} - -export class IndexLifecycleManagementPlugin implements Plugin { - setup(core: CoreSetup, plugins: PluginsSetup) { - // Extract individual core dependencies. - const { - application, - notifications: { toasts }, - fatalErrors, - http, - } = core; - - // The Plugin interface won't allow us to pass __LEGACY as a third argument, so we'll just - // sneak it inside of the plugins parameter for now. - const { __LEGACY } = plugins; - - application.register({ - id: PLUGIN.ID, - title: PLUGIN.TITLE, - async mount(config, mountPoint) { - const { - core: { - docLinks, - i18n: { Context: I18nContext }, - }, - } = config; - - const { element } = mountPoint; - const { renderApp } = await import('./application'); - - // Inject all dependencies into our app. - return renderApp({ - legacy: { ...__LEGACY }, - I18nContext, - http, - toasts, - fatalErrors, - docLinks, - element, - }); - }, - }); - } - start(core: CoreStart, plugins: any) {} - stop() {} -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/call_with_request_factory.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/call_with_request_factory.ts deleted file mode 100644 index 1b28dc4fde4f7..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/call_with_request_factory.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; -import { Legacy } from 'kibana'; - -const callWithRequest = once((server: Legacy.Server): any => { - const cluster = server.plugins.elasticsearch.getCluster('data'); - return cluster.callWithRequest; -}); - -export const callWithRequestFactory = (server: Legacy.Server, request: any) => { - return (...args: any[]) => { - return callWithRequest(server)(request, ...args); - }; -}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/index.ts deleted file mode 100644 index 787814d87dff9..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/index.ts +++ /dev/null @@ -1,7 +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 { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/__tests__/check_license.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/__tests__/check_license.js deleted file mode 100644 index 933fda01c055d..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/__tests__/check_license.js +++ /dev/null @@ -1,145 +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 expect from '@kbn/expect'; -import { set } from 'lodash'; -import { checkLicense } from '../check_license'; - -describe('check_license', function() { - let mockLicenseInfo; - beforeEach(() => (mockLicenseInfo = {})); - - describe('license information is undefined', () => { - beforeEach(() => (mockLicenseInfo = undefined)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('license information is not available', () => { - beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('license information is available', () => { - beforeEach(() => { - mockLicenseInfo.isAvailable = () => true; - set(mockLicenseInfo, 'license.getType', () => 'basic'); - }); - - describe('& license is > basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true); - }); - - it('should not set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.be(undefined); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); - - describe('& license is basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true); - }); - - it('should not set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.be(undefined); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.ts deleted file mode 100644 index b35ab14964d55..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export function checkLicense(xpackLicenseInfo: any): any { - const pluginName = 'Index Lifecycle Policies'; - - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - isAvailable: false, - showLinks: true, - enableLinks: false, - message: i18n.translate('xpack.indexLifecycleMgmt.checkLicense.errorUnavailableMessage', { - defaultMessage: - 'You cannot use {pluginName} because license information is not available at this time.', - values: { pluginName }, - }), - }; - } - - const VALID_LICENSE_MODES = ['basic', 'standard', 'gold', 'platinum', 'enterprise', 'trial']; - - const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_LICENSE_MODES); - const isLicenseActive = xpackLicenseInfo.license.isActive(); - const licenseType = xpackLicenseInfo.license.getType(); - - // License is not valid - if (!isLicenseModeValid) { - return { - isAvailable: false, - showLinks: false, - message: i18n.translate('xpack.indexLifecycleMgmt.checkLicense.errorUnsupportedMessage', { - defaultMessage: - 'Your {licenseType} license does not support {pluginName}. Please upgrade your license.', - values: { licenseType, pluginName }, - }), - }; - } - - // License is valid but not active - if (!isLicenseActive) { - return { - isAvailable: false, - showLinks: true, - enableLinks: false, - message: i18n.translate('xpack.indexLifecycleMgmt.checkLicense.errorExpiredMessage', { - defaultMessage: - 'You cannot use {pluginName} because your {licenseType} license has expired.', - values: { pluginName, licenseType }, - }), - }; - } - - // License is valid and active - return { - isAvailable: true, - showLinks: true, - enableLinks: true, - }; -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_custom_error.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_custom_error.js deleted file mode 100644 index f9c102be7a1ff..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_custom_error.js +++ /dev/null @@ -1,21 +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 expect from '@kbn/expect'; -import { wrapCustomError } from '../wrap_custom_error'; - -describe('wrap_custom_error', () => { - describe('#wrapCustomError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const statusCode = 404; - const wrappedError = wrapCustomError(originalError, statusCode); - - expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.output.statusCode).to.equal(statusCode); - }); - }); -}); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_es_error.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_es_error.js deleted file mode 100644 index fe2b6cce652f1..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_es_error.js +++ /dev/null @@ -1,38 +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 expect from '@kbn/expect'; -import { wrapEsError } from '../wrap_es_error'; - -describe('wrap_es_error', () => { - describe('#wrapEsError', () => { - let originalError; - beforeEach(() => { - originalError = new Error('I am an error'); - originalError.statusCode = 404; - }); - - it('should return a Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - - it('should return the correct Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be(originalError.message); - }); - - it('should return the correct Boom object with custom message', () => { - const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' }); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be('No encontrado!'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_unknown_error.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_unknown_error.js deleted file mode 100644 index 85e0b2b3033ad..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_unknown_error.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapUnknownError } from '../wrap_unknown_error'; - -describe('wrap_unknown_error', () => { - describe('#wrapUnknownError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const wrappedError = wrapUnknownError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/index.ts deleted file mode 100644 index f275f15637091..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/index.ts +++ /dev/null @@ -1,9 +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 { wrapCustomError } from './wrap_custom_error'; -export { wrapEsError } from './wrap_es_error'; -export { wrapUnknownError } from './wrap_unknown_error'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_custom_error.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_custom_error.ts deleted file mode 100644 index c5780e7c83fb5..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_custom_error.ts +++ /dev/null @@ -1,18 +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 Boom from 'boom'; - -/** - * Wraps a custom error into a Boom error response and returns it - * - * @param err Object error - * @param statusCode Error status code - * @return Object Boom error response - */ -export function wrapCustomError(err: any, statusCode: any): any { - return Boom.boomify(err, { statusCode }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_es_error.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_es_error.ts deleted file mode 100644 index 6980a5afa5eac..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_es_error.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps an error thrown by the ES JS client into a Boom error response and returns it - * - * @param err Object Error thrown by ES JS client - * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages - * @return Object Boom error response - */ -export function wrapEsError(err: any, statusCodeToMessageMap: any = {}): any { - const statusCode = err.statusCode; - - // If no custom message if specified for the error's status code, just - // wrap the error as a Boom error response and return it - if (!statusCodeToMessageMap[statusCode]) { - return Boom.boomify(err, { statusCode }); - } - - // Otherwise, use the custom message to create a Boom error response and - // return it - const message = statusCodeToMessageMap[statusCode]; - return new Boom(message, { statusCode }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_unknown_error.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_unknown_error.ts deleted file mode 100644 index ede1baec286f3..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_unknown_error.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps an unknown error into a Boom error response and returns it - * - * @param err Object Unknown error - * @return Object Boom error response - */ -export function wrapUnknownError(err: any): any { - return Boom.boomify(err); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/is_es_error/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/is_es_error/index.ts deleted file mode 100644 index a9a3c61472d8c..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/is_es_error/index.ts +++ /dev/null @@ -1,7 +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 { isEsError } from './is_es_error'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js deleted file mode 100644 index 4d3b33f8b3af3..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js +++ /dev/null @@ -1,69 +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 expect from '@kbn/expect'; -import { licensePreRoutingFactory } from '../license_pre_routing_factory'; - -describe('license_pre_routing_factory', () => { - describe('#reportingFeaturePreRoutingFactory', () => { - let mockServer; - let mockLicenseCheckResults; - - beforeEach(() => { - mockServer = { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }; - }); - - it('only instantiates one instance per server', () => { - const firstInstance = licensePreRoutingFactory(mockServer); - const secondInstance = licensePreRoutingFactory(mockServer); - - expect(firstInstance).to.be(secondInstance); - }); - - describe('isAvailable is false', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: false, - }; - }); - - it('replies with 403', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const stubRequest = {}; - expect(() => licensePreRouting(stubRequest)).to.throwException(response => { - expect(response).to.be.an(Error); - expect(response.isBoom).to.be(true); - expect(response.output.statusCode).to.be(403); - }); - }); - }); - - describe('isAvailable is true', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: true, - }; - }); - - it('replies with nothing', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const stubRequest = {}; - const response = licensePreRouting(stubRequest); - expect(response).to.be(null); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts deleted file mode 100644 index e348125967c14..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; -import { Legacy } from 'kibana'; - -import { PLUGIN } from '../../../common/constants'; -import { wrapCustomError } from '../error_wrappers'; - -export const licensePreRoutingFactory = once((server: Legacy.Server) => { - const xpackMainPlugin = server.plugins.xpack_main; - - // License checking and enable/disable logic - function licensePreRouting() { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - if (!licenseCheckResults.isAvailable) { - const error = new Error(licenseCheckResults.message); - const statusCode = 403; - throw wrapCustomError(error, statusCode); - } - - return null; - } - - return licensePreRouting; -}); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/index.ts deleted file mode 100644 index 7b0f97c38d129..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/index.ts +++ /dev/null @@ -1,7 +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 { registerLicenseChecker } from './register_license_checker'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/register_license_checker.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/register_license_checker.ts deleted file mode 100644 index 8e3b89fa20e33..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/register_license_checker.ts +++ /dev/null @@ -1,23 +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 { Legacy } from 'kibana'; -// @ts-ignore -import { mirrorPluginStatus } from '../../../../../server/lib/mirror_plugin_status'; -import { PLUGIN } from '../../../common/constants'; -import { checkLicense } from '../check_license'; - -export function registerLicenseChecker(server: Legacy.Server) { - const xpackMainPlugin = server.plugins.xpack_main as any; - const ilmPlugin = (server.plugins as any).index_lifecycle_management; - - mirrorPluginStatus(xpackMainPlugin, ilmPlugin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN.ID).registerLicenseCheckResultsGenerator(checkLicense); - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/index.ts deleted file mode 100644 index 82fb2e3b2a372..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/index.ts +++ /dev/null @@ -1,7 +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 { registerIndexRoutes } from './register_index_routes'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts deleted file mode 100644 index c3e235220931c..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts +++ /dev/null @@ -1,58 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function addLifecyclePolicy( - callWithRequest: any, - indexName: string, - policyName: string, - alias: string -) { - const body = { - lifecycle: { - name: policyName, - rollover_alias: alias, - }, - }; - - const params = { - method: 'PUT', - path: `/${encodeURIComponent(indexName)}/_settings`, - body, - }; - - return callWithRequest('transport.request', params); -} - -export function registerAddPolicyRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/index/add', - method: 'POST', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - const { indexName, policyName, alias } = request.payload as any; - try { - const response = await addLifecyclePolicy(callWithRequest, indexName, policyName, alias); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts deleted file mode 100644 index ed3b5a97a3b42..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts +++ /dev/null @@ -1,51 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function removeLifecycle(callWithRequest: any, indexNames: string[]) { - const responses = []; - for (let i = 0; i < indexNames.length; i++) { - const indexName = indexNames[i]; - const params = { - method: 'POST', - path: `/${encodeURIComponent(indexName)}/_ilm/remove`, - ignore: [404], - }; - - responses.push(callWithRequest('transport.request', params)); - } - return Promise.all(responses); -} - -export function registerRemoveRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/index/remove', - method: 'POST', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const response = await removeLifecycle(callWithRequest, request.payload.indexNames); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts deleted file mode 100644 index 89278edbecea2..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts +++ /dev/null @@ -1,51 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function retryLifecycle(callWithRequest: any, indexNames: string[]) { - const responses = []; - for (let i = 0; i < indexNames.length; i++) { - const indexName = indexNames[i]; - const params = { - method: 'POST', - path: `/${encodeURIComponent(indexName)}/_ilm/retry`, - ignore: [404], - }; - - responses.push(callWithRequest('transport.request', params)); - } - return Promise.all(responses); -} - -export function registerRetryRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/index/retry', - method: 'POST', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const response = await retryLifecycle(callWithRequest, request.payload.indexNames); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/constants.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/constants.ts deleted file mode 100644 index 4392dacac8fa4..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const NODE_ATTRS_KEYS_TO_IGNORE: string[] = [ - 'ml.enabled', - 'ml.machine_memory', - 'ml.max_open_jobs', - // Used by ML to identify nodes that have transform enabled: - // https://github.com/elastic/elasticsearch/pull/52712/files#diff-225cc2c1291b4c60a8c3412a619094e1R147 - 'transform.node', - 'xpack.installed', -]; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/index.ts deleted file mode 100644 index ef0ac271ae60e..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/index.ts +++ /dev/null @@ -1,7 +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 { registerNodesRoutes } from './register_nodes_routes'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts deleted file mode 100644 index c2c3f8bf07028..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts +++ /dev/null @@ -1,61 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function findMatchingNodes(stats: any, nodeAttrs: string): any { - return Object.entries(stats.nodes).reduce((accum: any[], [nodeId, nodeStats]: [any, any]) => { - const attributes = nodeStats.attributes || {}; - for (const [key, value] of Object.entries(attributes)) { - if (`${key}:${value}` === nodeAttrs) { - accum.push({ - nodeId, - stats: nodeStats, - }); - break; - } - } - return accum; - }, []); -} - -async function fetchNodeStats(callWithRequest: any): Promise { - const params = { - format: 'json', - }; - - return await callWithRequest('nodes.stats', params); -} - -export function registerDetailsRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/nodes/{nodeAttrs}/details', - method: 'GET', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const stats = await fetchNodeStats(callWithRequest); - const response = findMatchingNodes(stats, request.params.nodeAttrs); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts deleted file mode 100644 index edbe4289ed83c..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts +++ /dev/null @@ -1,63 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -import { NODE_ATTRS_KEYS_TO_IGNORE } from './constants'; - -function convertStatsIntoList(stats: any, attributesToBeFiltered: string[]): any { - return Object.entries(stats.nodes).reduce((accum: any, [nodeId, nodeStats]: [any, any]) => { - const attributes = nodeStats.attributes || {}; - for (const [key, value] of Object.entries(attributes)) { - if (!attributesToBeFiltered.includes(key)) { - const attributeString = `${key}:${value}`; - accum[attributeString] = accum[attributeString] || []; - accum[attributeString].push(nodeId); - } - } - return accum; - }, {}); -} - -async function fetchNodeStats(callWithRequest: any): Promise { - const params = { - format: 'json', - }; - - return await callWithRequest('nodes.stats', params); -} - -export function registerListRoute(server: any) { - const config = server.config(); - const filteredNodeAttributes = config.get('xpack.ilm.filteredNodeAttributes'); - const attributesToBeFiltered = [...NODE_ATTRS_KEYS_TO_IGNORE, ...filteredNodeAttributes]; - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/nodes/list', - method: 'GET', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const stats = await fetchNodeStats(callWithRequest); - const response = convertStatsIntoList(stats, attributesToBeFiltered); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/index.ts deleted file mode 100644 index 7c6103a3389ab..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/index.ts +++ /dev/null @@ -1,7 +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 { registerPoliciesRoutes } from './register_policies_routes'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts deleted file mode 100644 index f6bc96dd498a4..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ /dev/null @@ -1,52 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function createPolicy(callWithRequest: any, policy: any): Promise { - const body = { - policy: { - phases: policy.phases, - }, - }; - const params = { - method: 'PUT', - path: `/_ilm/policy/${encodeURIComponent(policy.name)}`, - ignore: [404], - body, - }; - - return await callWithRequest('transport.request', params); -} - -export function registerCreateRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/policies', - method: 'POST', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const response = await createPolicy(callWithRequest, request.payload); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts deleted file mode 100644 index c84f2efd92d8f..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function deletePolicies(policyNames: string, callWithRequest: any): Promise { - const params = { - method: 'DELETE', - path: `/_ilm/policy/${encodeURIComponent(policyNames)}`, - // we allow 404 since they may have no policies - ignore: [404], - }; - - return await callWithRequest('transport.request', params); -} - -export function registerDeleteRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/policies/{policyNames}', - method: 'DELETE', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - const { policyNames } = request.params; - try { - await deletePolicies(policyNames, callWithRequest); - return {}; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts deleted file mode 100644 index c65f849a47d87..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts +++ /dev/null @@ -1,84 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function formatPolicies(policiesMap: any): any { - if (policiesMap.status === 404) { - return []; - } - - return Object.keys(policiesMap).reduce((accum: any[], lifecycleName: string) => { - const policyEntry = policiesMap[lifecycleName]; - accum.push({ - ...policyEntry, - name: lifecycleName, - }); - return accum; - }, []); -} - -async function fetchPolicies(callWithRequest: any): Promise { - const params = { - method: 'GET', - path: '/_ilm/policy', - // we allow 404 since they may have no policies - ignore: [404], - }; - - return await callWithRequest('transport.request', params); -} -async function addLinkedIndices(policiesMap: any, callWithRequest: any) { - if (policiesMap.status === 404) { - return policiesMap; - } - const params = { - method: 'GET', - path: '/*/_ilm/explain', - // we allow 404 since they may have no policies - ignore: [404], - }; - - const policyExplanation: any = await callWithRequest('transport.request', params); - Object.entries(policyExplanation.indices).forEach(([indexName, { policy }]: [string, any]) => { - if (policy && policiesMap[policy]) { - policiesMap[policy].linkedIndices = policiesMap[policy].linkedIndices || []; - policiesMap[policy].linkedIndices.push(indexName); - } - }); -} - -export function registerFetchRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/policies', - method: 'GET', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - const { withIndices } = request.query; - try { - const policiesMap = await fetchPolicies(callWithRequest); - if (withIndices) { - await addLinkedIndices(policiesMap, callWithRequest); - } - return formatPolicies(policiesMap); - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/index.ts deleted file mode 100644 index dc9a0acaaf09b..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/index.ts +++ /dev/null @@ -1,7 +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 { registerTemplatesRoutes } from './register_templates_routes'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts deleted file mode 100644 index 57e5a91f60f5b..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts +++ /dev/null @@ -1,67 +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 { merge } from 'lodash'; - -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function getIndexTemplate(callWithRequest: any, templateName: string): Promise { - const response = await callWithRequest('indices.getTemplate', { name: templateName }); - return response[templateName]; -} - -async function updateIndexTemplate(callWithRequest: any, indexTemplatePatch: any): Promise { - // Fetch existing template - const template = await getIndexTemplate(callWithRequest, indexTemplatePatch.templateName); - merge(template, { - settings: { - index: { - lifecycle: { - name: indexTemplatePatch.policyName, - rollover_alias: indexTemplatePatch.aliasName, - }, - }, - }, - }); - - const params = { - method: 'PUT', - path: `/_template/${encodeURIComponent(indexTemplatePatch.templateName)}`, - ignore: [404], - body: template, - }; - - return await callWithRequest('transport.request', params); -} - -export function registerAddPolicyRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/template', - method: 'POST', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const response = await updateIndexTemplate(callWithRequest, request.payload); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_get_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_get_route.ts deleted file mode 100644 index 3edaea6e15818..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_get_route.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function fetchTemplate(callWithRequest: any, templateName: string): Promise { - const params = { - method: 'GET', - path: `/_template/${encodeURIComponent(templateName)}`, - // we allow 404 incase the user shutdown security in-between the check and now - ignore: [404], - }; - - return await callWithRequest('transport.request', params); -} - -export function registerGetRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/templates/{templateName}', - method: 'GET', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - const templateName = request.params.templateName; - - try { - const template = await fetchTemplate(callWithRequest, templateName); - return template[templateName]; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/shim.ts b/x-pack/legacy/plugins/index_lifecycle_management/shim.ts deleted file mode 100644 index 18b3d9ef28b6a..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/shim.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Legacy } from 'kibana'; - -export interface LegacySetup { - server: Legacy.Server; -} - -export function createShim(server: Legacy.Server): LegacySetup { - return { - server, - }; -} diff --git a/x-pack/legacy/plugins/index_management/index.ts b/x-pack/legacy/plugins/index_management/index.ts index 9eba98a526d2b..afca15203b970 100644 --- a/x-pack/legacy/plugins/index_management/index.ts +++ b/x-pack/legacy/plugins/index_management/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// TODO: Remove this once CCR is migrated to the plugins directory. export function indexManagement(kibana: any) { return new kibana.Plugin({ id: 'index_management', diff --git a/x-pack/legacy/plugins/rollup/public/plugin.ts b/x-pack/legacy/plugins/rollup/public/plugin.ts index 5782e88c3448b..17ec8a5a4aedf 100644 --- a/x-pack/legacy/plugins/rollup/public/plugin.ts +++ b/x-pack/legacy/plugins/rollup/public/plugin.ts @@ -24,7 +24,7 @@ import { // @ts-ignore import { CRUD_APP_BASE_PATH } from './crud_app/constants'; import { ManagementSetup } from '../../../../../src/plugins/management/public'; -import { IndexMgmtSetup } from '../../../../plugins/index_management/public'; +import { IndexManagementPluginSetup } from '../../../../plugins/index_management/public'; import { IndexPatternManagementSetup } from '../../../../../src/plugins/index_pattern_management/public'; import { search } from '../../../../../src/plugins/data/public'; // @ts-ignore @@ -35,7 +35,7 @@ import { renderApp } from './application'; export interface RollupPluginSetupDependencies { home?: HomePublicPluginSetup; management: ManagementSetup; - indexManagement?: IndexMgmtSetup; + indexManagement?: IndexManagementPluginSetup; indexPatternManagement: IndexPatternManagementSetup; } diff --git a/x-pack/legacy/plugins/rollup/server/plugin.ts b/x-pack/legacy/plugins/rollup/server/plugin.ts index 090cb8a47377a..5f29ad160e052 100644 --- a/x-pack/legacy/plugins/rollup/server/plugin.ts +++ b/x-pack/legacy/plugins/rollup/server/plugin.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; -import { IndexMgmtSetup } from '../../../../plugins/index_management/server'; +import { IndexManagementPluginSetup } from '../../../../plugins/index_management/server'; import { registerLicenseChecker } from '../../../server/lib/register_license_checker'; import { PLUGIN } from '../common'; import { ServerShim, RouteDependencies } from './types'; @@ -44,7 +44,7 @@ export class RollupsServerPlugin implements Plugin { __LEGACY: ServerShim; usageCollection?: UsageCollectionSetup; metrics?: VisTypeTimeseriesSetup; - indexManagement?: IndexMgmtSetup; + indexManagement?: IndexManagementPluginSetup; } ) { const elasticsearch = await elasticsearchService.adminClient; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap rename to x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap rename to x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js similarity index 96% rename from x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js rename to x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js index 9d143c4d3fc8e..bf4de823f1833 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js @@ -13,13 +13,13 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import sinon from 'sinon'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { mountWithIntl } from '../../../../../test_utils/enzyme_helpers'; -import { fetchedPolicies, fetchedNodes } from '../../public/np_ready/application/store/actions'; -import { indexLifecycleManagementStore } from '../../public/np_ready/application/store'; -import { EditPolicy } from '../../public/np_ready/application/sections/edit_policy'; -import { init as initHttp } from '../../public/np_ready/application/services/http'; -import { init as initUiMetric } from '../../public/np_ready/application/services/ui_metric'; -import { init as initNotification } from '../../public/np_ready/application/services/notification'; +import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; +import { fetchedPolicies, fetchedNodes } from '../../public/application/store/actions'; +import { indexLifecycleManagementStore } from '../../public/application/store'; +import { EditPolicy } from '../../public/application/sections/edit_policy'; +import { init as initHttp } from '../../public/application/services/http'; +import { init as initUiMetric } from '../../public/application/services/ui_metric'; +import { init as initNotification } from '../../public/application/services/notification'; import { positiveNumbersAboveZeroErrorMessage, positiveNumberRequiredMessage, @@ -33,16 +33,14 @@ import { policyNameMustBeDifferentErrorMessage, policyNameAlreadyUsedErrorMessage, maximumDocumentsRequiredMessage, -} from '../../public/np_ready/application/store/selectors/lifecycle'; +} from '../../public/application/store/selectors/lifecycle'; initHttp(axios.create({ adapter: axiosXhrAdapter }), path => path); -initUiMetric(() => () => {}); +initUiMetric({ reportUiStats: () => {} }); initNotification({ addDanger: () => {}, }); -jest.mock('ui/new_platform'); - let server; let store; const policy = { diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js similarity index 91% rename from x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js rename to x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js index a3a9c5e59bfa4..78c5c181eea62 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js @@ -12,17 +12,15 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import sinon from 'sinon'; import { findTestSubject, takeMountedSnapshot } from '@elastic/eui/lib/test'; -import { mountWithIntl } from '../../../../../test_utils/enzyme_helpers'; -import { fetchedPolicies } from '../../public/np_ready/application/store/actions'; -import { indexLifecycleManagementStore } from '../../public/np_ready/application/store'; -import { PolicyTable } from '../../public/np_ready/application/sections/policy_table'; -import { init as initHttp } from '../../public/np_ready/application/services/http'; -import { init as initUiMetric } from '../../public/np_ready/application/services/ui_metric'; +import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; +import { fetchedPolicies } from '../../public/application/store/actions'; +import { indexLifecycleManagementStore } from '../../public/application/store'; +import { PolicyTable } from '../../public/application/sections/policy_table'; +import { init as initHttp } from '../../public/application/services/http'; +import { init as initUiMetric } from '../../public/application/services/ui_metric'; initHttp(axios.create({ adapter: axiosXhrAdapter }), path => path); -initUiMetric(() => () => {}); - -jest.mock('ui/new_platform'); +initUiMetric({ reportUiStats: () => {} }); let server = null; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js similarity index 93% rename from x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js rename to x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js index d2619778617c3..900de27ca36ab 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js @@ -8,7 +8,7 @@ import moment from 'moment-timezone'; import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; +import { mountWithIntl } from '../../../test_utils/enzyme_helpers'; import { retryLifecycleActionExtension, removeLifecyclePolicyActionExtension, @@ -16,21 +16,18 @@ import { ilmBannerExtension, ilmFilterExtension, ilmSummaryExtension, -} from '../public/np_ready/extend_index_management'; -import { init as initHttp } from '../public/np_ready/application/services/http'; -import { init as initUiMetric } from '../public/np_ready/application/services/ui_metric'; +} from '../public/extend_index_management'; +import { init as initHttp } from '../public/application/services/http'; +import { init as initUiMetric } from '../public/application/services/ui_metric'; // We need to init the http with a mock for any tests that depend upon the http service. // For example, add_lifecycle_confirm_modal makes an API request in its componentDidMount // lifecycle method. If we don't mock this, CI will fail with "Call retries were exceeded". initHttp(axios.create({ adapter: axiosXhrAdapter }), path => path); -initUiMetric(() => () => {}); +initUiMetric({ reportUiStats: () => {} }); -jest.mock('ui/new_platform'); -jest.mock('../../../../plugins/index_management/public', async () => { - const { indexManagementMock } = await import( - '../../../../plugins/index_management/public/mocks.ts' - ); +jest.mock('../../../plugins/index_management/public', async () => { + const { indexManagementMock } = await import('../../../plugins/index_management/public/mocks.ts'); return indexManagementMock.createSetup(); }); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/common/constants/index.ts b/x-pack/plugins/index_lifecycle_management/common/constants/index.ts similarity index 72% rename from x-pack/legacy/plugins/index_lifecycle_management/common/constants/index.ts rename to x-pack/plugins/index_lifecycle_management/common/constants/index.ts index 9193efb561a0f..700039985eaf5 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/common/constants/index.ts +++ b/x-pack/plugins/index_lifecycle_management/common/constants/index.ts @@ -5,12 +5,18 @@ */ import { i18n } from '@kbn/i18n'; +import { LicenseType } from '../../../licensing/common/types'; + +const basicLicense: LicenseType = 'basic'; export const PLUGIN = { ID: 'index_lifecycle_management', + minimumLicenseType: basicLicense, TITLE: i18n.translate('xpack.indexLifecycleMgmt.appTitle', { defaultMessage: 'Index Lifecycle Policies', }), }; export const BASE_PATH = '/management/elasticsearch/index_lifecycle_management/'; + +export const API_BASE_PATH = '/api/index_lifecycle_management'; diff --git a/x-pack/plugins/index_lifecycle_management/kibana.json b/x-pack/plugins/index_lifecycle_management/kibana.json new file mode 100644 index 0000000000000..6385646b95789 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/kibana.json @@ -0,0 +1,16 @@ +{ + "id": "indexLifecycleManagement", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": [ + "home", + "licensing", + "management" + ], + "optionalPlugins": [ + "usageCollection", + "indexManagement" + ], + "configPath": ["xpack", "ilm"] +} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/app.tsx b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx similarity index 94% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/app.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/app.tsx index 6738d7caa4444..993dced20bbe6 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/app.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx @@ -8,7 +8,7 @@ import React, { useEffect } from 'react'; import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; import { METRIC_TYPE } from '@kbn/analytics'; -import { BASE_PATH } from '../../../common/constants'; +import { BASE_PATH } from '../../common/constants'; import { UIM_APP_LOAD } from './constants'; import { EditPolicy } from './sections/edit_policy'; import { PolicyTable } from './sections/policy_table'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/constants/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/constants/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/constants/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/ui_metric.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/constants/ui_metric.ts rename to x-pack/plugins/index_lifecycle_management/public/application/constants/ui_metric.ts diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx new file mode 100644 index 0000000000000..a7d88d31e58fc --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { Provider } from 'react-redux'; +import { I18nStart } from 'kibana/public'; +import { UnmountCallback } from 'src/core/public'; + +import { App } from './app'; +import { indexLifecycleManagementStore } from './store'; + +export const renderApp = (element: Element, I18nContext: I18nStart['Context']): UnmountCallback => { + render( + + + + + , + element + ); + + return () => unmountComponentAtNode(element); +}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/active_badge.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/active_badge.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/active_badge.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/components/active_badge.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/components/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/learn_more_link.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/learn_more_link.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/optional_label.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/optional_label.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/optional_label.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/components/optional_label.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/phase_error_message.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/phase_error_message.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/phase_error_message.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/components/phase_error_message.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/cold_phase/cold_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/cold_phase/cold_phase.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/cold_phase/cold_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/cold_phase/cold_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/cold_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/cold_phase/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/delete_phase/delete_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/delete_phase/delete_phase.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/delete_phase/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/delete_phase/delete_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/delete_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/delete_phase/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/hot_phase/hot_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/hot_phase/hot_phase.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/hot_phase/hot_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/hot_phase/hot_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/hot_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/hot_phase/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/min_age_input.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/min_age_input.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_allocation/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_allocation/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_allocation/node_allocation.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_allocation/node_allocation.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_allocation/node_allocation.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_allocation/node_allocation.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_attrs_details/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_attrs_details/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/policy_json_flyout.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/policy_json_flyout.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/set_priority_input.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/set_priority_input.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/warm_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/warm_phase/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/warm_phase/warm_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/warm_phase/warm_phase.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/warm_phase/warm_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/warm_phase/warm_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/edit_policy.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/edit_policy.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/edit_policy.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/edit_policy.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/form_errors.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/form_errors.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/index.d.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/no_match/components/no_match/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/no_match/components/no_match/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/no_match/components/no_match/no_match.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/no_match.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/no_match/components/no_match/no_match.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/no_match.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/no_match/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/no_match/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/confirm_delete.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/confirm_delete.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/confirm_delete.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/confirm_delete.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/policy_table.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/policy_table.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/policy_table.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js similarity index 98% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/policy_table.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js index 903161fe094fc..d406d86bc6ce7 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/policy_table.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js @@ -37,8 +37,8 @@ import { import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; -import { getIndexListUri } from '../../../../../../../../../../plugins/index_management/public'; -import { BASE_PATH } from '../../../../../../../common/constants'; +import { getIndexListUri } from '../../../../../../../index_management/public'; +import { BASE_PATH } from '../../../../../../common/constants'; import { UIM_EDIT_CLICK } from '../../../../constants'; import { getPolicyPath } from '../../../../services/navigation'; import { flattenPanelTree } from '../../../../services/flatten_panel_tree'; @@ -52,6 +52,7 @@ const COLUMNS = { label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.nameHeader', { defaultMessage: 'Name', }), + width: 200, }, linkedIndices: { label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.linkedIndicesHeader', { @@ -179,7 +180,6 @@ export class PolicyTable extends Component { return ( /* eslint-disable-next-line @elastic/eui/href-or-on-click */ trackUiMetric('click', UIM_EDIT_CLICK)} @@ -415,7 +415,7 @@ export class PolicyTable extends Component { tableContent = ; } else if (totalNumberOfPolicies > 0) { tableContent = ( - + { + const response = await sendPost(`index/retry`, { indexNames }); + // Only track successful actions. + trackUiMetric('count', UIM_INDEX_RETRY_STEP); + return response; +}; + +export const removeLifecycleForIndex = async indexNames => { + const response = await sendPost(`index/remove`, { indexNames }); + // Only track successful actions. + trackUiMetric('count', UIM_POLICY_DETACH_INDEX); + return response; +}; + +export const addLifecyclePolicyToIndex = async body => { + const response = await sendPost(`index/add`, body); + // Only track successful actions. + trackUiMetric('count', UIM_POLICY_ATTACH_INDEX); + return response; +}; + +export const addLifecyclePolicyToTemplate = async body => { + const response = await sendPost(`template`, body); + // Only track successful actions. + trackUiMetric('count', UIM_POLICY_ATTACH_INDEX_TEMPLATE); + return response; +}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/api_errors.js b/x-pack/plugins/index_lifecycle_management/public/application/services/api_errors.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/api_errors.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/api_errors.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/documentation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/documentation.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/documentation.ts rename to x-pack/plugins/index_lifecycle_management/public/application/services/documentation.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/filter_items.js b/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/filter_items.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/find_errors.js b/x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/find_errors.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/flatten_panel_tree.js b/x-pack/plugins/index_lifecycle_management/public/application/services/flatten_panel_tree.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/flatten_panel_tree.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/flatten_panel_tree.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/http.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts similarity index 50% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/http.ts rename to x-pack/plugins/index_lifecycle_management/public/application/services/http.ts index bbda1ebd2e0e5..47e96ea28bb8c 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/http.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts @@ -20,16 +20,14 @@ function getFullPath(path: string): string { return apiPrefix; } -// The extend_index_management module requires that we support an injected httpClient here. - -export function sendPost(path: string, payload: any, httpClient = _httpClient): any { - return httpClient.post(getFullPath(path), { body: JSON.stringify(payload) }); +export function sendPost(path: string, payload: any): any { + return _httpClient.post(getFullPath(path), { body: JSON.stringify(payload) }); } -export function sendGet(path: string, query: any, httpClient = _httpClient): any { - return httpClient.get(getFullPath(path), { query }); +export function sendGet(path: string, query: any): any { + return _httpClient.get(getFullPath(path), { query }); } -export function sendDelete(path: string, httpClient = _httpClient): any { - return httpClient.delete(getFullPath(path)); +export function sendDelete(path: string): any { + return _httpClient.delete(getFullPath(path)); } diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/index.js b/x-pack/plugins/index_lifecycle_management/public/application/services/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/navigation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/navigation.ts similarity index 59% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/navigation.ts rename to x-pack/plugins/index_lifecycle_management/public/application/services/navigation.ts index 943f9a49d0ab6..2d518ebb3015e 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/navigation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/navigation.ts @@ -4,18 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BASE_PATH } from '../../../../common/constants'; - -// This depends upon Angular, which is why we use this provider pattern to access it within -// our React app. -let _redirect: any; - -export function init(redirect: any) { - _redirect = redirect; -} +import { BASE_PATH } from '../../../common/constants'; export const goToPolicyList = () => { - _redirect(`${BASE_PATH}policies`); + window.location.hash = `${BASE_PATH}policies`; }; export const getPolicyPath = (policyName: string): string => { diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/notification.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/notification.ts rename to x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/sort_table.js b/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/sort_table.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/ui_metric.test.js b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/ui_metric.test.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts similarity index 85% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/ui_metric.ts rename to x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts index d9f2c26048317..ca6c0b44d5804 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/ui_metric.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts @@ -6,7 +6,8 @@ import { get } from 'lodash'; -import { createUiStatsReporter } from '../../../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { UiStatsMetricType } from '@kbn/analytics'; import { UIM_APP_NAME, @@ -22,12 +23,10 @@ import { import { defaultColdPhase, defaultWarmPhase, defaultHotPhase } from '../store/defaults'; -export let trackUiMetric: ReturnType; +export let trackUiMetric: (metricType: UiStatsMetricType, eventName: string) => void; -export function init(getReporter: typeof createUiStatsReporter): void { - if (getReporter) { - trackUiMetric = getReporter(UIM_APP_NAME); - } +export function init(usageCollection: UsageCollectionSetup): void { + trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, UIM_APP_NAME); } export function getUiMetricsForPhases(phases: any): any { diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/general.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/lifecycle.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/nodes.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/policies.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/cold_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/cold_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/delete_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/hot_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/hot_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/index.d.ts rename to x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/warm_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/warm_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/store/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/index.d.ts rename to x-pack/plugins/index_lifecycle_management/public/application/store/index.d.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/general.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/nodes.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/policies.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/general.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/lifecycle.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/nodes.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/policies.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/store.js b/x-pack/plugins/index_lifecycle_management/public/application/store/store.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/store.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/store.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/add_lifecycle_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js similarity index 97% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/add_lifecycle_confirm_modal.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js index 5b8f2d197daf4..143895150172d 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/add_lifecycle_confirm_modal.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js @@ -23,7 +23,7 @@ import { EuiModalHeaderTitle, } from '@elastic/eui'; -import { BASE_PATH } from '../../../../common/constants'; +import { BASE_PATH } from '../../../common/constants'; import { loadPolicies, addLifecyclePolicyToIndex } from '../../application/services/api'; import { showApiError } from '../../application/services/api_errors'; import { toasts } from '../../application/services/notification'; @@ -38,7 +38,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { }; } addPolicy = async () => { - const { indexName, httpClient, closeModal, reloadIndices } = this.props; + const { indexName, closeModal, reloadIndices } = this.props; const { selectedPolicyName, selectedAlias } = this.state; if (!selectedPolicyName) { this.setState({ @@ -55,7 +55,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { policyName: selectedPolicyName, alias: selectedAlias, }; - await addLifecyclePolicyToIndex(body, httpClient); + await addLifecyclePolicyToIndex(body); closeModal(); toasts.addSuccess( i18n.translate( diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/index_lifecycle_summary.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/index_lifecycle_summary.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/remove_lifecycle_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.js similarity index 96% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/remove_lifecycle_confirm_modal.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.js index 0ba5ed1720084..4e0d2383c7d79 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/remove_lifecycle_confirm_modal.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.js @@ -24,10 +24,10 @@ export class RemoveLifecyclePolicyConfirmModal extends Component { } removePolicy = async () => { - const { indexNames, httpClient, closeModal, reloadIndices } = this.props; + const { indexNames, closeModal, reloadIndices } = this.props; try { - await removeLifecycleForIndex(indexNames, httpClient); + await removeLifecycleForIndex(indexNames); closeModal(); toasts.addSuccess( i18n.translate( diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.d.ts rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js similarity index 80% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js index 69658d31695bc..40ff04408002f 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js @@ -9,8 +9,6 @@ import { get, every, any } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiSearchBar } from '@elastic/eui'; -import { init as initUiMetric } from '../application/services/ui_metric'; -import { init as initNotification } from '../application/services/notification'; import { retryLifecycleForIndex } from '../application/services/api'; import { IndexLifecycleSummary } from './components/index_lifecycle_summary'; import { AddLifecyclePolicyConfirmModal } from './components/add_lifecycle_confirm_modal'; @@ -18,21 +16,7 @@ import { RemoveLifecyclePolicyConfirmModal } from './components/remove_lifecycle const stepPath = 'ilm.step'; -export const retryLifecycleActionExtension = ({ - indices, - usageCollection, - toasts, - fatalErrors, -}) => { - // These are hacks that we can remove once the New Platform migration is done. They're needed here - // because API requests and API errors require them. - const getLegacyReporter = appName => (type, name) => { - usageCollection.reportUiStats(appName, type, name); - }; - - initUiMetric(getLegacyReporter); - initNotification(toasts, fatalErrors); - +export const retryLifecycleActionExtension = ({ indices }) => { const allHaveErrors = every(indices, index => { return index.ilm && index.ilm.failed_step; }); @@ -57,19 +41,7 @@ export const retryLifecycleActionExtension = ({ }; }; -export const removeLifecyclePolicyActionExtension = ({ - indices, - reloadIndices, - createUiStatsReporter, - toasts, - fatalErrors, - httpClient, -}) => { - // These are hacks that we can remove once the New Platform migration is done. They're needed here - // because API requests and API errors require them. - initUiMetric(createUiStatsReporter); - initNotification(toasts, fatalErrors); - +export const removeLifecyclePolicyActionExtension = ({ indices, reloadIndices }) => { const allHaveIlm = every(indices, index => { return index.ilm && index.ilm.managed; }); @@ -83,8 +55,6 @@ export const removeLifecyclePolicyActionExtension = ({ ); @@ -97,19 +67,7 @@ export const removeLifecyclePolicyActionExtension = ({ }; }; -export const addLifecyclePolicyActionExtension = ({ - indices, - reloadIndices, - createUiStatsReporter, - toasts, - fatalErrors, - httpClient, -}) => { - // These are hacks that we can remove once the New Platform migration is done. They're needed here - // because API requests and API errors require them. - initUiMetric(createUiStatsReporter); - initNotification(toasts, fatalErrors); - +export const addLifecyclePolicyActionExtension = ({ indices, reloadIndices }) => { if (indices.length !== 1) { return null; } @@ -126,8 +84,6 @@ export const addLifecyclePolicyActionExtension = ({ diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/index.ts b/x-pack/plugins/index_lifecycle_management/public/index.ts similarity index 58% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/index.ts rename to x-pack/plugins/index_lifecycle_management/public/index.ts index 1af0b697a9283..586763188a54b 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/index.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/public'; +import { PluginInitializerContext } from 'kibana/public'; + import { IndexLifecycleManagementPlugin } from './plugin'; -export const createPlugin = (ctx: PluginInitializerContext) => new IndexLifecycleManagementPlugin(); +/** @public */ +export const plugin = (initializerContext: PluginInitializerContext) => { + return new IndexLifecycleManagementPlugin(initializerContext); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx new file mode 100644 index 0000000000000..ca93646e20fcf --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.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 { CoreSetup, PluginInitializerContext } from 'src/core/public'; + +import { PLUGIN } from '../common/constants'; +import { init as initHttp } from './application/services/http'; +import { init as initDocumentation } from './application/services/documentation'; +import { init as initUiMetric } from './application/services/ui_metric'; +import { init as initNotification } from './application/services/notification'; +import { addAllExtensions } from './extend_index_management'; +import { PluginsDependencies, ClientConfigType } from './types'; + +export class IndexLifecycleManagementPlugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup(coreSetup: CoreSetup, plugins: PluginsDependencies) { + const { + ui: { enabled: isIndexLifecycleManagementUiEnabled }, + } = this.initializerContext.config.get(); + + if (isIndexLifecycleManagementUiEnabled) { + const { + http, + notifications: { toasts }, + fatalErrors, + getStartServices, + } = coreSetup; + + const { usageCollection, management, indexManagement } = plugins; + + // Initialize services even if the app isn't mounted, because they're used by index management extensions. + initHttp(http); + initUiMetric(usageCollection); + initNotification(toasts, fatalErrors); + + management.sections.getSection('elasticsearch')!.registerApp({ + id: PLUGIN.ID, + title: PLUGIN.TITLE, + order: 2, + mount: async ({ element }) => { + const [coreStart] = await getStartServices(); + const { + i18n: { Context: I18nContext }, + docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + } = coreStart; + + // Initialize additional services. + initDocumentation( + `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/` + ); + + const { renderApp } = await import('./application'); + return renderApp(element, I18nContext); + }, + }); + + if (indexManagement) { + addAllExtensions(indexManagement.extensionsService); + } + } + } + + public start() {} + public stop() {} +} diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts new file mode 100644 index 0000000000000..f9e0abae56cb4 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/types.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 { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; +import { IndexManagementPluginSetup } from '../../index_management/public'; + +export interface PluginsDependencies { + usageCollection: UsageCollectionSetup; + management: ManagementSetup; + indexManagement?: IndexManagementPluginSetup; +} + +export interface ClientConfigType { + ui: { + enabled: boolean; + }; +} diff --git a/x-pack/plugins/index_lifecycle_management/server/config.ts b/x-pack/plugins/index_lifecycle_management/server/config.ts new file mode 100644 index 0000000000000..9728e31a8a148 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + // Cloud requires the ability to hide internal node attributes from users. + filteredNodeAttributes: schema.arrayOf(schema.string(), { defaultValue: [] }), +}); + +export type IndexLifecycleManagementConfig = TypeOf; diff --git a/x-pack/plugins/index_lifecycle_management/server/index.ts b/x-pack/plugins/index_lifecycle_management/server/index.ts new file mode 100644 index 0000000000000..8a5f0fe19f9b0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/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. + */ + +import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; +import { IndexLifecycleManagementServerPlugin } from './plugin'; +import { configSchema, IndexLifecycleManagementConfig } from './config'; + +export const plugin = (ctx: PluginInitializerContext) => + new IndexLifecycleManagementServerPlugin(ctx); + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + ui: true, + }, +}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/is_es_error/is_es_error.ts b/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/server/lib/is_es_error/is_es_error.ts rename to x-pack/plugins/index_lifecycle_management/server/lib/is_es_error.ts diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts new file mode 100644 index 0000000000000..48c50f9a48ee5 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup, Plugin, Logger, PluginInitializerContext, APICaller } from 'src/core/server'; + +import { PLUGIN } from '../common/constants'; +import { Dependencies } from './types'; +import { registerApiRoutes } from './routes'; +import { License } from './services'; +import { IndexLifecycleManagementConfig } from './config'; +import { isEsError } from './lib/is_es_error'; + +const indexLifecycleDataEnricher = async (indicesList: any, callAsCurrentUser: APICaller) => { + if (!indicesList || !indicesList.length) { + return; + } + + const params = { + path: '/*/_ilm/explain', + method: 'GET', + }; + + const { indices: ilmIndicesData } = await callAsCurrentUser('transport.request', params); + + return indicesList.map((index: any): any => { + return { + ...index, + ilm: { ...(ilmIndicesData[index.name] || {}) }, + }; + }); +}; + +export class IndexLifecycleManagementServerPlugin implements Plugin { + private readonly config$: Observable; + private readonly license: License; + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + this.config$ = initializerContext.config.create(); + this.license = new License(); + } + + async setup({ http }: CoreSetup, { licensing, indexManagement }: Dependencies): Promise { + const router = http.createRouter(); + const config = await this.config$.pipe(first()).toPromise(); + + this.license.setup( + { + pluginId: PLUGIN.ID, + minimumLicenseType: PLUGIN.minimumLicenseType, + defaultErrorMessage: i18n.translate('xpack.indexLifecycleMgmt.licenseCheckErrorMessage', { + defaultMessage: 'License check failed', + }), + }, + { + licensing, + logger: this.logger, + } + ); + + registerApiRoutes({ + router, + config, + license: this.license, + lib: { + isEsError, + }, + }); + + if (config.ui.enabled) { + if (indexManagement.indexDataEnricher) { + indexManagement.indexDataEnricher.add(indexLifecycleDataEnricher); + } + } + } + + start() {} + stop() {} +} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_index_routes.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/index.ts similarity index 65% rename from x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_index_routes.ts rename to x-pack/plugins/index_lifecycle_management/server/routes/api/index/index.ts index 74eb1a86a93ba..abe00af74b63a 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_index_routes.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/index.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RouteDependencies } from '../../../types'; import { registerRetryRoute } from './register_retry_route'; import { registerRemoveRoute } from './register_remove_route'; import { registerAddPolicyRoute } from './register_add_policy_route'; -export function registerIndexRoutes(server: any) { - registerRetryRoute(server); - registerRemoveRoute(server); - registerAddPolicyRoute(server); +export function registerIndexRoutes(dependencies: RouteDependencies) { + registerRetryRoute(dependencies); + registerRemoveRoute(dependencies); + registerAddPolicyRoute(dependencies); } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts new file mode 100644 index 0000000000000..9627f6399eaaf --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function addLifecyclePolicy( + callAsCurrentUser: APICaller, + indexName: string, + policyName: string, + alias: string +) { + const params = { + method: 'PUT', + path: `/${encodeURIComponent(indexName)}/_settings`, + body: { + lifecycle: { + name: policyName, + rollover_alias: alias, + }, + }, + }; + + return callAsCurrentUser('transport.request', params); +} + +const bodySchema = schema.object({ + indexName: schema.string(), + policyName: schema.string(), + alias: schema.maybe(schema.string()), +}); + +export function registerAddPolicyRoute({ router, license, lib }: RouteDependencies) { + router.post( + { path: addBasePath('/index/add'), validate: { body: bodySchema } }, + license.guardApiRoute(async (context, request, response) => { + const body = request.body as typeof bodySchema.type; + const { indexName, policyName, alias = '' } = body; + + try { + await addLifecyclePolicy( + context.core.elasticsearch.dataClient.callAsCurrentUser, + indexName, + policyName, + alias + ); + return response.ok(); + } catch (e) { + if (lib.isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts new file mode 100644 index 0000000000000..8ec94a8591785 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function removeLifecycle(callAsCurrentUser: APICaller, indexNames: string[]) { + const responses = []; + for (let i = 0; i < indexNames.length; i++) { + const indexName = indexNames[i]; + const params = { + method: 'POST', + path: `/${encodeURIComponent(indexName)}/_ilm/remove`, + ignore: [404], + }; + + responses.push(callAsCurrentUser('transport.request', params)); + } + return Promise.all(responses); +} + +const bodySchema = schema.object({ + indexNames: schema.arrayOf(schema.string()), +}); + +export function registerRemoveRoute({ router, license, lib }: RouteDependencies) { + router.post( + { path: addBasePath('/index/remove'), validate: { body: bodySchema } }, + license.guardApiRoute(async (context, request, response) => { + const body = request.body as typeof bodySchema.type; + const { indexNames } = body; + + try { + await removeLifecycle(context.core.elasticsearch.dataClient.callAsCurrentUser, indexNames); + return response.ok(); + } catch (e) { + if (lib.isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts new file mode 100644 index 0000000000000..1e2d621cab173 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function retryLifecycle(callAsCurrentUser: APICaller, indexNames: string[]) { + const responses = []; + for (let i = 0; i < indexNames.length; i++) { + const indexName = indexNames[i]; + const params = { + method: 'POST', + path: `/${encodeURIComponent(indexName)}/_ilm/retry`, + ignore: [404], + }; + + responses.push(callAsCurrentUser('transport.request', params)); + } + return Promise.all(responses); +} + +const bodySchema = schema.object({ + indexNames: schema.arrayOf(schema.string()), +}); + +export function registerRetryRoute({ router, license, lib }: RouteDependencies) { + router.post( + { path: addBasePath('/index/retry'), validate: { body: bodySchema } }, + license.guardApiRoute(async (context, request, response) => { + const body = request.body as typeof bodySchema.type; + const { indexNames } = body; + + try { + await retryLifecycle(context.core.elasticsearch.dataClient.callAsCurrentUser, indexNames); + return response.ok(); + } catch (e) { + if (lib.isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_nodes_routes.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/index.ts similarity index 65% rename from x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_nodes_routes.ts rename to x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/index.ts index 4486d97038657..bde56f0318bbd 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_nodes_routes.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/index.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RouteDependencies } from '../../../types'; import { registerListRoute } from './register_list_route'; import { registerDetailsRoute } from './register_details_route'; -export function registerNodesRoutes(server: any) { - registerListRoute(server); - registerDetailsRoute(server); +export function registerNodesRoutes(dependencies: RouteDependencies) { + registerListRoute(dependencies); + registerDetailsRoute(dependencies); } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts new file mode 100644 index 0000000000000..6ff1f147e7ea7 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +function findMatchingNodes(stats: any, nodeAttrs: string): any { + return Object.entries(stats.nodes).reduce((accum: any[], [nodeId, nodeStats]: [any, any]) => { + const attributes = nodeStats.attributes || {}; + for (const [key, value] of Object.entries(attributes)) { + if (`${key}:${value}` === nodeAttrs) { + accum.push({ + nodeId, + stats: nodeStats, + }); + break; + } + } + return accum; + }, []); +} + +async function fetchNodeStats(callAsCurrentUser: APICaller): Promise { + const params = { + format: 'json', + }; + + return await callAsCurrentUser('nodes.stats', params); +} + +const paramsSchema = schema.object({ + nodeAttrs: schema.string(), +}); + +export function registerDetailsRoute({ router, license, lib }: RouteDependencies) { + router.get( + { path: addBasePath('/nodes/{nodeAttrs}/details'), validate: { params: paramsSchema } }, + license.guardApiRoute(async (context, request, response) => { + const params = request.params as typeof paramsSchema.type; + const { nodeAttrs } = params; + + try { + const stats = await fetchNodeStats(context.core.elasticsearch.dataClient.callAsCurrentUser); + const okResponse = { body: findMatchingNodes(stats, nodeAttrs) }; + return response.ok(okResponse); + } catch (e) { + if (lib.isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts new file mode 100644 index 0000000000000..73d85c78d3b11 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts @@ -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 { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +function convertStatsIntoList(stats: any, disallowedNodeAttributes: string[]): any { + return Object.entries(stats.nodes).reduce((accum: any, [nodeId, nodeStats]: [any, any]) => { + const attributes = nodeStats.attributes || {}; + for (const [key, value] of Object.entries(attributes)) { + const isNodeAttributeAllowed = !disallowedNodeAttributes.includes(key); + if (isNodeAttributeAllowed) { + const attributeString = `${key}:${value}`; + accum[attributeString] = accum[attributeString] || []; + accum[attributeString].push(nodeId); + } + } + return accum; + }, {}); +} + +async function fetchNodeStats(callAsCurrentUser: APICaller): Promise { + const params = { + format: 'json', + }; + + return await callAsCurrentUser('nodes.stats', params); +} + +export function registerListRoute({ router, config, license, lib }: RouteDependencies) { + const { filteredNodeAttributes } = config; + + const NODE_ATTRS_KEYS_TO_IGNORE: string[] = [ + 'ml.enabled', + 'ml.machine_memory', + 'ml.max_open_jobs', + // Used by ML to identify nodes that have transform enabled: + // https://github.com/elastic/elasticsearch/pull/52712/files#diff-225cc2c1291b4c60a8c3412a619094e1R147 + 'transform.node', + 'xpack.installed', + ]; + + const disallowedNodeAttributes = [...NODE_ATTRS_KEYS_TO_IGNORE, ...filteredNodeAttributes]; + + router.get( + { path: addBasePath('/nodes/list'), validate: false }, + license.guardApiRoute(async (context, request, response) => { + try { + const stats = await fetchNodeStats(context.core.elasticsearch.dataClient.callAsCurrentUser); + const okResponse = { body: convertStatsIntoList(stats, disallowedNodeAttributes) }; + return response.ok(okResponse); + } catch (e) { + if (lib.isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_policies_routes.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/index.ts similarity index 64% rename from x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_policies_routes.ts rename to x-pack/plugins/index_lifecycle_management/server/routes/api/policies/index.ts index 279b016da178f..c30dc04c61169 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_policies_routes.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/index.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RouteDependencies } from '../../../types'; import { registerFetchRoute } from './register_fetch_route'; import { registerCreateRoute } from './register_create_route'; import { registerDeleteRoute } from './register_delete_route'; -export function registerPoliciesRoutes(server: any) { - registerFetchRoute(server); - registerCreateRoute(server); - registerDeleteRoute(server); +export function registerPoliciesRoutes(dependencies: RouteDependencies) { + registerFetchRoute(dependencies); + registerCreateRoute(dependencies); + registerDeleteRoute(dependencies); } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts new file mode 100644 index 0000000000000..a9c6bab58fdd9 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function createPolicy(callAsCurrentUser: APICaller, name: string, phases: any): Promise { + const body = { + policy: { + phases, + }, + }; + const params = { + method: 'PUT', + path: `/_ilm/policy/${encodeURIComponent(name)}`, + ignore: [404], + body, + }; + + return await callAsCurrentUser('transport.request', params); +} + +const minAgeSchema = schema.maybe(schema.string()); + +const setPrioritySchema = schema.maybe( + schema.object({ + priority: schema.number(), + }) +); + +const unfollowSchema = schema.maybe(schema.object({})); // Unfollow has no options + +const allocateNodeSchema = schema.maybe(schema.recordOf(schema.string(), schema.string())); +const allocateSchema = schema.maybe( + schema.object({ + number_of_replicas: schema.maybe(schema.number()), + include: allocateNodeSchema, + exclude: allocateNodeSchema, + require: allocateNodeSchema, + }) +); + +const hotPhaseSchema = schema.object({ + min_age: minAgeSchema, + actions: schema.object({ + set_priority: setPrioritySchema, + unfollow: unfollowSchema, + rollover: schema.maybe( + schema.object({ + max_age: schema.maybe(schema.string()), + max_size: schema.maybe(schema.string()), + max_docs: schema.maybe(schema.number()), + }) + ), + }), +}); + +const warmPhaseSchema = schema.maybe( + schema.object({ + min_age: minAgeSchema, + actions: schema.object({ + set_priority: setPrioritySchema, + unfollow: unfollowSchema, + read_only: schema.maybe(schema.object({})), // Readonly has no options + allocate: allocateSchema, + shrink: schema.maybe( + schema.object({ + number_of_shards: schema.number(), + }) + ), + forcemerge: schema.maybe( + schema.object({ + max_num_segments: schema.number(), + }) + ), + }), + }) +); + +const coldPhaseSchema = schema.maybe( + schema.object({ + min_age: minAgeSchema, + actions: schema.object({ + set_priority: setPrioritySchema, + unfollow: unfollowSchema, + allocate: allocateSchema, + freeze: schema.maybe(schema.object({})), // Freeze has no options + }), + }) +); + +const deletePhaseSchema = schema.maybe( + schema.object({ + min_age: minAgeSchema, + actions: schema.object({ + wait_for_snapshot: schema.maybe( + schema.object({ + policy: schema.string(), + }) + ), + delete: schema.maybe(schema.object({})), // Delete has no options + }), + }) +); + +// Per https://www.elastic.co/guide/en/elasticsearch/reference/current/_actions.html +const bodySchema = schema.object({ + name: schema.string(), + phases: schema.object({ + hot: hotPhaseSchema, + warm: warmPhaseSchema, + cold: coldPhaseSchema, + delete: deletePhaseSchema, + }), +}); + +export function registerCreateRoute({ router, license, lib }: RouteDependencies) { + router.post( + { path: addBasePath('/policies'), validate: { body: bodySchema } }, + license.guardApiRoute(async (context, request, response) => { + const body = request.body as typeof bodySchema.type; + const { name, phases } = body; + + try { + await createPolicy(context.core.elasticsearch.dataClient.callAsCurrentUser, name, phases); + return response.ok(); + } catch (e) { + if (lib.isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts new file mode 100644 index 0000000000000..e08297f4d7bc4 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function deletePolicies(callAsCurrentUser: APICaller, policyNames: string): Promise { + const params = { + method: 'DELETE', + path: `/_ilm/policy/${encodeURIComponent(policyNames)}`, + // we allow 404 since they may have no policies + ignore: [404], + }; + + return await callAsCurrentUser('transport.request', params); +} + +const paramsSchema = schema.object({ + policyNames: schema.string(), +}); + +export function registerDeleteRoute({ router, license, lib }: RouteDependencies) { + router.delete( + { path: addBasePath('/policies/{policyNames}'), validate: { params: paramsSchema } }, + license.guardApiRoute(async (context, request, response) => { + const params = request.params as typeof paramsSchema.type; + const { policyNames } = params; + + try { + await deletePolicies(context.core.elasticsearch.dataClient.callAsCurrentUser, policyNames); + return response.ok(); + } catch (e) { + if (lib.isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts new file mode 100644 index 0000000000000..294b7c4c65cba --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.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 { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +function formatPolicies(policiesMap: any): any { + if (policiesMap.status === 404) { + return []; + } + + return Object.keys(policiesMap).reduce((accum: any[], lifecycleName: string) => { + const policyEntry = policiesMap[lifecycleName]; + accum.push({ + ...policyEntry, + name: lifecycleName, + }); + return accum; + }, []); +} + +async function fetchPolicies(callAsCurrentUser: APICaller): Promise { + const params = { + method: 'GET', + path: '/_ilm/policy', + // we allow 404 since they may have no policies + ignore: [404], + }; + + return await callAsCurrentUser('transport.request', params); +} + +async function addLinkedIndices(callAsCurrentUser: APICaller, policiesMap: any) { + if (policiesMap.status === 404) { + return policiesMap; + } + const params = { + method: 'GET', + path: '/*/_ilm/explain', + // we allow 404 since they may have no policies + ignore: [404], + }; + + const policyExplanation: any = await callAsCurrentUser('transport.request', params); + Object.entries(policyExplanation.indices).forEach(([indexName, { policy }]: [string, any]) => { + if (policy && policiesMap[policy]) { + policiesMap[policy].linkedIndices = policiesMap[policy].linkedIndices || []; + policiesMap[policy].linkedIndices.push(indexName); + } + }); +} + +const querySchema = schema.object({ + withIndices: schema.boolean({ defaultValue: false }), +}); + +export function registerFetchRoute({ router, license, lib }: RouteDependencies) { + router.get( + { path: addBasePath('/policies'), validate: { query: querySchema } }, + license.guardApiRoute(async (context, request, response) => { + const query = request.query as typeof querySchema.type; + const { withIndices } = query; + const { callAsCurrentUser } = context.core.elasticsearch.dataClient; + + try { + const policiesMap = await fetchPolicies(callAsCurrentUser); + if (withIndices) { + await addLinkedIndices(callAsCurrentUser, policiesMap); + } + const okResponse = { body: formatPolicies(policiesMap) }; + return response.ok(okResponse); + } catch (e) { + if (lib.isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_templates_routes.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/index.ts similarity index 64% rename from x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_templates_routes.ts rename to x-pack/plugins/index_lifecycle_management/server/routes/api/templates/index.ts index 424b2d36b1ba2..a2d885c3170b9 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_templates_routes.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/index.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RouteDependencies } from '../../../types'; import { registerFetchRoute } from './register_fetch_route'; -import { registerGetRoute } from './register_get_route'; import { registerAddPolicyRoute } from './register_add_policy_route'; -export function registerTemplatesRoutes(server: any) { - registerFetchRoute(server); - registerGetRoute(server); - registerAddPolicyRoute(server); +export function registerTemplatesRoutes(dependencies: RouteDependencies) { + registerFetchRoute(dependencies); + registerAddPolicyRoute(dependencies); } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts new file mode 100644 index 0000000000000..0da8535f8d4ec --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts @@ -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 { merge } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function getIndexTemplate(callAsCurrentUser: APICaller, templateName: string): Promise { + const response = await callAsCurrentUser('indices.getTemplate', { name: templateName }); + return response[templateName]; +} + +async function updateIndexTemplate( + callAsCurrentUser: APICaller, + templateName: string, + policyName: string, + aliasName?: string +): Promise { + // Fetch existing template + const template = await getIndexTemplate(callAsCurrentUser, templateName); + merge(template, { + settings: { + index: { + lifecycle: { + name: policyName, + rollover_alias: aliasName, + }, + }, + }, + }); + + const params = { + method: 'PUT', + path: `/_template/${encodeURIComponent(templateName)}`, + ignore: [404], + body: template, + }; + + return await callAsCurrentUser('transport.request', params); +} + +const bodySchema = schema.object({ + templateName: schema.string(), + policyName: schema.string(), + aliasName: schema.maybe(schema.string()), +}); + +export function registerAddPolicyRoute({ router, license, lib }: RouteDependencies) { + router.post( + { path: addBasePath('/template'), validate: { body: bodySchema } }, + license.guardApiRoute(async (context, request, response) => { + const body = request.body as typeof bodySchema.type; + const { templateName, policyName, aliasName } = body; + + try { + await updateIndexTemplate( + context.core.elasticsearch.dataClient.callAsCurrentUser, + templateName, + policyName, + aliasName + ); + return response.ok(); + } catch (e) { + if (lib.isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts similarity index 63% rename from x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts rename to x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts index fd58f471d69bb..a2dc67cb77afe 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; /** * We don't want to output system template (whose name starts with a ".") which don't @@ -49,7 +49,7 @@ function filterAndFormatTemplates(templates: any): any { return formattedTemplates; } -async function fetchTemplates(callWithRequest: any): Promise { +async function fetchTemplates(callAsCurrentUser: APICaller): Promise { const params = { method: 'GET', path: '/_template', @@ -57,30 +57,29 @@ async function fetchTemplates(callWithRequest: any): Promise { ignore: [404], }; - return await callWithRequest('transport.request', params); + return await callAsCurrentUser('transport.request', params); } -export function registerFetchRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/templates', - method: 'GET', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); +export function registerFetchRoute({ router, license, lib }: RouteDependencies) { + router.get( + { path: addBasePath('/templates'), validate: false }, + license.guardApiRoute(async (context, request, response) => { try { - const templates = await fetchTemplates(callWithRequest); - return filterAndFormatTemplates(templates); - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); + const templates = await fetchTemplates( + context.core.elasticsearch.dataClient.callAsCurrentUser + ); + const okResponse = { body: filterAndFormatTemplates(templates) }; + return response.ok(okResponse); + } catch (e) { + if (lib.isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: e, + }); } - - return wrapUnknownError(err); + // Case: default + return response.internalError({ body: e }); } - }, - config: { - pre: [licensePreRouting], - }, - }); + }) + ); } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts new file mode 100644 index 0000000000000..35996721854c6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/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. + */ + +import { RouteDependencies } from '../types'; + +import { registerIndexRoutes } from './api/index'; +import { registerNodesRoutes } from './api/nodes'; +import { registerPoliciesRoutes } from './api/policies'; +import { registerTemplatesRoutes } from './api/templates'; + +export function registerApiRoutes(dependencies: RouteDependencies) { + registerIndexRoutes(dependencies); + registerNodesRoutes(dependencies); + registerPoliciesRoutes(dependencies); + registerTemplatesRoutes(dependencies); +} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/index.ts b/x-pack/plugins/index_lifecycle_management/server/services/add_base_path.ts similarity index 64% rename from x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/index.ts rename to x-pack/plugins/index_lifecycle_management/server/services/add_base_path.ts index f2c070fd44b6e..3f3dd131df7c7 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/index.ts +++ b/x-pack/plugins/index_lifecycle_management/server/services/add_base_path.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { checkLicense } from './check_license'; +import { API_BASE_PATH } from '../../common/constants'; + +export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/index.ts b/x-pack/plugins/index_lifecycle_management/server/services/index.ts similarity index 74% rename from x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/index.ts rename to x-pack/plugins/index_lifecycle_management/server/services/index.ts index 0743e443955f4..d7b544b290c39 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/index.ts +++ b/x-pack/plugins/index_lifecycle_management/server/services/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { licensePreRoutingFactory } from './license_pre_routing_factory'; +export { License } from './license'; +export { addBasePath } from './add_base_path'; diff --git a/x-pack/plugins/index_lifecycle_management/server/services/license.ts b/x-pack/plugins/index_lifecycle_management/server/services/license.ts new file mode 100644 index 0000000000000..31d3654c51e3e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/services/license.ts @@ -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 { Logger } from 'src/core/server'; +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'kibana/server'; + +import { LicensingPluginSetup } from '../../../licensing/server'; +import { LicenseType } from '../../../licensing/common/types'; + +export interface LicenseStatus { + isValid: boolean; + message?: string; +} + +interface SetupSettings { + pluginId: string; + minimumLicenseType: LicenseType; + defaultErrorMessage: string; +} + +export class License { + private licenseStatus: LicenseStatus = { + isValid: false, + message: 'Invalid License', + }; + + setup( + { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, + { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } + ) { + licensing.license$.subscribe(license => { + const { state, message } = license.check(pluginId, minimumLicenseType); + const hasRequiredLicense = state === 'valid'; + + if (hasRequiredLicense) { + this.licenseStatus = { isValid: true }; + } else { + this.licenseStatus = { + isValid: false, + message: message || defaultErrorMessage, + }; + if (message) { + logger.info(message); + } + } + }); + } + + guardApiRoute(handler: RequestHandler) { + const license = this; + + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseStatus = license.getStatus(); + + if (!licenseStatus.isValid) { + return response.customError({ + body: { + message: licenseStatus.message || '', + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; + } + + getStatus() { + return this.licenseStatus; + } +} diff --git a/x-pack/plugins/index_lifecycle_management/server/types.ts b/x-pack/plugins/index_lifecycle_management/server/types.ts new file mode 100644 index 0000000000000..7f64c1a47197a --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'src/core/server'; + +import { LicensingPluginSetup } from '../../licensing/server'; +import { IndexManagementPluginSetup } from '../../index_management/server'; +import { License } from './services'; +import { IndexLifecycleManagementConfig } from './config'; +import { isEsError } from './lib/is_es_error'; + +export interface Dependencies { + licensing: LicensingPluginSetup; + indexManagement: IndexManagementPluginSetup; +} + +export interface RouteDependencies { + router: IRouter; + config: IndexLifecycleManagementConfig; + license: License; + lib: { + isEsError: typeof isEsError; + }; +} diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js index 8f794ce1ed612..a351d39b123a8 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js @@ -46,11 +46,7 @@ export class IndexActionsContextMenu extends Component { confirmAction = isActionConfirmed => { this.setState({ isActionConfirmed }); }; - panels({ - core: { fatalErrors }, - services: { extensionsService, httpService, notificationService }, - plugins: { usageCollection }, - }) { + panels({ services: { extensionsService } }) { const { closeIndices, openIndices, @@ -218,15 +214,6 @@ export class IndexActionsContextMenu extends Component { const actionExtensionDefinition = actionExtension({ indices, reloadIndices, - // These config options can be removed once the NP migration out of legacy is complete. - // They're needed for now because ILM's extensions make API calls which require these - // dependencies, but they're not available unless the app's "setup" lifecycle stage occurs. - // Within the old platform, "setup" only occurs once the user actually visits the app. - // Once ILM and IM have been moved out of legacy this hack won't be necessary. - usageCollection, - toasts: notificationService.toasts, - fatalErrors, - httpClient: httpService.httpClient, }); if (actionExtensionDefinition) { const { diff --git a/x-pack/plugins/index_management/public/index.ts b/x-pack/plugins/index_management/public/index.ts index 6bb921ef648f3..7a76fff7f3ec6 100644 --- a/x-pack/plugins/index_management/public/index.ts +++ b/x-pack/plugins/index_management/public/index.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import './index.scss'; -import { IndexMgmtUIPlugin, IndexMgmtSetup } from './plugin'; +import { IndexMgmtUIPlugin, IndexManagementPluginSetup } from './plugin'; /** @public */ export const plugin = () => { return new IndexMgmtUIPlugin(); }; -export { IndexMgmtSetup }; +export { IndexManagementPluginSetup }; export { getIndexListUri } from './application/services/navigation'; diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts index 4aa06d286e3c4..f9e2a47170b3d 100644 --- a/x-pack/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -20,7 +20,7 @@ import { setUiMetricService } from './application/services/api'; import { IndexMgmtMetricsType } from './types'; import { ExtensionsService, ExtensionsSetup } from './services'; -export interface IndexMgmtSetup { +export interface IndexManagementPluginSetup { extensionsService: ExtensionsSetup; } @@ -40,7 +40,7 @@ export class IndexMgmtUIPlugin { setUiMetricService(this.uiMetricService); } - public setup(coreSetup: CoreSetup, plugins: PluginsDependencies): IndexMgmtSetup { + public setup(coreSetup: CoreSetup, plugins: PluginsDependencies): IndexManagementPluginSetup { const { http, notifications } = coreSetup; const { usageCollection, management } = plugins; diff --git a/x-pack/plugins/index_management/server/index.ts b/x-pack/plugins/index_management/server/index.ts index e4102711708cb..4d9409e4a516c 100644 --- a/x-pack/plugins/index_management/server/index.ts +++ b/x-pack/plugins/index_management/server/index.ts @@ -17,6 +17,6 @@ export const config = { /** @public */ export { Dependencies } from './types'; -export { IndexMgmtSetup } from './plugin'; +export { IndexManagementPluginSetup } from './plugin'; export { Index } from './types'; export { IndexManagementConfig } from './config'; diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index a0a9151cdb71f..e5bd7451b028f 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -12,13 +12,13 @@ import { ApiRoutes } from './routes'; import { License, IndexDataEnricher } from './services'; import { isEsError } from './lib/is_es_error'; -export interface IndexMgmtSetup { +export interface IndexManagementPluginSetup { indexDataEnricher: { add: IndexDataEnricher['add']; }; } -export class IndexMgmtServerPlugin implements Plugin { +export class IndexMgmtServerPlugin implements Plugin { private readonly apiRoutes: ApiRoutes; private readonly license: License; private readonly logger: Logger; @@ -31,7 +31,7 @@ export class IndexMgmtServerPlugin implements Plugin { const loadTemplates = () => supertest.get(`${API_BASE_PATH}/templates`); - const getTemplate = name => supertest.get(`${API_BASE_PATH}/templates/${name}`); - const addPolicyToTemplate = (templateName, policyName, aliasName) => supertest .post(`${API_BASE_PATH}/template`) @@ -23,7 +21,6 @@ export const registerHelpers = ({ supertest }) => { return { loadTemplates, - getTemplate, addPolicyToTemplate, }; }; diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js index 374577825cd94..d30c20527471b 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js @@ -16,7 +16,7 @@ export default function({ getService }) { const { createIndexTemplate, cleanUp: cleanUpEsResources } = initElasticsearchHelpers(es); - const { loadTemplates, getTemplate, addPolicyToTemplate } = registerTemplatesHelpers({ + const { loadTemplates, addPolicyToTemplate } = registerTemplatesHelpers({ supertest, }); @@ -48,18 +48,6 @@ export default function({ getService }) { }); }); - describe('get', () => { - it('should fetch a single template', async () => { - // Create a template with the ES client - const templateName = getRandomString(); - const template = getTemplatePayload(); - await createIndexTemplate(templateName, template); - - const { body } = await getTemplate(templateName).expect(200); - expect(body.index_patterns).to.eql(template.index_patterns); - }); - }); - describe('update', () => { it('should add a policy to a template', async () => { // Create policy @@ -78,12 +66,13 @@ export default function({ getService }) { await addPolicyToTemplate(templateName, policyName, rolloverAlias).expect(200); // Fetch the template and verify that the policy has been attached - const { body } = await getTemplate(templateName); + const { body } = await loadTemplates(); + const fetchedTemplate = body.find(({ name }) => templateName === name); const { settings: { index: { lifecycle }, }, - } = body; + } = fetchedTemplate; expect(lifecycle.name).to.equal(policyName); expect(lifecycle.rollover_alias).to.equal(rolloverAlias); }); From 330956ec1a7ea94cc6665147b870075f683d05b3 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Fri, 10 Apr 2020 02:55:28 +0300 Subject: [PATCH 77/81] Code coverage: fix missing coverage after merging oss & x-pack data (#63178) * update nyc & istanbul-babel deps * update index.js in kbn-pm Co-authored-by: Elastic Machine --- package.json | 4 +- packages/kbn-pm/dist/index.js | 1919 ++++++++++++++++----------------- yarn.lock | 410 ++++--- 3 files changed, 1190 insertions(+), 1143 deletions(-) diff --git a/package.json b/package.json index ea930d07e7b43..ec72d5b660345 100644 --- a/package.json +++ b/package.json @@ -396,7 +396,7 @@ "axe-core": "^3.4.1", "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", - "babel-plugin-istanbul": "^5.2.0", + "babel-plugin-istanbul": "^6.0.0", "backport": "5.1.3", "chai": "3.5.0", "chance": "1.0.18", @@ -469,7 +469,7 @@ "nock": "12.0.3", "node-sass": "^4.13.1", "normalize-path": "^3.0.0", - "nyc": "^14.1.1", + "nyc": "^15.0.1", "pixelmatch": "^5.1.0", "pkg-up": "^2.0.0", "pngjs": "^3.4.0", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 7a858deff41d3..399720f310f67 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(704); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(703); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); @@ -105,10 +105,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(515); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Project", function() { return _utils_project__WEBPACK_IMPORTED_MODULE_3__["Project"]; }); -/* harmony import */ var _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(577); +/* harmony import */ var _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(576); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "copyWorkspacePackages", function() { return _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__["copyWorkspacePackages"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(578); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(577); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -152,7 +152,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(17); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(688); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(687); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(34); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -2506,9 +2506,9 @@ module.exports = require("path"); __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(18); -/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(585); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(685); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(686); +/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(584); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(684); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(685); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -2551,8 +2551,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(34); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(499); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(500); -/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(579); -/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(584); +/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(578); +/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(583); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -43866,7 +43866,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(514); /* harmony import */ var _project__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(515); -/* harmony import */ var _workspaces__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(577); +/* harmony import */ var _workspaces__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(576); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -47386,7 +47386,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(514); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(34); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(516); -/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(562); +/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(561); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -52557,8 +52557,8 @@ const fs = __webpack_require__(545); const writeFileAtomic = __webpack_require__(549); const sortKeys = __webpack_require__(556); const makeDir = __webpack_require__(558); -const pify = __webpack_require__(560); -const detectIndent = __webpack_require__(561); +const pify = __webpack_require__(559); +const detectIndent = __webpack_require__(560); const init = (fn, filePath, data, options) => { if (!filePath) { @@ -54852,81 +54852,6 @@ module.exports = (input, options) => { "use strict"; -const processFn = (fn, options) => function (...args) { - const P = options.promiseModule; - - return new P((resolve, reject) => { - if (options.multiArgs) { - args.push((...result) => { - if (options.errorFirst) { - if (result[0]) { - reject(result); - } else { - result.shift(); - resolve(result); - } - } else { - resolve(result); - } - }); - } else if (options.errorFirst) { - args.push((error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - } else { - args.push(resolve); - } - - fn.apply(this, args); - }); -}; - -module.exports = (input, options) => { - options = Object.assign({ - exclude: [/.+(Sync|Stream)$/], - errorFirst: true, - promiseModule: Promise - }, options); - - const objType = typeof input; - if (!(input !== null && (objType === 'object' || objType === 'function'))) { - throw new TypeError(`Expected \`input\` to be a \`Function\` or \`Object\`, got \`${input === null ? 'null' : objType}\``); - } - - const filter = key => { - const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key); - return options.include ? options.include.some(match) : !options.exclude.some(match); - }; - - let ret; - if (objType === 'function') { - ret = function (...args) { - return options.excludeMain ? input(...args) : processFn(input, options).apply(this, args); - }; - } else { - ret = Object.create(Object.getPrototypeOf(input)); - } - - for (const key in input) { // eslint-disable-line guard-for-in - const property = input[key]; - ret[key] = typeof property === 'function' && filter(key) ? processFn(property, options) : property; - } - - return ret; -}; - - -/***/ }), -/* 561 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - // detect either spaces or tabs but not both to properly handle tabs // for indentation and spaces for alignment const INDENT_RE = /^(?:( )+|\t+)/; @@ -55050,7 +54975,7 @@ module.exports = str => { /***/ }), -/* 562 */ +/* 561 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55059,7 +54984,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackage", function() { return runScriptInPackage; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackageStreaming", function() { return runScriptInPackageStreaming; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "yarnWorkspacesInfo", function() { return yarnWorkspacesInfo; }); -/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(563); +/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(562); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -55129,7 +55054,7 @@ async function yarnWorkspacesInfo(directory) { } /***/ }), -/* 563 */ +/* 562 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55140,9 +55065,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(351); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(564); +/* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(563); /* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(log_symbols__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(569); +/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(568); /* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } @@ -55208,12 +55133,12 @@ function spawnStreaming(command, args, opts, { } /***/ }), -/* 564 */ +/* 563 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(565); +const chalk = __webpack_require__(564); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -55235,16 +55160,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 565 */ +/* 564 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(3); -const ansiStyles = __webpack_require__(566); -const stdoutColor = __webpack_require__(567).stdout; +const ansiStyles = __webpack_require__(565); +const stdoutColor = __webpack_require__(566).stdout; -const template = __webpack_require__(568); +const template = __webpack_require__(567); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -55470,7 +55395,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 566 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55643,7 +55568,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 567 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55785,7 +55710,7 @@ module.exports = { /***/ }), -/* 568 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55920,7 +55845,7 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 569 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { // Copyright IBM Corp. 2014,2018. All Rights Reserved. @@ -55928,12 +55853,12 @@ module.exports = (chalk, tmp) => { // This file is licensed under the Apache License 2.0. // License text available at https://opensource.org/licenses/Apache-2.0 -module.exports = __webpack_require__(570); -module.exports.cli = __webpack_require__(574); +module.exports = __webpack_require__(569); +module.exports.cli = __webpack_require__(573); /***/ }), -/* 570 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55948,9 +55873,9 @@ var stream = __webpack_require__(27); var util = __webpack_require__(29); var fs = __webpack_require__(23); -var through = __webpack_require__(571); -var duplexer = __webpack_require__(572); -var StringDecoder = __webpack_require__(573).StringDecoder; +var through = __webpack_require__(570); +var duplexer = __webpack_require__(571); +var StringDecoder = __webpack_require__(572).StringDecoder; module.exports = Logger; @@ -56139,7 +56064,7 @@ function lineMerger(host) { /***/ }), -/* 571 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27) @@ -56253,7 +56178,7 @@ function through (write, end, opts) { /***/ }), -/* 572 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27) @@ -56346,13 +56271,13 @@ function duplex(writer, reader) { /***/ }), -/* 573 */ +/* 572 */ /***/ (function(module, exports) { module.exports = require("string_decoder"); /***/ }), -/* 574 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56363,11 +56288,11 @@ module.exports = require("string_decoder"); -var minimist = __webpack_require__(575); +var minimist = __webpack_require__(574); var path = __webpack_require__(16); -var Logger = __webpack_require__(570); -var pkg = __webpack_require__(576); +var Logger = __webpack_require__(569); +var pkg = __webpack_require__(575); module.exports = cli; @@ -56421,7 +56346,7 @@ function usage($0, p) { /***/ }), -/* 575 */ +/* 574 */ /***/ (function(module, exports) { module.exports = function (args, opts) { @@ -56663,13 +56588,13 @@ function isNumber (x) { /***/ }), -/* 576 */ +/* 575 */ /***/ (function(module) { module.exports = JSON.parse("{\"name\":\"strong-log-transformer\",\"version\":\"2.1.0\",\"description\":\"Stream transformer that prefixes lines with timestamps and other things.\",\"author\":\"Ryan Graham \",\"license\":\"Apache-2.0\",\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/strongloop/strong-log-transformer\"},\"keywords\":[\"logging\",\"streams\"],\"bugs\":{\"url\":\"https://github.com/strongloop/strong-log-transformer/issues\"},\"homepage\":\"https://github.com/strongloop/strong-log-transformer\",\"directories\":{\"test\":\"test\"},\"bin\":{\"sl-log-transformer\":\"bin/sl-log-transformer.js\"},\"main\":\"index.js\",\"scripts\":{\"test\":\"tap --100 test/test-*\"},\"dependencies\":{\"duplexer\":\"^0.1.1\",\"minimist\":\"^1.2.0\",\"through\":\"^2.3.4\"},\"devDependencies\":{\"tap\":\"^12.0.1\"},\"engines\":{\"node\":\">=4\"}}"); /***/ }), -/* 577 */ +/* 576 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56682,7 +56607,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(578); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(577); /* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(20); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(516); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(500); @@ -56777,7 +56702,7 @@ function packagesFromGlobPattern({ } /***/ }), -/* 578 */ +/* 577 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56847,7 +56772,7 @@ function getProjectPaths({ } /***/ }), -/* 579 */ +/* 578 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56855,13 +56780,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getAllChecksums", function() { return getAllChecksums; }); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(23); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(580); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(579); /* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(351); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_3__); -/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(581); +/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(580); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -57087,19 +57012,19 @@ async function getAllChecksums(kbn, log) { } /***/ }), -/* 580 */ +/* 579 */ /***/ (function(module, exports) { module.exports = require("crypto"); /***/ }), -/* 581 */ +/* 580 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "readYarnLock", function() { return readYarnLock; }); -/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(582); +/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(581); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(20); /* @@ -57143,7 +57068,7 @@ async function readYarnLock(kbn) { } /***/ }), -/* 582 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { module.exports = @@ -58702,7 +58627,7 @@ module.exports = invariant; /* 9 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(580); +module.exports = __webpack_require__(579); /***/ }), /* 10 */, @@ -61026,7 +60951,7 @@ function onceStrict (fn) { /* 63 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(583); +module.exports = __webpack_require__(582); /***/ }), /* 64 */, @@ -67421,13 +67346,13 @@ module.exports = process && support(supportLevel); /******/ ]); /***/ }), -/* 583 */ +/* 582 */ /***/ (function(module, exports) { module.exports = require("buffer"); /***/ }), -/* 584 */ +/* 583 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -67524,7 +67449,7 @@ class BootstrapCacheFile { } /***/ }), -/* 585 */ +/* 584 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -67532,9 +67457,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CleanCommand", function() { return CleanCommand; }); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(586); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(585); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(674); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(673); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); @@ -67633,21 +67558,21 @@ const CleanCommand = { }; /***/ }), -/* 586 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const path = __webpack_require__(16); -const globby = __webpack_require__(587); -const isGlob = __webpack_require__(604); -const slash = __webpack_require__(665); +const globby = __webpack_require__(586); +const isGlob = __webpack_require__(603); +const slash = __webpack_require__(664); const gracefulFs = __webpack_require__(22); -const isPathCwd = __webpack_require__(667); -const isPathInside = __webpack_require__(668); -const rimraf = __webpack_require__(669); -const pMap = __webpack_require__(670); +const isPathCwd = __webpack_require__(666); +const isPathInside = __webpack_require__(667); +const rimraf = __webpack_require__(668); +const pMap = __webpack_require__(669); const rimrafP = promisify(rimraf); @@ -67761,19 +67686,19 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options /***/ }), -/* 587 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const arrayUnion = __webpack_require__(588); -const merge2 = __webpack_require__(589); -const glob = __webpack_require__(590); -const fastGlob = __webpack_require__(595); -const dirGlob = __webpack_require__(661); -const gitignore = __webpack_require__(663); -const {FilterStream, UniqueStream} = __webpack_require__(666); +const arrayUnion = __webpack_require__(587); +const merge2 = __webpack_require__(588); +const glob = __webpack_require__(589); +const fastGlob = __webpack_require__(594); +const dirGlob = __webpack_require__(660); +const gitignore = __webpack_require__(662); +const {FilterStream, UniqueStream} = __webpack_require__(665); const DEFAULT_FILTER = () => false; @@ -67946,7 +67871,7 @@ module.exports.gitignore = gitignore; /***/ }), -/* 588 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67958,7 +67883,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 589 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68072,7 +67997,7 @@ function pauseStreams (streams, options) { /***/ }), -/* 590 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -68121,13 +68046,13 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(502) var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(591) +var inherits = __webpack_require__(590) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(510) -var globSync = __webpack_require__(593) -var common = __webpack_require__(594) +var globSync = __webpack_require__(592) +var common = __webpack_require__(593) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -68868,7 +68793,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 591 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -68878,12 +68803,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(592); + module.exports = __webpack_require__(591); } /***/ }), -/* 592 */ +/* 591 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -68916,7 +68841,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 593 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync @@ -68926,12 +68851,12 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(502) var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(590).Glob +var Glob = __webpack_require__(589).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(510) -var common = __webpack_require__(594) +var common = __webpack_require__(593) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -69408,7 +69333,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 594 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -69654,17 +69579,17 @@ function childrenIgnored (self, path) { /***/ }), -/* 595 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(596); -const async_1 = __webpack_require__(624); -const stream_1 = __webpack_require__(657); -const sync_1 = __webpack_require__(658); -const settings_1 = __webpack_require__(660); -const utils = __webpack_require__(597); +const taskManager = __webpack_require__(595); +const async_1 = __webpack_require__(623); +const stream_1 = __webpack_require__(656); +const sync_1 = __webpack_require__(657); +const settings_1 = __webpack_require__(659); +const utils = __webpack_require__(596); function FastGlob(source, options) { try { assertPatternsInput(source); @@ -69722,13 +69647,13 @@ module.exports = FastGlob; /***/ }), -/* 596 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(597); +const utils = __webpack_require__(596); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -69796,28 +69721,28 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 597 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const array = __webpack_require__(598); +const array = __webpack_require__(597); exports.array = array; -const errno = __webpack_require__(599); +const errno = __webpack_require__(598); exports.errno = errno; -const fs = __webpack_require__(600); +const fs = __webpack_require__(599); exports.fs = fs; -const path = __webpack_require__(601); +const path = __webpack_require__(600); exports.path = path; -const pattern = __webpack_require__(602); +const pattern = __webpack_require__(601); exports.pattern = pattern; -const stream = __webpack_require__(623); +const stream = __webpack_require__(622); exports.stream = stream; /***/ }), -/* 598 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69830,7 +69755,7 @@ exports.flatten = flatten; /***/ }), -/* 599 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69843,7 +69768,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 600 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69868,7 +69793,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 601 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69889,16 +69814,16 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 602 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const globParent = __webpack_require__(603); -const isGlob = __webpack_require__(604); -const micromatch = __webpack_require__(606); +const globParent = __webpack_require__(602); +const isGlob = __webpack_require__(603); +const micromatch = __webpack_require__(605); const GLOBSTAR = '**'; function isStaticPattern(pattern) { return !isDynamicPattern(pattern); @@ -69987,13 +69912,13 @@ exports.matchAny = matchAny; /***/ }), -/* 603 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isGlob = __webpack_require__(604); +var isGlob = __webpack_require__(603); var pathPosixDirname = __webpack_require__(16).posix.dirname; var isWin32 = __webpack_require__(11).platform() === 'win32'; @@ -70028,7 +69953,7 @@ module.exports = function globParent(str) { /***/ }), -/* 604 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -70038,7 +69963,7 @@ module.exports = function globParent(str) { * Released under the MIT License. */ -var isExtglob = __webpack_require__(605); +var isExtglob = __webpack_require__(604); var chars = { '{': '}', '(': ')', '[': ']'}; var strictRegex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; var relaxedRegex = /\\(.)|(^!|[*?{}()[\]]|\(\?)/; @@ -70082,7 +70007,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 605 */ +/* 604 */ /***/ (function(module, exports) { /*! @@ -70108,16 +70033,16 @@ module.exports = function isExtglob(str) { /***/ }), -/* 606 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const util = __webpack_require__(29); -const braces = __webpack_require__(607); -const picomatch = __webpack_require__(617); -const utils = __webpack_require__(620); +const braces = __webpack_require__(606); +const picomatch = __webpack_require__(616); +const utils = __webpack_require__(619); const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); /** @@ -70582,16 +70507,16 @@ module.exports = micromatch; /***/ }), -/* 607 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(608); -const compile = __webpack_require__(610); -const expand = __webpack_require__(614); -const parse = __webpack_require__(615); +const stringify = __webpack_require__(607); +const compile = __webpack_require__(609); +const expand = __webpack_require__(613); +const parse = __webpack_require__(614); /** * Expand the given pattern or create a regex-compatible string. @@ -70759,13 +70684,13 @@ module.exports = braces; /***/ }), -/* 608 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(609); +const utils = __webpack_require__(608); module.exports = (ast, options = {}) => { let stringify = (node, parent = {}) => { @@ -70798,7 +70723,7 @@ module.exports = (ast, options = {}) => { /***/ }), -/* 609 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70917,14 +70842,14 @@ exports.flatten = (...args) => { /***/ }), -/* 610 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(611); -const utils = __webpack_require__(609); +const fill = __webpack_require__(610); +const utils = __webpack_require__(608); const compile = (ast, options = {}) => { let walk = (node, parent = {}) => { @@ -70981,7 +70906,7 @@ module.exports = compile; /***/ }), -/* 611 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70995,7 +70920,7 @@ module.exports = compile; const util = __webpack_require__(29); -const toRegexRange = __webpack_require__(612); +const toRegexRange = __webpack_require__(611); const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); @@ -71237,7 +71162,7 @@ module.exports = fill; /***/ }), -/* 612 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71250,7 +71175,7 @@ module.exports = fill; -const isNumber = __webpack_require__(613); +const isNumber = __webpack_require__(612); const toRegexRange = (min, max, options) => { if (isNumber(min) === false) { @@ -71532,7 +71457,7 @@ module.exports = toRegexRange; /***/ }), -/* 613 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71557,15 +71482,15 @@ module.exports = function(num) { /***/ }), -/* 614 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(611); -const stringify = __webpack_require__(608); -const utils = __webpack_require__(609); +const fill = __webpack_require__(610); +const stringify = __webpack_require__(607); +const utils = __webpack_require__(608); const append = (queue = '', stash = '', enclose = false) => { let result = []; @@ -71677,13 +71602,13 @@ module.exports = expand; /***/ }), -/* 615 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(608); +const stringify = __webpack_require__(607); /** * Constants @@ -71705,7 +71630,7 @@ const { CHAR_SINGLE_QUOTE, /* ' */ CHAR_NO_BREAK_SPACE, CHAR_ZERO_WIDTH_NOBREAK_SPACE -} = __webpack_require__(616); +} = __webpack_require__(615); /** * parse @@ -72017,7 +71942,7 @@ module.exports = parse; /***/ }), -/* 616 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72081,26 +72006,26 @@ module.exports = { /***/ }), -/* 617 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(618); +module.exports = __webpack_require__(617); /***/ }), -/* 618 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const scan = __webpack_require__(619); -const parse = __webpack_require__(622); -const utils = __webpack_require__(620); +const scan = __webpack_require__(618); +const parse = __webpack_require__(621); +const utils = __webpack_require__(619); /** * Creates a matcher function from one or more glob patterns. The @@ -72403,7 +72328,7 @@ picomatch.toRegex = (source, options) => { * @return {Object} */ -picomatch.constants = __webpack_require__(621); +picomatch.constants = __webpack_require__(620); /** * Expose "picomatch" @@ -72413,13 +72338,13 @@ module.exports = picomatch; /***/ }), -/* 619 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(620); +const utils = __webpack_require__(619); const { CHAR_ASTERISK, /* * */ @@ -72437,7 +72362,7 @@ const { CHAR_RIGHT_CURLY_BRACE, /* } */ CHAR_RIGHT_PARENTHESES, /* ) */ CHAR_RIGHT_SQUARE_BRACKET /* ] */ -} = __webpack_require__(621); +} = __webpack_require__(620); const isPathSeparator = code => { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; @@ -72639,7 +72564,7 @@ module.exports = (input, options) => { /***/ }), -/* 620 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72651,7 +72576,7 @@ const { REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL, REGEX_REMOVE_BACKSLASH -} = __webpack_require__(621); +} = __webpack_require__(620); exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); @@ -72689,7 +72614,7 @@ exports.escapeLast = (input, char, lastIdx) => { /***/ }), -/* 621 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72875,14 +72800,14 @@ module.exports = { /***/ }), -/* 622 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(620); -const constants = __webpack_require__(621); +const utils = __webpack_require__(619); +const constants = __webpack_require__(620); /** * Constants @@ -73893,13 +73818,13 @@ module.exports = parse; /***/ }), -/* 623 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const merge2 = __webpack_require__(589); +const merge2 = __webpack_require__(588); function merge(streams) { const mergedStream = merge2(streams); streams.forEach((stream) => { @@ -73911,14 +73836,14 @@ exports.merge = merge; /***/ }), -/* 624 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(625); -const provider_1 = __webpack_require__(652); +const stream_1 = __webpack_require__(624); +const provider_1 = __webpack_require__(651); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -73946,16 +73871,16 @@ exports.default = ProviderAsync; /***/ }), -/* 625 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(27); -const fsStat = __webpack_require__(626); -const fsWalk = __webpack_require__(631); -const reader_1 = __webpack_require__(651); +const fsStat = __webpack_require__(625); +const fsWalk = __webpack_require__(630); +const reader_1 = __webpack_require__(650); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -74008,15 +73933,15 @@ exports.default = ReaderStream; /***/ }), -/* 626 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(627); -const sync = __webpack_require__(628); -const settings_1 = __webpack_require__(629); +const async = __webpack_require__(626); +const sync = __webpack_require__(627); +const settings_1 = __webpack_require__(628); exports.Settings = settings_1.default; function stat(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -74039,7 +73964,7 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 627 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74077,7 +74002,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 628 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74106,13 +74031,13 @@ exports.read = read; /***/ }), -/* 629 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(630); +const fs = __webpack_require__(629); class Settings { constructor(_options = {}) { this._options = _options; @@ -74129,7 +74054,7 @@ exports.default = Settings; /***/ }), -/* 630 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74152,16 +74077,16 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 631 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(632); -const stream_1 = __webpack_require__(647); -const sync_1 = __webpack_require__(648); -const settings_1 = __webpack_require__(650); +const async_1 = __webpack_require__(631); +const stream_1 = __webpack_require__(646); +const sync_1 = __webpack_require__(647); +const settings_1 = __webpack_require__(649); exports.Settings = settings_1.default; function walk(dir, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -74191,13 +74116,13 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 632 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(633); +const async_1 = __webpack_require__(632); class AsyncProvider { constructor(_root, _settings) { this._root = _root; @@ -74228,17 +74153,17 @@ function callSuccessCallback(callback, entries) { /***/ }), -/* 633 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = __webpack_require__(379); -const fsScandir = __webpack_require__(634); -const fastq = __webpack_require__(643); -const common = __webpack_require__(645); -const reader_1 = __webpack_require__(646); +const fsScandir = __webpack_require__(633); +const fastq = __webpack_require__(642); +const common = __webpack_require__(644); +const reader_1 = __webpack_require__(645); class AsyncReader extends reader_1.default { constructor(_root, _settings) { super(_root, _settings); @@ -74328,15 +74253,15 @@ exports.default = AsyncReader; /***/ }), -/* 634 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(635); -const sync = __webpack_require__(640); -const settings_1 = __webpack_require__(641); +const async = __webpack_require__(634); +const sync = __webpack_require__(639); +const settings_1 = __webpack_require__(640); exports.Settings = settings_1.default; function scandir(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -74359,16 +74284,16 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 635 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(626); -const rpl = __webpack_require__(636); -const constants_1 = __webpack_require__(637); -const utils = __webpack_require__(638); +const fsStat = __webpack_require__(625); +const rpl = __webpack_require__(635); +const constants_1 = __webpack_require__(636); +const utils = __webpack_require__(637); function read(dir, settings, callback) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(dir, settings, callback); @@ -74457,7 +74382,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 636 */ +/* 635 */ /***/ (function(module, exports) { module.exports = runParallel @@ -74511,7 +74436,7 @@ function runParallel (tasks, cb) { /***/ }), -/* 637 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74527,18 +74452,18 @@ exports.IS_SUPPORT_READDIR_WITH_FILE_TYPES = MAJOR_VERSION > 10 || (MAJOR_VERSIO /***/ }), -/* 638 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(639); +const fs = __webpack_require__(638); exports.fs = fs; /***/ }), -/* 639 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74563,15 +74488,15 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 640 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(626); -const constants_1 = __webpack_require__(637); -const utils = __webpack_require__(638); +const fsStat = __webpack_require__(625); +const constants_1 = __webpack_require__(636); +const utils = __webpack_require__(637); function read(dir, settings) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(dir, settings); @@ -74622,15 +74547,15 @@ exports.readdir = readdir; /***/ }), -/* 641 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const fsStat = __webpack_require__(626); -const fs = __webpack_require__(642); +const fsStat = __webpack_require__(625); +const fs = __webpack_require__(641); class Settings { constructor(_options = {}) { this._options = _options; @@ -74653,7 +74578,7 @@ exports.default = Settings; /***/ }), -/* 642 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74678,13 +74603,13 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 643 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var reusify = __webpack_require__(644) +var reusify = __webpack_require__(643) function fastqueue (context, worker, concurrency) { if (typeof context === 'function') { @@ -74858,7 +74783,7 @@ module.exports = fastqueue /***/ }), -/* 644 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74898,7 +74823,7 @@ module.exports = reusify /***/ }), -/* 645 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74929,13 +74854,13 @@ exports.joinPathSegments = joinPathSegments; /***/ }), -/* 646 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const common = __webpack_require__(645); +const common = __webpack_require__(644); class Reader { constructor(_root, _settings) { this._root = _root; @@ -74947,14 +74872,14 @@ exports.default = Reader; /***/ }), -/* 647 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(27); -const async_1 = __webpack_require__(633); +const async_1 = __webpack_require__(632); class StreamProvider { constructor(_root, _settings) { this._root = _root; @@ -74984,13 +74909,13 @@ exports.default = StreamProvider; /***/ }), -/* 648 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(649); +const sync_1 = __webpack_require__(648); class SyncProvider { constructor(_root, _settings) { this._root = _root; @@ -75005,15 +74930,15 @@ exports.default = SyncProvider; /***/ }), -/* 649 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsScandir = __webpack_require__(634); -const common = __webpack_require__(645); -const reader_1 = __webpack_require__(646); +const fsScandir = __webpack_require__(633); +const common = __webpack_require__(644); +const reader_1 = __webpack_require__(645); class SyncReader extends reader_1.default { constructor() { super(...arguments); @@ -75071,14 +74996,14 @@ exports.default = SyncReader; /***/ }), -/* 650 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const fsScandir = __webpack_require__(634); +const fsScandir = __webpack_require__(633); class Settings { constructor(_options = {}) { this._options = _options; @@ -75104,15 +75029,15 @@ exports.default = Settings; /***/ }), -/* 651 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const fsStat = __webpack_require__(626); -const utils = __webpack_require__(597); +const fsStat = __webpack_require__(625); +const utils = __webpack_require__(596); class Reader { constructor(_settings) { this._settings = _settings; @@ -75144,17 +75069,17 @@ exports.default = Reader; /***/ }), -/* 652 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const deep_1 = __webpack_require__(653); -const entry_1 = __webpack_require__(654); -const error_1 = __webpack_require__(655); -const entry_2 = __webpack_require__(656); +const deep_1 = __webpack_require__(652); +const entry_1 = __webpack_require__(653); +const error_1 = __webpack_require__(654); +const entry_2 = __webpack_require__(655); class Provider { constructor(_settings) { this._settings = _settings; @@ -75199,13 +75124,13 @@ exports.default = Provider; /***/ }), -/* 653 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(597); +const utils = __webpack_require__(596); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -75265,13 +75190,13 @@ exports.default = DeepFilter; /***/ }), -/* 654 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(597); +const utils = __webpack_require__(596); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -75326,13 +75251,13 @@ exports.default = EntryFilter; /***/ }), -/* 655 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(597); +const utils = __webpack_require__(596); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -75348,13 +75273,13 @@ exports.default = ErrorFilter; /***/ }), -/* 656 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(597); +const utils = __webpack_require__(596); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -75381,15 +75306,15 @@ exports.default = EntryTransformer; /***/ }), -/* 657 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(27); -const stream_2 = __webpack_require__(625); -const provider_1 = __webpack_require__(652); +const stream_2 = __webpack_require__(624); +const provider_1 = __webpack_require__(651); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -75417,14 +75342,14 @@ exports.default = ProviderStream; /***/ }), -/* 658 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(659); -const provider_1 = __webpack_require__(652); +const sync_1 = __webpack_require__(658); +const provider_1 = __webpack_require__(651); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -75447,15 +75372,15 @@ exports.default = ProviderSync; /***/ }), -/* 659 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(626); -const fsWalk = __webpack_require__(631); -const reader_1 = __webpack_require__(651); +const fsStat = __webpack_require__(625); +const fsWalk = __webpack_require__(630); +const reader_1 = __webpack_require__(650); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -75497,7 +75422,7 @@ exports.default = ReaderSync; /***/ }), -/* 660 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75557,13 +75482,13 @@ exports.default = Settings; /***/ }), -/* 661 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(662); +const pathType = __webpack_require__(661); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -75639,7 +75564,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 662 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75689,7 +75614,7 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 663 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75697,9 +75622,9 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); const {promisify} = __webpack_require__(29); const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(595); -const gitIgnore = __webpack_require__(664); -const slash = __webpack_require__(665); +const fastGlob = __webpack_require__(594); +const gitIgnore = __webpack_require__(663); +const slash = __webpack_require__(664); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -75813,7 +75738,7 @@ module.exports.sync = options => { /***/ }), -/* 664 */ +/* 663 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -76404,7 +76329,7 @@ if ( /***/ }), -/* 665 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76422,7 +76347,7 @@ module.exports = path => { /***/ }), -/* 666 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76475,7 +76400,7 @@ module.exports = { /***/ }), -/* 667 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76497,7 +76422,7 @@ module.exports = path_ => { /***/ }), -/* 668 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76525,7 +76450,7 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 669 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { const assert = __webpack_require__(30) @@ -76533,7 +76458,7 @@ const path = __webpack_require__(16) const fs = __webpack_require__(23) let glob = undefined try { - glob = __webpack_require__(590) + glob = __webpack_require__(589) } catch (_err) { // treat glob as optional. } @@ -76899,12 +76824,12 @@ rimraf.sync = rimrafSync /***/ }), -/* 670 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(671); +const AggregateError = __webpack_require__(670); module.exports = async ( iterable, @@ -76987,13 +76912,13 @@ module.exports = async ( /***/ }), -/* 671 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(672); -const cleanStack = __webpack_require__(673); +const indentString = __webpack_require__(671); +const cleanStack = __webpack_require__(672); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -77041,7 +76966,7 @@ module.exports = AggregateError; /***/ }), -/* 672 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77083,7 +77008,7 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 673 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77130,15 +77055,15 @@ module.exports = (stack, options) => { /***/ }), -/* 674 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(675); -const cliCursor = __webpack_require__(679); -const cliSpinners = __webpack_require__(683); -const logSymbols = __webpack_require__(564); +const chalk = __webpack_require__(674); +const cliCursor = __webpack_require__(678); +const cliSpinners = __webpack_require__(682); +const logSymbols = __webpack_require__(563); class Ora { constructor(options) { @@ -77285,16 +77210,16 @@ module.exports.promise = (action, options) => { /***/ }), -/* 675 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(3); -const ansiStyles = __webpack_require__(676); -const stdoutColor = __webpack_require__(677).stdout; +const ansiStyles = __webpack_require__(675); +const stdoutColor = __webpack_require__(676).stdout; -const template = __webpack_require__(678); +const template = __webpack_require__(677); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -77520,7 +77445,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 676 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77693,7 +77618,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 677 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77835,7 +77760,7 @@ module.exports = { /***/ }), -/* 678 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77970,12 +77895,12 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 679 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(680); +const restoreCursor = __webpack_require__(679); let hidden = false; @@ -78016,12 +77941,12 @@ exports.toggle = (force, stream) => { /***/ }), -/* 680 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const onetime = __webpack_require__(681); +const onetime = __webpack_require__(680); const signalExit = __webpack_require__(377); module.exports = onetime(() => { @@ -78032,12 +77957,12 @@ module.exports = onetime(() => { /***/ }), -/* 681 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const mimicFn = __webpack_require__(682); +const mimicFn = __webpack_require__(681); module.exports = (fn, opts) => { // TODO: Remove this in v3 @@ -78078,7 +78003,7 @@ module.exports = (fn, opts) => { /***/ }), -/* 682 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78094,22 +78019,22 @@ module.exports = (to, from) => { /***/ }), -/* 683 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(684); +module.exports = __webpack_require__(683); /***/ }), -/* 684 */ +/* 683 */ /***/ (function(module) { module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]}}"); /***/ }), -/* 685 */ +/* 684 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78169,7 +78094,7 @@ const RunCommand = { }; /***/ }), -/* 686 */ +/* 685 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78180,7 +78105,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(34); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(499); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(500); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(687); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(686); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -78264,7 +78189,7 @@ const WatchCommand = { }; /***/ }), -/* 687 */ +/* 686 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78338,7 +78263,7 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 688 */ +/* 687 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78346,15 +78271,15 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runCommand", function() { return runCommand; }); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(689); +/* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(688); /* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(indent_string__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(690); +/* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(689); /* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(wrap_ansi__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(514); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(34); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(500); -/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(697); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(698); +/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(696); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(697); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -78442,7 +78367,7 @@ function toArray(value) { } /***/ }), -/* 689 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78476,13 +78401,13 @@ module.exports = (str, count, opts) => { /***/ }), -/* 690 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringWidth = __webpack_require__(691); -const stripAnsi = __webpack_require__(695); +const stringWidth = __webpack_require__(690); +const stripAnsi = __webpack_require__(694); const ESCAPES = new Set([ '\u001B', @@ -78676,13 +78601,13 @@ module.exports = (str, cols, opts) => { /***/ }), -/* 691 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stripAnsi = __webpack_require__(692); -const isFullwidthCodePoint = __webpack_require__(694); +const stripAnsi = __webpack_require__(691); +const isFullwidthCodePoint = __webpack_require__(693); module.exports = str => { if (typeof str !== 'string' || str.length === 0) { @@ -78719,18 +78644,18 @@ module.exports = str => { /***/ }), -/* 692 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(693); +const ansiRegex = __webpack_require__(692); module.exports = input => typeof input === 'string' ? input.replace(ansiRegex(), '') : input; /***/ }), -/* 693 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78747,7 +78672,7 @@ module.exports = () => { /***/ }), -/* 694 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78800,18 +78725,18 @@ module.exports = x => { /***/ }), -/* 695 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(696); +const ansiRegex = __webpack_require__(695); module.exports = input => typeof input === 'string' ? input.replace(ansiRegex(), '') : input; /***/ }), -/* 696 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78828,7 +78753,7 @@ module.exports = () => { /***/ }), -/* 697 */ +/* 696 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78981,7 +78906,7 @@ function addProjectToTree(tree, pathParts, project) { } /***/ }), -/* 698 */ +/* 697 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78989,12 +78914,12 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Kibana", function() { return Kibana; }); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(699); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(698); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(703); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(702); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(500); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(578); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(577); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -79135,15 +79060,15 @@ class Kibana { } /***/ }), -/* 699 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const minimatch = __webpack_require__(504); -const arrayUnion = __webpack_require__(700); -const arrayDiffer = __webpack_require__(701); -const arrify = __webpack_require__(702); +const arrayUnion = __webpack_require__(699); +const arrayDiffer = __webpack_require__(700); +const arrify = __webpack_require__(701); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -79167,7 +79092,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 700 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79179,7 +79104,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 701 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79194,7 +79119,7 @@ module.exports = arrayDiffer; /***/ }), -/* 702 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79224,7 +79149,7 @@ module.exports = arrify; /***/ }), -/* 703 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79252,15 +79177,15 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 704 */ +/* 703 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(705); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(704); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(923); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(922); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -79285,19 +79210,19 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 705 */ +/* 704 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(705); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(586); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(585); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(578); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(577); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(20); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(34); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(516); @@ -79433,7 +79358,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 706 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79441,13 +79366,13 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(379); const path = __webpack_require__(16); const os = __webpack_require__(11); -const pAll = __webpack_require__(707); -const arrify = __webpack_require__(709); -const globby = __webpack_require__(710); -const isGlob = __webpack_require__(604); -const cpFile = __webpack_require__(908); -const junk = __webpack_require__(920); -const CpyError = __webpack_require__(921); +const pAll = __webpack_require__(706); +const arrify = __webpack_require__(708); +const globby = __webpack_require__(709); +const isGlob = __webpack_require__(603); +const cpFile = __webpack_require__(907); +const junk = __webpack_require__(919); +const CpyError = __webpack_require__(920); const defaultOptions = { ignoreJunk: true @@ -79566,12 +79491,12 @@ module.exports = (source, destination, { /***/ }), -/* 707 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(708); +const pMap = __webpack_require__(707); module.exports = (iterable, options) => pMap(iterable, element => element(), options); // TODO: Remove this for the next major release @@ -79579,7 +79504,7 @@ module.exports.default = module.exports; /***/ }), -/* 708 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79658,7 +79583,7 @@ module.exports.default = pMap; /***/ }), -/* 709 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79688,17 +79613,17 @@ module.exports = arrify; /***/ }), -/* 710 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const arrayUnion = __webpack_require__(711); -const glob = __webpack_require__(713); -const fastGlob = __webpack_require__(718); -const dirGlob = __webpack_require__(901); -const gitignore = __webpack_require__(904); +const arrayUnion = __webpack_require__(710); +const glob = __webpack_require__(712); +const fastGlob = __webpack_require__(717); +const dirGlob = __webpack_require__(900); +const gitignore = __webpack_require__(903); const DEFAULT_FILTER = () => false; @@ -79843,12 +79768,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 711 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(712); +var arrayUniq = __webpack_require__(711); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -79856,7 +79781,7 @@ module.exports = function () { /***/ }), -/* 712 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79925,7 +79850,7 @@ if ('Set' in global) { /***/ }), -/* 713 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -79974,13 +79899,13 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(502) var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(714) +var inherits = __webpack_require__(713) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(510) -var globSync = __webpack_require__(716) -var common = __webpack_require__(717) +var globSync = __webpack_require__(715) +var common = __webpack_require__(716) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -80721,7 +80646,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 714 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -80731,12 +80656,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(715); + module.exports = __webpack_require__(714); } /***/ }), -/* 715 */ +/* 714 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -80769,7 +80694,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 716 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync @@ -80779,12 +80704,12 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(502) var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(713).Glob +var Glob = __webpack_require__(712).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(510) -var common = __webpack_require__(717) +var common = __webpack_require__(716) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -81261,7 +81186,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 717 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -81507,10 +81432,10 @@ function childrenIgnored (self, path) { /***/ }), -/* 718 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(719); +const pkg = __webpack_require__(718); module.exports = pkg.async; module.exports.default = pkg.async; @@ -81523,19 +81448,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 719 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(720); -var taskManager = __webpack_require__(721); -var reader_async_1 = __webpack_require__(872); -var reader_stream_1 = __webpack_require__(896); -var reader_sync_1 = __webpack_require__(897); -var arrayUtils = __webpack_require__(899); -var streamUtils = __webpack_require__(900); +var optionsManager = __webpack_require__(719); +var taskManager = __webpack_require__(720); +var reader_async_1 = __webpack_require__(871); +var reader_stream_1 = __webpack_require__(895); +var reader_sync_1 = __webpack_require__(896); +var arrayUtils = __webpack_require__(898); +var streamUtils = __webpack_require__(899); /** * Synchronous API. */ @@ -81601,7 +81526,7 @@ function isString(source) { /***/ }), -/* 720 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81639,13 +81564,13 @@ exports.prepare = prepare; /***/ }), -/* 721 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(722); +var patternUtils = __webpack_require__(721); /** * Generate tasks based on parent directory of each pattern. */ @@ -81736,16 +81661,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 722 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var globParent = __webpack_require__(723); -var isGlob = __webpack_require__(726); -var micromatch = __webpack_require__(727); +var globParent = __webpack_require__(722); +var isGlob = __webpack_require__(725); +var micromatch = __webpack_require__(726); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -81891,15 +81816,15 @@ exports.matchAny = matchAny; /***/ }), -/* 723 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(16); -var isglob = __webpack_require__(724); -var pathDirname = __webpack_require__(725); +var isglob = __webpack_require__(723); +var pathDirname = __webpack_require__(724); var isWin32 = __webpack_require__(11).platform() === 'win32'; module.exports = function globParent(str) { @@ -81922,7 +81847,7 @@ module.exports = function globParent(str) { /***/ }), -/* 724 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -81932,7 +81857,7 @@ module.exports = function globParent(str) { * Licensed under the MIT License. */ -var isExtglob = __webpack_require__(605); +var isExtglob = __webpack_require__(604); module.exports = function isGlob(str) { if (typeof str !== 'string' || str === '') { @@ -81953,7 +81878,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 725 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82103,7 +82028,7 @@ module.exports.win32 = win32; /***/ }), -/* 726 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -82113,7 +82038,7 @@ module.exports.win32 = win32; * Released under the MIT License. */ -var isExtglob = __webpack_require__(605); +var isExtglob = __webpack_require__(604); var chars = { '{': '}', '(': ')', '[': ']'}; module.exports = function isGlob(str, options) { @@ -82155,7 +82080,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 727 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82166,18 +82091,18 @@ module.exports = function isGlob(str, options) { */ var util = __webpack_require__(29); -var braces = __webpack_require__(728); -var toRegex = __webpack_require__(830); -var extend = __webpack_require__(838); +var braces = __webpack_require__(727); +var toRegex = __webpack_require__(829); +var extend = __webpack_require__(837); /** * Local dependencies */ -var compilers = __webpack_require__(841); -var parsers = __webpack_require__(868); -var cache = __webpack_require__(869); -var utils = __webpack_require__(870); +var compilers = __webpack_require__(840); +var parsers = __webpack_require__(867); +var cache = __webpack_require__(868); +var utils = __webpack_require__(869); var MAX_LENGTH = 1024 * 64; /** @@ -83039,7 +82964,7 @@ module.exports = micromatch; /***/ }), -/* 728 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83049,18 +82974,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(729); -var unique = __webpack_require__(741); -var extend = __webpack_require__(738); +var toRegex = __webpack_require__(728); +var unique = __webpack_require__(740); +var extend = __webpack_require__(737); /** * Local dependencies */ -var compilers = __webpack_require__(742); -var parsers = __webpack_require__(757); -var Braces = __webpack_require__(767); -var utils = __webpack_require__(743); +var compilers = __webpack_require__(741); +var parsers = __webpack_require__(756); +var Braces = __webpack_require__(766); +var utils = __webpack_require__(742); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -83364,15 +83289,15 @@ module.exports = braces; /***/ }), -/* 729 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(730); -var extend = __webpack_require__(738); -var not = __webpack_require__(740); +var define = __webpack_require__(729); +var extend = __webpack_require__(737); +var not = __webpack_require__(739); var MAX_LENGTH = 1024 * 64; /** @@ -83519,7 +83444,7 @@ module.exports.makeRe = makeRe; /***/ }), -/* 730 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83532,7 +83457,7 @@ module.exports.makeRe = makeRe; -var isDescriptor = __webpack_require__(731); +var isDescriptor = __webpack_require__(730); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -83557,7 +83482,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 731 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83570,9 +83495,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(732); -var isAccessor = __webpack_require__(733); -var isData = __webpack_require__(736); +var typeOf = __webpack_require__(731); +var isAccessor = __webpack_require__(732); +var isData = __webpack_require__(735); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -83586,7 +83511,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 732 */ +/* 731 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -83739,7 +83664,7 @@ function isBuffer(val) { /***/ }), -/* 733 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83752,7 +83677,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(734); +var typeOf = __webpack_require__(733); // accessor descriptor properties var accessor = { @@ -83815,10 +83740,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 734 */ +/* 733 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(734); var toString = Object.prototype.toString; /** @@ -83937,7 +83862,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 735 */ +/* 734 */ /***/ (function(module, exports) { /*! @@ -83964,7 +83889,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 736 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83977,7 +83902,7 @@ function isSlowBuffer (obj) { -var typeOf = __webpack_require__(737); +var typeOf = __webpack_require__(736); // data descriptor properties var data = { @@ -84026,10 +83951,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 737 */ +/* 736 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(734); var toString = Object.prototype.toString; /** @@ -84148,13 +84073,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 738 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(739); +var isObject = __webpack_require__(738); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -84188,7 +84113,7 @@ function hasOwn(obj, key) { /***/ }), -/* 739 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84208,13 +84133,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 740 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(738); +var extend = __webpack_require__(737); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -84281,7 +84206,7 @@ module.exports = toRegex; /***/ }), -/* 741 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84331,13 +84256,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 742 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(743); +var utils = __webpack_require__(742); module.exports = function(braces, options) { braces.compiler @@ -84620,25 +84545,25 @@ function hasQueue(node) { /***/ }), -/* 743 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(744); +var splitString = __webpack_require__(743); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(738); -utils.flatten = __webpack_require__(750); -utils.isObject = __webpack_require__(748); -utils.fillRange = __webpack_require__(751); -utils.repeat = __webpack_require__(756); -utils.unique = __webpack_require__(741); +utils.extend = __webpack_require__(737); +utils.flatten = __webpack_require__(749); +utils.isObject = __webpack_require__(747); +utils.fillRange = __webpack_require__(750); +utils.repeat = __webpack_require__(755); +utils.unique = __webpack_require__(740); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -84970,7 +84895,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 744 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84983,7 +84908,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(745); +var extend = __webpack_require__(744); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -85148,14 +85073,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 745 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(746); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(745); +var assignSymbols = __webpack_require__(748); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -85215,7 +85140,7 @@ function isEnum(obj, key) { /***/ }), -/* 746 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85228,7 +85153,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(746); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -85236,7 +85161,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 747 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85249,7 +85174,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(747); function isObjectObject(o) { return isObject(o) === true @@ -85280,7 +85205,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 748 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85299,7 +85224,7 @@ module.exports = function isObject(val) { /***/ }), -/* 749 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85346,7 +85271,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 750 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85375,7 +85300,7 @@ function flat(arr, res) { /***/ }), -/* 751 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85389,10 +85314,10 @@ function flat(arr, res) { var util = __webpack_require__(29); -var isNumber = __webpack_require__(752); -var extend = __webpack_require__(738); -var repeat = __webpack_require__(754); -var toRegex = __webpack_require__(755); +var isNumber = __webpack_require__(751); +var extend = __webpack_require__(737); +var repeat = __webpack_require__(753); +var toRegex = __webpack_require__(754); /** * Return a range of numbers or letters. @@ -85590,7 +85515,7 @@ module.exports = fillRange; /***/ }), -/* 752 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85603,7 +85528,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(752); module.exports = function isNumber(num) { var type = typeOf(num); @@ -85619,10 +85544,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 753 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(734); var toString = Object.prototype.toString; /** @@ -85741,7 +85666,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 754 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85818,7 +85743,7 @@ function repeat(str, num) { /***/ }), -/* 755 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85831,8 +85756,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(754); -var isNumber = __webpack_require__(752); +var repeat = __webpack_require__(753); +var isNumber = __webpack_require__(751); var cache = {}; function toRegexRange(min, max, options) { @@ -86119,7 +86044,7 @@ module.exports = toRegexRange; /***/ }), -/* 756 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86144,14 +86069,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 757 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(758); -var utils = __webpack_require__(743); +var Node = __webpack_require__(757); +var utils = __webpack_require__(742); /** * Braces parsers @@ -86511,15 +86436,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 758 */ +/* 757 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(748); -var define = __webpack_require__(759); -var utils = __webpack_require__(766); +var isObject = __webpack_require__(747); +var define = __webpack_require__(758); +var utils = __webpack_require__(765); var ownNames; /** @@ -87010,7 +86935,7 @@ exports = module.exports = Node; /***/ }), -/* 759 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87023,7 +86948,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(759); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -87048,7 +86973,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 760 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87061,9 +86986,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(761); -var isAccessor = __webpack_require__(762); -var isData = __webpack_require__(764); +var typeOf = __webpack_require__(760); +var isAccessor = __webpack_require__(761); +var isData = __webpack_require__(763); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -87077,7 +87002,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 761 */ +/* 760 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87212,7 +87137,7 @@ function isBuffer(val) { /***/ }), -/* 762 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87225,7 +87150,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(763); +var typeOf = __webpack_require__(762); // accessor descriptor properties var accessor = { @@ -87288,7 +87213,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 763 */ +/* 762 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87423,7 +87348,7 @@ function isBuffer(val) { /***/ }), -/* 764 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87436,7 +87361,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(765); +var typeOf = __webpack_require__(764); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -87479,7 +87404,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 765 */ +/* 764 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87614,13 +87539,13 @@ function isBuffer(val) { /***/ }), -/* 766 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(752); var utils = module.exports; /** @@ -88640,17 +88565,17 @@ function assert(val, message) { /***/ }), -/* 767 */ +/* 766 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(738); -var Snapdragon = __webpack_require__(768); -var compilers = __webpack_require__(742); -var parsers = __webpack_require__(757); -var utils = __webpack_require__(743); +var extend = __webpack_require__(737); +var Snapdragon = __webpack_require__(767); +var compilers = __webpack_require__(741); +var parsers = __webpack_require__(756); +var utils = __webpack_require__(742); /** * Customize Snapdragon parser and renderer @@ -88751,17 +88676,17 @@ module.exports = Braces; /***/ }), -/* 768 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(769); -var define = __webpack_require__(730); -var Compiler = __webpack_require__(798); -var Parser = __webpack_require__(827); -var utils = __webpack_require__(807); +var Base = __webpack_require__(768); +var define = __webpack_require__(729); +var Compiler = __webpack_require__(797); +var Parser = __webpack_require__(826); +var utils = __webpack_require__(806); var regexCache = {}; var cache = {}; @@ -88932,20 +88857,20 @@ module.exports.Parser = Parser; /***/ }), -/* 769 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var define = __webpack_require__(770); -var CacheBase = __webpack_require__(771); -var Emitter = __webpack_require__(772); -var isObject = __webpack_require__(748); -var merge = __webpack_require__(789); -var pascal = __webpack_require__(792); -var cu = __webpack_require__(793); +var define = __webpack_require__(769); +var CacheBase = __webpack_require__(770); +var Emitter = __webpack_require__(771); +var isObject = __webpack_require__(747); +var merge = __webpack_require__(788); +var pascal = __webpack_require__(791); +var cu = __webpack_require__(792); /** * Optionally define a custom `cache` namespace to use. @@ -89374,7 +89299,7 @@ module.exports.namespace = namespace; /***/ }), -/* 770 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89387,7 +89312,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(759); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -89412,21 +89337,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 771 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(748); -var Emitter = __webpack_require__(772); -var visit = __webpack_require__(773); -var toPath = __webpack_require__(776); -var union = __webpack_require__(777); -var del = __webpack_require__(781); -var get = __webpack_require__(779); -var has = __webpack_require__(786); -var set = __webpack_require__(780); +var isObject = __webpack_require__(747); +var Emitter = __webpack_require__(771); +var visit = __webpack_require__(772); +var toPath = __webpack_require__(775); +var union = __webpack_require__(776); +var del = __webpack_require__(780); +var get = __webpack_require__(778); +var has = __webpack_require__(785); +var set = __webpack_require__(779); /** * Create a `Cache` constructor that when instantiated will @@ -89680,7 +89605,7 @@ module.exports.namespace = namespace; /***/ }), -/* 772 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { @@ -89849,7 +89774,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 773 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89862,8 +89787,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(774); -var mapVisit = __webpack_require__(775); +var visit = __webpack_require__(773); +var mapVisit = __webpack_require__(774); module.exports = function(collection, method, val) { var result; @@ -89886,7 +89811,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 774 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89899,7 +89824,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(747); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -89926,14 +89851,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 775 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var visit = __webpack_require__(774); +var visit = __webpack_require__(773); /** * Map `visit` over an array of objects. @@ -89970,7 +89895,7 @@ function isObject(val) { /***/ }), -/* 776 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89983,7 +89908,7 @@ function isObject(val) { -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(752); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -90010,16 +89935,16 @@ function filter(arr) { /***/ }), -/* 777 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(739); -var union = __webpack_require__(778); -var get = __webpack_require__(779); -var set = __webpack_require__(780); +var isObject = __webpack_require__(738); +var union = __webpack_require__(777); +var get = __webpack_require__(778); +var set = __webpack_require__(779); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -90047,7 +89972,7 @@ function arrayify(val) { /***/ }), -/* 778 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90083,7 +90008,7 @@ module.exports = function union(init) { /***/ }), -/* 779 */ +/* 778 */ /***/ (function(module, exports) { /*! @@ -90139,7 +90064,7 @@ function toString(val) { /***/ }), -/* 780 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90152,10 +90077,10 @@ function toString(val) { -var split = __webpack_require__(744); -var extend = __webpack_require__(738); -var isPlainObject = __webpack_require__(747); -var isObject = __webpack_require__(739); +var split = __webpack_require__(743); +var extend = __webpack_require__(737); +var isPlainObject = __webpack_require__(746); +var isObject = __webpack_require__(738); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -90201,7 +90126,7 @@ function isValidKey(key) { /***/ }), -/* 781 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90214,8 +90139,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(748); -var has = __webpack_require__(782); +var isObject = __webpack_require__(747); +var has = __webpack_require__(781); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -90240,7 +90165,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 782 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90253,9 +90178,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(783); -var hasValues = __webpack_require__(785); -var get = __webpack_require__(779); +var isObject = __webpack_require__(782); +var hasValues = __webpack_require__(784); +var get = __webpack_require__(778); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -90266,7 +90191,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 783 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90279,7 +90204,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(784); +var isArray = __webpack_require__(783); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -90287,7 +90212,7 @@ module.exports = function isObject(val) { /***/ }), -/* 784 */ +/* 783 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -90298,7 +90223,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 785 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90341,7 +90266,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 786 */ +/* 785 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90354,9 +90279,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(748); -var hasValues = __webpack_require__(787); -var get = __webpack_require__(779); +var isObject = __webpack_require__(747); +var hasValues = __webpack_require__(786); +var get = __webpack_require__(778); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -90364,7 +90289,7 @@ module.exports = function(val, prop) { /***/ }), -/* 787 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90377,8 +90302,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(788); -var isNumber = __webpack_require__(752); +var typeOf = __webpack_require__(787); +var isNumber = __webpack_require__(751); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -90431,10 +90356,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 788 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(734); var toString = Object.prototype.toString; /** @@ -90556,14 +90481,14 @@ module.exports = function kindOf(val) { /***/ }), -/* 789 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(790); -var forIn = __webpack_require__(791); +var isExtendable = __webpack_require__(789); +var forIn = __webpack_require__(790); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -90627,7 +90552,7 @@ module.exports = mixinDeep; /***/ }), -/* 790 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90640,7 +90565,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(746); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -90648,7 +90573,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 791 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90671,7 +90596,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 792 */ +/* 791 */ /***/ (function(module, exports) { /*! @@ -90698,14 +90623,14 @@ module.exports = pascalcase; /***/ }), -/* 793 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var utils = __webpack_require__(794); +var utils = __webpack_require__(793); /** * Expose class utils @@ -91070,7 +90995,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 794 */ +/* 793 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91084,10 +91009,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(778); -utils.define = __webpack_require__(730); -utils.isObj = __webpack_require__(748); -utils.staticExtend = __webpack_require__(795); +utils.union = __webpack_require__(777); +utils.define = __webpack_require__(729); +utils.isObj = __webpack_require__(747); +utils.staticExtend = __webpack_require__(794); /** @@ -91098,7 +91023,7 @@ module.exports = utils; /***/ }), -/* 795 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91111,8 +91036,8 @@ module.exports = utils; -var copy = __webpack_require__(796); -var define = __webpack_require__(730); +var copy = __webpack_require__(795); +var define = __webpack_require__(729); var util = __webpack_require__(29); /** @@ -91195,15 +91120,15 @@ module.exports = extend; /***/ }), -/* 796 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(753); -var copyDescriptor = __webpack_require__(797); -var define = __webpack_require__(730); +var typeOf = __webpack_require__(752); +var copyDescriptor = __webpack_require__(796); +var define = __webpack_require__(729); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -91376,7 +91301,7 @@ module.exports.has = has; /***/ }), -/* 797 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91464,16 +91389,16 @@ function isObject(val) { /***/ }), -/* 798 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(799); -var define = __webpack_require__(730); -var debug = __webpack_require__(801)('snapdragon:compiler'); -var utils = __webpack_require__(807); +var use = __webpack_require__(798); +var define = __webpack_require__(729); +var debug = __webpack_require__(800)('snapdragon:compiler'); +var utils = __webpack_require__(806); /** * Create a new `Compiler` with the given `options`. @@ -91627,7 +91552,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(826); + var sourcemaps = __webpack_require__(825); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -91648,7 +91573,7 @@ module.exports = Compiler; /***/ }), -/* 799 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91661,7 +91586,7 @@ module.exports = Compiler; -var utils = __webpack_require__(800); +var utils = __webpack_require__(799); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -91776,7 +91701,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 800 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91790,8 +91715,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(730); -utils.isObject = __webpack_require__(748); +utils.define = __webpack_require__(729); +utils.isObject = __webpack_require__(747); utils.isString = function(val) { @@ -91806,7 +91731,7 @@ module.exports = utils; /***/ }), -/* 801 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91815,14 +91740,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(802); + module.exports = __webpack_require__(801); } else { - module.exports = __webpack_require__(805); + module.exports = __webpack_require__(804); } /***/ }), -/* 802 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91831,7 +91756,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(803); +exports = module.exports = __webpack_require__(802); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -92013,7 +91938,7 @@ function localstorage() { /***/ }), -/* 803 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { @@ -92029,7 +91954,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(804); +exports.humanize = __webpack_require__(803); /** * The currently active debug mode names, and names to skip. @@ -92221,7 +92146,7 @@ function coerce(val) { /***/ }), -/* 804 */ +/* 803 */ /***/ (function(module, exports) { /** @@ -92379,7 +92304,7 @@ function plural(ms, n, name) { /***/ }), -/* 805 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -92395,7 +92320,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(803); +exports = module.exports = __webpack_require__(802); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -92574,7 +92499,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(806); + var net = __webpack_require__(805); stream = new net.Socket({ fd: fd, readable: false, @@ -92633,13 +92558,13 @@ exports.enable(load()); /***/ }), -/* 806 */ +/* 805 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 807 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92649,9 +92574,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(738); -exports.SourceMap = __webpack_require__(808); -exports.sourceMapResolve = __webpack_require__(819); +exports.extend = __webpack_require__(737); +exports.SourceMap = __webpack_require__(807); +exports.sourceMapResolve = __webpack_require__(818); /** * Convert backslash in the given string to forward slashes @@ -92694,7 +92619,7 @@ exports.last = function(arr, n) { /***/ }), -/* 808 */ +/* 807 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -92702,13 +92627,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(809).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(815).SourceMapConsumer; -exports.SourceNode = __webpack_require__(818).SourceNode; +exports.SourceMapGenerator = __webpack_require__(808).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(814).SourceMapConsumer; +exports.SourceNode = __webpack_require__(817).SourceNode; /***/ }), -/* 809 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -92718,10 +92643,10 @@ exports.SourceNode = __webpack_require__(818).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(810); -var util = __webpack_require__(812); -var ArraySet = __webpack_require__(813).ArraySet; -var MappingList = __webpack_require__(814).MappingList; +var base64VLQ = __webpack_require__(809); +var util = __webpack_require__(811); +var ArraySet = __webpack_require__(812).ArraySet; +var MappingList = __webpack_require__(813).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -93130,7 +93055,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 810 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93170,7 +93095,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(811); +var base64 = __webpack_require__(810); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -93276,7 +93201,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 811 */ +/* 810 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93349,7 +93274,7 @@ exports.decode = function (charCode) { /***/ }), -/* 812 */ +/* 811 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93772,7 +93697,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 813 */ +/* 812 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93782,7 +93707,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); +var util = __webpack_require__(811); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -93899,7 +93824,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 814 */ +/* 813 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93909,7 +93834,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); +var util = __webpack_require__(811); /** * Determine whether mappingB is after mappingA with respect to generated @@ -93984,7 +93909,7 @@ exports.MappingList = MappingList; /***/ }), -/* 815 */ +/* 814 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93994,11 +93919,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); -var binarySearch = __webpack_require__(816); -var ArraySet = __webpack_require__(813).ArraySet; -var base64VLQ = __webpack_require__(810); -var quickSort = __webpack_require__(817).quickSort; +var util = __webpack_require__(811); +var binarySearch = __webpack_require__(815); +var ArraySet = __webpack_require__(812).ArraySet; +var base64VLQ = __webpack_require__(809); +var quickSort = __webpack_require__(816).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -95072,7 +94997,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 816 */ +/* 815 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95189,7 +95114,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 817 */ +/* 816 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95309,7 +95234,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 818 */ +/* 817 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95319,8 +95244,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(809).SourceMapGenerator; -var util = __webpack_require__(812); +var SourceMapGenerator = __webpack_require__(808).SourceMapGenerator; +var util = __webpack_require__(811); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -95728,17 +95653,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 819 */ +/* 818 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(820) -var resolveUrl = __webpack_require__(821) -var decodeUriComponent = __webpack_require__(822) -var urix = __webpack_require__(824) -var atob = __webpack_require__(825) +var sourceMappingURL = __webpack_require__(819) +var resolveUrl = __webpack_require__(820) +var decodeUriComponent = __webpack_require__(821) +var urix = __webpack_require__(823) +var atob = __webpack_require__(824) @@ -96036,7 +95961,7 @@ module.exports = { /***/ }), -/* 820 */ +/* 819 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -96099,7 +96024,7 @@ void (function(root, factory) { /***/ }), -/* 821 */ +/* 820 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96117,13 +96042,13 @@ module.exports = resolveUrl /***/ }), -/* 822 */ +/* 821 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(823) +var decodeUriComponent = __webpack_require__(822) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -96134,7 +96059,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 823 */ +/* 822 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96235,7 +96160,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 824 */ +/* 823 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96258,7 +96183,7 @@ module.exports = urix /***/ }), -/* 825 */ +/* 824 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96272,7 +96197,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 826 */ +/* 825 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96280,8 +96205,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(23); var path = __webpack_require__(16); -var define = __webpack_require__(730); -var utils = __webpack_require__(807); +var define = __webpack_require__(729); +var utils = __webpack_require__(806); /** * Expose `mixin()`. @@ -96424,19 +96349,19 @@ exports.comment = function(node) { /***/ }), -/* 827 */ +/* 826 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(799); +var use = __webpack_require__(798); var util = __webpack_require__(29); -var Cache = __webpack_require__(828); -var define = __webpack_require__(730); -var debug = __webpack_require__(801)('snapdragon:parser'); -var Position = __webpack_require__(829); -var utils = __webpack_require__(807); +var Cache = __webpack_require__(827); +var define = __webpack_require__(729); +var debug = __webpack_require__(800)('snapdragon:parser'); +var Position = __webpack_require__(828); +var utils = __webpack_require__(806); /** * Create a new `Parser` with the given `input` and `options`. @@ -96964,7 +96889,7 @@ module.exports = Parser; /***/ }), -/* 828 */ +/* 827 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97071,13 +96996,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 829 */ +/* 828 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(730); +var define = __webpack_require__(729); /** * Store position for a node @@ -97092,16 +97017,16 @@ module.exports = function Position(start, parser) { /***/ }), -/* 830 */ +/* 829 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(831); -var define = __webpack_require__(837); -var extend = __webpack_require__(838); -var not = __webpack_require__(840); +var safe = __webpack_require__(830); +var define = __webpack_require__(836); +var extend = __webpack_require__(837); +var not = __webpack_require__(839); var MAX_LENGTH = 1024 * 64; /** @@ -97254,10 +97179,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 831 */ +/* 830 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(832); +var parse = __webpack_require__(831); var types = parse.types; module.exports = function (re, opts) { @@ -97303,13 +97228,13 @@ function isRegExp (x) { /***/ }), -/* 832 */ +/* 831 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(833); -var types = __webpack_require__(834); -var sets = __webpack_require__(835); -var positions = __webpack_require__(836); +var util = __webpack_require__(832); +var types = __webpack_require__(833); +var sets = __webpack_require__(834); +var positions = __webpack_require__(835); module.exports = function(regexpStr) { @@ -97591,11 +97516,11 @@ module.exports.types = types; /***/ }), -/* 833 */ +/* 832 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); -var sets = __webpack_require__(835); +var types = __webpack_require__(833); +var sets = __webpack_require__(834); // All of these are private and only used by randexp. @@ -97708,7 +97633,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 834 */ +/* 833 */ /***/ (function(module, exports) { module.exports = { @@ -97724,10 +97649,10 @@ module.exports = { /***/ }), -/* 835 */ +/* 834 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); +var types = __webpack_require__(833); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -97812,10 +97737,10 @@ exports.anyChar = function() { /***/ }), -/* 836 */ +/* 835 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); +var types = __webpack_require__(833); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -97835,7 +97760,7 @@ exports.end = function() { /***/ }), -/* 837 */ +/* 836 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97848,8 +97773,8 @@ exports.end = function() { -var isobject = __webpack_require__(748); -var isDescriptor = __webpack_require__(760); +var isobject = __webpack_require__(747); +var isDescriptor = __webpack_require__(759); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -97880,14 +97805,14 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 838 */ +/* 837 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(839); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(838); +var assignSymbols = __webpack_require__(748); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -97947,7 +97872,7 @@ function isEnum(obj, key) { /***/ }), -/* 839 */ +/* 838 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97960,7 +97885,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(746); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -97968,14 +97893,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 840 */ +/* 839 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(838); -var safe = __webpack_require__(831); +var extend = __webpack_require__(837); +var safe = __webpack_require__(830); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -98047,14 +97972,14 @@ module.exports = toRegex; /***/ }), -/* 841 */ +/* 840 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(842); -var extglob = __webpack_require__(857); +var nanomatch = __webpack_require__(841); +var extglob = __webpack_require__(856); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -98131,7 +98056,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 842 */ +/* 841 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98142,17 +98067,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(29); -var toRegex = __webpack_require__(729); -var extend = __webpack_require__(843); +var toRegex = __webpack_require__(728); +var extend = __webpack_require__(842); /** * Local dependencies */ -var compilers = __webpack_require__(845); -var parsers = __webpack_require__(846); -var cache = __webpack_require__(849); -var utils = __webpack_require__(851); +var compilers = __webpack_require__(844); +var parsers = __webpack_require__(845); +var cache = __webpack_require__(848); +var utils = __webpack_require__(850); var MAX_LENGTH = 1024 * 64; /** @@ -98976,14 +98901,14 @@ module.exports = nanomatch; /***/ }), -/* 843 */ +/* 842 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(844); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(843); +var assignSymbols = __webpack_require__(748); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -99043,7 +98968,7 @@ function isEnum(obj, key) { /***/ }), -/* 844 */ +/* 843 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99056,7 +98981,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(746); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -99064,7 +98989,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 845 */ +/* 844 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99410,15 +99335,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 846 */ +/* 845 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(740); -var toRegex = __webpack_require__(729); -var isOdd = __webpack_require__(847); +var regexNot = __webpack_require__(739); +var toRegex = __webpack_require__(728); +var isOdd = __webpack_require__(846); /** * Characters to use in negation regex (we want to "not" match @@ -99804,7 +99729,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 847 */ +/* 846 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99817,7 +99742,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(848); +var isNumber = __webpack_require__(847); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -99831,7 +99756,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 848 */ +/* 847 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99859,14 +99784,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 849 */ +/* 848 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(850))(); +module.exports = new (__webpack_require__(849))(); /***/ }), -/* 850 */ +/* 849 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99879,7 +99804,7 @@ module.exports = new (__webpack_require__(850))(); -var MapCache = __webpack_require__(828); +var MapCache = __webpack_require__(827); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -100001,7 +99926,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 851 */ +/* 850 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100014,14 +99939,14 @@ var path = __webpack_require__(16); * Module dependencies */ -var isWindows = __webpack_require__(852)(); -var Snapdragon = __webpack_require__(768); -utils.define = __webpack_require__(853); -utils.diff = __webpack_require__(854); -utils.extend = __webpack_require__(843); -utils.pick = __webpack_require__(855); -utils.typeOf = __webpack_require__(856); -utils.unique = __webpack_require__(741); +var isWindows = __webpack_require__(851)(); +var Snapdragon = __webpack_require__(767); +utils.define = __webpack_require__(852); +utils.diff = __webpack_require__(853); +utils.extend = __webpack_require__(842); +utils.pick = __webpack_require__(854); +utils.typeOf = __webpack_require__(855); +utils.unique = __webpack_require__(740); /** * Returns true if the given value is effectively an empty string @@ -100387,7 +100312,7 @@ utils.unixify = function(options) { /***/ }), -/* 852 */ +/* 851 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -100415,7 +100340,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 853 */ +/* 852 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100428,8 +100353,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(748); -var isDescriptor = __webpack_require__(760); +var isobject = __webpack_require__(747); +var isDescriptor = __webpack_require__(759); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -100460,7 +100385,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 854 */ +/* 853 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100514,7 +100439,7 @@ function diffArray(one, two) { /***/ }), -/* 855 */ +/* 854 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100527,7 +100452,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(747); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -100556,7 +100481,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 856 */ +/* 855 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -100691,7 +100616,7 @@ function isBuffer(val) { /***/ }), -/* 857 */ +/* 856 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100701,18 +100626,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(738); -var unique = __webpack_require__(741); -var toRegex = __webpack_require__(729); +var extend = __webpack_require__(737); +var unique = __webpack_require__(740); +var toRegex = __webpack_require__(728); /** * Local dependencies */ -var compilers = __webpack_require__(858); -var parsers = __webpack_require__(864); -var Extglob = __webpack_require__(867); -var utils = __webpack_require__(866); +var compilers = __webpack_require__(857); +var parsers = __webpack_require__(863); +var Extglob = __webpack_require__(866); +var utils = __webpack_require__(865); var MAX_LENGTH = 1024 * 64; /** @@ -101029,13 +100954,13 @@ module.exports = extglob; /***/ }), -/* 858 */ +/* 857 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(859); +var brackets = __webpack_require__(858); /** * Extglob compilers @@ -101205,7 +101130,7 @@ module.exports = function(extglob) { /***/ }), -/* 859 */ +/* 858 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101215,17 +101140,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(860); -var parsers = __webpack_require__(862); +var compilers = __webpack_require__(859); +var parsers = __webpack_require__(861); /** * Module dependencies */ -var debug = __webpack_require__(801)('expand-brackets'); -var extend = __webpack_require__(738); -var Snapdragon = __webpack_require__(768); -var toRegex = __webpack_require__(729); +var debug = __webpack_require__(800)('expand-brackets'); +var extend = __webpack_require__(737); +var Snapdragon = __webpack_require__(767); +var toRegex = __webpack_require__(728); /** * Parses the given POSIX character class `pattern` and returns a @@ -101423,13 +101348,13 @@ module.exports = brackets; /***/ }), -/* 860 */ +/* 859 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(861); +var posix = __webpack_require__(860); module.exports = function(brackets) { brackets.compiler @@ -101517,7 +101442,7 @@ module.exports = function(brackets) { /***/ }), -/* 861 */ +/* 860 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101546,14 +101471,14 @@ module.exports = { /***/ }), -/* 862 */ +/* 861 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(863); -var define = __webpack_require__(730); +var utils = __webpack_require__(862); +var define = __webpack_require__(729); /** * Text regex @@ -101772,14 +101697,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 863 */ +/* 862 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(729); -var regexNot = __webpack_require__(740); +var toRegex = __webpack_require__(728); +var regexNot = __webpack_require__(739); var cached; /** @@ -101813,15 +101738,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 864 */ +/* 863 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(859); -var define = __webpack_require__(865); -var utils = __webpack_require__(866); +var brackets = __webpack_require__(858); +var define = __webpack_require__(864); +var utils = __webpack_require__(865); /** * Characters to use in text regex (we want to "not" match @@ -101976,7 +101901,7 @@ module.exports = parsers; /***/ }), -/* 865 */ +/* 864 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101989,7 +101914,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(759); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -102014,14 +101939,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 866 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(740); -var Cache = __webpack_require__(850); +var regex = __webpack_require__(739); +var Cache = __webpack_require__(849); /** * Utils @@ -102090,7 +102015,7 @@ utils.createRegex = function(str) { /***/ }), -/* 867 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102100,16 +102025,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(768); -var define = __webpack_require__(865); -var extend = __webpack_require__(738); +var Snapdragon = __webpack_require__(767); +var define = __webpack_require__(864); +var extend = __webpack_require__(737); /** * Local dependencies */ -var compilers = __webpack_require__(858); -var parsers = __webpack_require__(864); +var compilers = __webpack_require__(857); +var parsers = __webpack_require__(863); /** * Customize Snapdragon parser and renderer @@ -102175,16 +102100,16 @@ module.exports = Extglob; /***/ }), -/* 868 */ +/* 867 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(857); -var nanomatch = __webpack_require__(842); -var regexNot = __webpack_require__(740); -var toRegex = __webpack_require__(830); +var extglob = __webpack_require__(856); +var nanomatch = __webpack_require__(841); +var regexNot = __webpack_require__(739); +var toRegex = __webpack_require__(829); var not; /** @@ -102265,14 +102190,14 @@ function textRegex(pattern) { /***/ }), -/* 869 */ +/* 868 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(850))(); +module.exports = new (__webpack_require__(849))(); /***/ }), -/* 870 */ +/* 869 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102285,13 +102210,13 @@ var path = __webpack_require__(16); * Module dependencies */ -var Snapdragon = __webpack_require__(768); -utils.define = __webpack_require__(837); -utils.diff = __webpack_require__(854); -utils.extend = __webpack_require__(838); -utils.pick = __webpack_require__(855); -utils.typeOf = __webpack_require__(871); -utils.unique = __webpack_require__(741); +var Snapdragon = __webpack_require__(767); +utils.define = __webpack_require__(836); +utils.diff = __webpack_require__(853); +utils.extend = __webpack_require__(837); +utils.pick = __webpack_require__(854); +utils.typeOf = __webpack_require__(870); +utils.unique = __webpack_require__(740); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -102588,7 +102513,7 @@ utils.unixify = function(options) { /***/ }), -/* 871 */ +/* 870 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -102723,7 +102648,7 @@ function isBuffer(val) { /***/ }), -/* 872 */ +/* 871 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102742,9 +102667,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(873); -var reader_1 = __webpack_require__(886); -var fs_stream_1 = __webpack_require__(890); +var readdir = __webpack_require__(872); +var reader_1 = __webpack_require__(885); +var fs_stream_1 = __webpack_require__(889); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -102805,15 +102730,15 @@ exports.default = ReaderAsync; /***/ }), -/* 873 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(874); -const readdirAsync = __webpack_require__(882); -const readdirStream = __webpack_require__(885); +const readdirSync = __webpack_require__(873); +const readdirAsync = __webpack_require__(881); +const readdirStream = __webpack_require__(884); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -102897,7 +102822,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 874 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102905,11 +102830,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(875); +const DirectoryReader = __webpack_require__(874); let syncFacade = { - fs: __webpack_require__(880), - forEach: __webpack_require__(881), + fs: __webpack_require__(879), + forEach: __webpack_require__(880), sync: true }; @@ -102938,7 +102863,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 875 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102947,9 +102872,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; const EventEmitter = __webpack_require__(379).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(876); -const stat = __webpack_require__(878); -const call = __webpack_require__(879); +const normalizeOptions = __webpack_require__(875); +const stat = __webpack_require__(877); +const call = __webpack_require__(878); /** * Asynchronously reads the contents of a directory and streams the results @@ -103325,14 +103250,14 @@ module.exports = DirectoryReader; /***/ }), -/* 876 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(877); +const globToRegExp = __webpack_require__(876); module.exports = normalizeOptions; @@ -103509,7 +103434,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 877 */ +/* 876 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -103646,13 +103571,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 878 */ +/* 877 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(879); +const call = __webpack_require__(878); module.exports = stat; @@ -103727,7 +103652,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 879 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103788,14 +103713,14 @@ function callOnce (fn) { /***/ }), -/* 880 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(879); +const call = __webpack_require__(878); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -103859,7 +103784,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 881 */ +/* 880 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103888,7 +103813,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 882 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103896,12 +103821,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(883); -const DirectoryReader = __webpack_require__(875); +const maybe = __webpack_require__(882); +const DirectoryReader = __webpack_require__(874); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(884), + forEach: __webpack_require__(883), async: true }; @@ -103943,7 +103868,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 883 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103970,7 +103895,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 884 */ +/* 883 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104006,7 +103931,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 885 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104014,11 +103939,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(875); +const DirectoryReader = __webpack_require__(874); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(884), + forEach: __webpack_require__(883), async: true }; @@ -104038,16 +103963,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 886 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(887); -var entry_1 = __webpack_require__(889); -var pathUtil = __webpack_require__(888); +var deep_1 = __webpack_require__(886); +var entry_1 = __webpack_require__(888); +var pathUtil = __webpack_require__(887); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -104113,14 +104038,14 @@ exports.default = Reader; /***/ }), -/* 887 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(888); -var patternUtils = __webpack_require__(722); +var pathUtils = __webpack_require__(887); +var patternUtils = __webpack_require__(721); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -104203,7 +104128,7 @@ exports.default = DeepFilter; /***/ }), -/* 888 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104234,14 +104159,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 889 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(888); -var patternUtils = __webpack_require__(722); +var pathUtils = __webpack_require__(887); +var patternUtils = __webpack_require__(721); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -104326,7 +104251,7 @@ exports.default = EntryFilter; /***/ }), -/* 890 */ +/* 889 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104346,8 +104271,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(891); -var fs_1 = __webpack_require__(895); +var fsStat = __webpack_require__(890); +var fs_1 = __webpack_require__(894); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -104397,14 +104322,14 @@ exports.default = FileSystemStream; /***/ }), -/* 891 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(892); -const statProvider = __webpack_require__(894); +const optionsManager = __webpack_require__(891); +const statProvider = __webpack_require__(893); /** * Asynchronous API. */ @@ -104435,13 +104360,13 @@ exports.statSync = statSync; /***/ }), -/* 892 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(893); +const fsAdapter = __webpack_require__(892); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -104454,7 +104379,7 @@ exports.prepare = prepare; /***/ }), -/* 893 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104477,7 +104402,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 894 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104529,7 +104454,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 895 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104560,7 +104485,7 @@ exports.default = FileSystem; /***/ }), -/* 896 */ +/* 895 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104580,9 +104505,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(873); -var reader_1 = __webpack_require__(886); -var fs_stream_1 = __webpack_require__(890); +var readdir = __webpack_require__(872); +var reader_1 = __webpack_require__(885); +var fs_stream_1 = __webpack_require__(889); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -104650,7 +104575,7 @@ exports.default = ReaderStream; /***/ }), -/* 897 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104669,9 +104594,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(873); -var reader_1 = __webpack_require__(886); -var fs_sync_1 = __webpack_require__(898); +var readdir = __webpack_require__(872); +var reader_1 = __webpack_require__(885); +var fs_sync_1 = __webpack_require__(897); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -104731,7 +104656,7 @@ exports.default = ReaderSync; /***/ }), -/* 898 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104750,8 +104675,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(891); -var fs_1 = __webpack_require__(895); +var fsStat = __webpack_require__(890); +var fs_1 = __webpack_require__(894); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -104797,7 +104722,7 @@ exports.default = FileSystemSync; /***/ }), -/* 899 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104813,13 +104738,13 @@ exports.flatten = flatten; /***/ }), -/* 900 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var merge2 = __webpack_require__(589); +var merge2 = __webpack_require__(588); /** * Merge multiple streams and propagate their errors into one stream in parallel. */ @@ -104834,13 +104759,13 @@ exports.merge = merge; /***/ }), -/* 901 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(902); +const pathType = __webpack_require__(901); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -104906,13 +104831,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 902 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(903); +const pify = __webpack_require__(902); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -104955,7 +104880,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 903 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105046,17 +104971,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 904 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(718); -const gitIgnore = __webpack_require__(905); -const pify = __webpack_require__(906); -const slash = __webpack_require__(907); +const fastGlob = __webpack_require__(717); +const gitIgnore = __webpack_require__(904); +const pify = __webpack_require__(905); +const slash = __webpack_require__(906); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -105154,7 +105079,7 @@ module.exports.sync = options => { /***/ }), -/* 905 */ +/* 904 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -105623,7 +105548,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 906 */ +/* 905 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105698,7 +105623,7 @@ module.exports = (input, options) => { /***/ }), -/* 907 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105716,17 +105641,17 @@ module.exports = input => { /***/ }), -/* 908 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const pEvent = __webpack_require__(909); -const CpFileError = __webpack_require__(912); -const fs = __webpack_require__(916); -const ProgressEmitter = __webpack_require__(919); +const pEvent = __webpack_require__(908); +const CpFileError = __webpack_require__(911); +const fs = __webpack_require__(915); +const ProgressEmitter = __webpack_require__(918); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -105840,12 +105765,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 909 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(910); +const pTimeout = __webpack_require__(909); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -106136,12 +106061,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 910 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(911); +const pFinally = __webpack_require__(910); class TimeoutError extends Error { constructor(message) { @@ -106187,7 +106112,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 911 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106209,12 +106134,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 912 */ +/* 911 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(913); +const NestedError = __webpack_require__(912); class CpFileError extends NestedError { constructor(message, nested) { @@ -106228,10 +106153,10 @@ module.exports = CpFileError; /***/ }), -/* 913 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(914); +var inherits = __webpack_require__(913); var NestedError = function (message, nested) { this.nested = nested; @@ -106282,7 +106207,7 @@ module.exports = NestedError; /***/ }), -/* 914 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -106290,12 +106215,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(915); + module.exports = __webpack_require__(914); } /***/ }), -/* 915 */ +/* 914 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -106324,16 +106249,16 @@ if (typeof Object.create === 'function') { /***/ }), -/* 916 */ +/* 915 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(917); -const pEvent = __webpack_require__(909); -const CpFileError = __webpack_require__(912); +const makeDir = __webpack_require__(916); +const pEvent = __webpack_require__(908); +const CpFileError = __webpack_require__(911); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -106430,7 +106355,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 917 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106438,7 +106363,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const {promisify} = __webpack_require__(29); -const semver = __webpack_require__(918); +const semver = __webpack_require__(917); const defaults = { mode: 0o777 & (~process.umask()), @@ -106587,7 +106512,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 918 */ +/* 917 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -108189,7 +108114,7 @@ function coerce (version, options) { /***/ }), -/* 919 */ +/* 918 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -108230,7 +108155,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 920 */ +/* 919 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -108276,12 +108201,12 @@ exports.default = module.exports; /***/ }), -/* 921 */ +/* 920 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(922); +const NestedError = __webpack_require__(921); class CpyError extends NestedError { constructor(message, nested) { @@ -108295,7 +108220,7 @@ module.exports = CpyError; /***/ }), -/* 922 */ +/* 921 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(29).inherits; @@ -108351,7 +108276,7 @@ module.exports = NestedError; /***/ }), -/* 923 */ +/* 922 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/yarn.lock b/yarn.lock index 20e33fdefc996..11abd95498c8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -61,7 +61,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.0.0", "@babel/core@^7.0.1", "@babel/core@^7.1.0", "@babel/core@^7.4.3", "@babel/core@^7.9.0": +"@babel/core@^7.0.0", "@babel/core@^7.0.1", "@babel/core@^7.1.0", "@babel/core@^7.4.3", "@babel/core@^7.7.5", "@babel/core@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e" integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w== @@ -83,7 +83,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.4.0", "@babel/generator@^7.5.5", "@babel/generator@^7.9.0": +"@babel/generator@^7.0.0", "@babel/generator@^7.5.5", "@babel/generator@^7.9.0": version "7.9.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.4.tgz#12441e90c3b3c4159cdecf312075bf1a8ce2dbce" integrity sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA== @@ -93,6 +93,16 @@ lodash "^4.17.13" source-map "^0.5.0" +"@babel/generator@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.5.tgz#27f0917741acc41e6eaaced6d68f96c3fa9afaf9" + integrity sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ== + dependencies: + "@babel/types" "^7.9.5" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" @@ -183,6 +193,15 @@ "@babel/template" "^7.8.3" "@babel/types" "^7.8.3" +"@babel/helper-function-name@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c" + integrity sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw== + dependencies: + "@babel/helper-get-function-arity" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/types" "^7.9.5" + "@babel/helper-get-function-arity@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" @@ -284,6 +303,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed" integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw== +"@babel/helper-validator-identifier@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" + integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== + "@babel/helper-wrap-function@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610" @@ -312,7 +336,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.0", "@babel/parser@^7.4.3", "@babel/parser@^7.5.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0", "@babel/parser@^7.9.3": +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.0", "@babel/parser@^7.5.5", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0", "@babel/parser@^7.9.3": version "7.9.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== @@ -1101,7 +1125,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.0.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": +"@babel/template@^7.0.0", "@babel/template@^7.4.4", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== @@ -1110,7 +1134,7 @@ "@babel/parser" "^7.8.6" "@babel/types" "^7.8.6" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.5.5", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.4.5", "@babel/traverse@^7.5.5", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892" integrity sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w== @@ -1125,6 +1149,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.7.4": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.5.tgz#6e7c56b44e2ac7011a948c21e283ddd9d9db97a2" + integrity sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.9.5" + "@babel/helper-function-name" "^7.9.5" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/parser" "^7.9.0" + "@babel/types" "^7.9.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + "@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5" @@ -1134,6 +1173,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" + integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg== + dependencies: + "@babel/helper-validator-identifier" "^7.9.5" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -1542,6 +1590,21 @@ resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw== +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b" + integrity sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" + integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== + "@jest/console@^24.7.1": version "24.7.1" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545" @@ -6109,12 +6172,12 @@ append-buffer@^1.0.2: dependencies: buffer-equal "^1.0.0" -append-transform@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" - integrity sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw== +append-transform@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-2.0.0.tgz#99d9d29c7b38391e6f428d28ce136551f0b77e12" + integrity sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg== dependencies: - default-require-extensions "^2.0.0" + default-require-extensions "^3.0.0" aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" @@ -6867,15 +6930,16 @@ babel-plugin-istanbul@^5.1.0: istanbul-lib-instrument "^3.0.0" test-exclude "^5.0.0" -babel-plugin-istanbul@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854" - integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw== +babel-plugin-istanbul@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765" + integrity sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - find-up "^3.0.0" - istanbul-lib-instrument "^3.3.0" - test-exclude "^5.2.3" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^4.0.0" + test-exclude "^6.0.0" babel-plugin-jest-hoist@^24.9.0: version "24.9.0" @@ -8129,15 +8193,15 @@ cachedir@2.3.0: resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== -caching-transform@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-3.0.2.tgz#601d46b91eca87687a281e71cef99791b0efca70" - integrity sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w== +caching-transform@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-4.0.0.tgz#00d297a4206d71e2163c39eaffa8157ac0651f0f" + integrity sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA== dependencies: - hasha "^3.0.0" - make-dir "^2.0.0" - package-hash "^3.0.0" - write-file-atomic "^2.4.2" + hasha "^5.0.0" + make-dir "^3.0.0" + package-hash "^4.0.0" + write-file-atomic "^3.0.0" call-me-maybe@^1.0.1: version "1.0.1" @@ -9564,7 +9628,7 @@ convert-source-map@^0.3.3: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= -convert-source-map@^1.5.1, convert-source-map@^1.6.0: +convert-source-map@^1.5.1: version "1.6.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== @@ -9768,17 +9832,6 @@ cosmiconfig@^5.2.0, cosmiconfig@^5.2.1: js-yaml "^3.13.1" parse-json "^4.0.0" -cp-file@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-6.2.0.tgz#40d5ea4a1def2a9acdd07ba5c0b0246ef73dc10d" - integrity sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA== - dependencies: - graceful-fs "^4.1.2" - make-dir "^2.0.0" - nested-error-stacks "^2.0.0" - pify "^4.0.1" - safe-buffer "^5.0.1" - cp-file@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-7.0.0.tgz#b9454cfd07fe3b974ab9ea0e5f29655791a9b8cd" @@ -9948,7 +10001,7 @@ cross-spawn@^3.0.0: lru-cache "^4.0.1" which "^1.2.9" -cross-spawn@^4, cross-spawn@^4.0.2: +cross-spawn@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" integrity sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE= @@ -10847,12 +10900,12 @@ default-gateway@^4.2.0: execa "^1.0.0" ip-regex "^2.1.0" -default-require-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7" - integrity sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc= +default-require-extensions@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.0.tgz#e03f93aac9b2b6443fc52e5e4a37b3ad9ad8df96" + integrity sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg== dependencies: - strip-bom "^3.0.0" + strip-bom "^4.0.0" default-resolution@^2.0.0: version "2.0.0" @@ -13842,13 +13895,13 @@ foreachasync@^3.0.0: resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" integrity sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY= -foreground-child@^1.5.6: - version "1.5.6" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-1.5.6.tgz#4fd71ad2dfde96789b980a5c0a295937cb2f5ce9" - integrity sha1-T9ca0t/elnibmApcCilZN8svXOk= +foreground-child@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53" + integrity sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA== dependencies: - cross-spawn "^4" - signal-exit "^3.0.0" + cross-spawn "^7.0.0" + signal-exit "^3.0.2" forever-agent@~0.6.1: version "0.6.1" @@ -13954,6 +14007,11 @@ from2@^2.1.0, from2@^2.1.1, from2@^2.3.0: inherits "^2.0.1" readable-stream "^2.0.0" +fromentries@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.2.0.tgz#e6aa06f240d6267f913cea422075ef88b63e7897" + integrity sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ== + front-matter@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/front-matter/-/front-matter-2.1.2.tgz#f75983b9f2f413be658c93dfd7bd8ce4078f5cdb" @@ -15636,12 +15694,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" -hasha@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/hasha/-/hasha-3.0.0.tgz#52a32fab8569d41ca69a61ff1a214f8eb7c8bd39" - integrity sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk= +hasha@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.0.tgz#33094d1f69c40a4a6ac7be53d5fe3ff95a269e0c" + integrity sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw== dependencies: - is-stream "^1.0.1" + is-stream "^2.0.0" + type-fest "^0.8.0" hast-util-from-parse5@^5.0.0: version "5.0.0" @@ -15845,6 +15904,11 @@ html-entities@^1.2.0, html-entities@^1.2.1: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + html-loader@^0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-0.5.5.tgz#6356dbeb0c49756d8ebd5ca327f16ff06ab5faea" @@ -17320,7 +17384,7 @@ is-symbol@^1.0.2: dependencies: has-symbols "^1.0.0" -is-typedarray@~1.0.0: +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= @@ -17482,17 +17546,17 @@ istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.3: resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#0b891e5ad42312c2b9488554f603795f9a2211ba" integrity sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw== -istanbul-lib-coverage@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" - integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.0.0-alpha.1: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" + integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== -istanbul-lib-hook@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz#c95695f383d4f8f60df1f04252a9550e15b5b133" - integrity sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA== +istanbul-lib-hook@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz#8f84c9434888cc6b1d0a9d7092a76d239ebf0cc6" + integrity sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ== dependencies: - append-transform "^1.0.0" + append-transform "^2.0.0" istanbul-lib-instrument@^1.7.3: version "1.10.2" @@ -17520,18 +17584,31 @@ istanbul-lib-instrument@^3.0.0, istanbul-lib-instrument@^3.0.1: istanbul-lib-coverage "^2.0.3" semver "^5.5.0" -istanbul-lib-instrument@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" - integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== +istanbul-lib-instrument@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz#61f13ac2c96cfefb076fe7131156cc05907874e6" + integrity sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg== + dependencies: + "@babel/core" "^7.7.5" + "@babel/parser" "^7.7.5" + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" + +istanbul-lib-processinfo@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz#e1426514662244b2f25df728e8fd1ba35fe53b9c" + integrity sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw== dependencies: - "@babel/generator" "^7.4.0" - "@babel/parser" "^7.4.3" - "@babel/template" "^7.4.0" - "@babel/traverse" "^7.4.3" - "@babel/types" "^7.4.0" - istanbul-lib-coverage "^2.0.5" - semver "^6.0.0" + archy "^1.0.0" + cross-spawn "^7.0.0" + istanbul-lib-coverage "^3.0.0-alpha.1" + make-dir "^3.0.0" + p-map "^3.0.0" + rimraf "^3.0.0" + uuid "^3.3.3" istanbul-lib-report@^2.0.4: version "2.0.4" @@ -17542,14 +17619,14 @@ istanbul-lib-report@^2.0.4: make-dir "^1.3.0" supports-color "^6.0.0" -istanbul-lib-report@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" - integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== dependencies: - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - supports-color "^6.1.0" + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" istanbul-lib-source-maps@^3.0.1: version "3.0.2" @@ -17562,24 +17639,30 @@ istanbul-lib-source-maps@^3.0.1: rimraf "^2.6.2" source-map "^0.6.1" -istanbul-lib-source-maps@^3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" - integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== +istanbul-lib-source-maps@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9" + integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg== dependencies: debug "^4.1.1" - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - rimraf "^2.6.3" + istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^2.2.4, istanbul-reports@^2.2.6: +istanbul-reports@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af" integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA== dependencies: handlebars "^4.1.2" +istanbul-reports@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" + integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + istanbul@^0.4.0: version "0.4.5" resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" @@ -20121,13 +20204,6 @@ merge-source-map@1.0.4: dependencies: source-map "^0.5.6" -merge-source-map@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" - integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== - dependencies: - source-map "^0.6.1" - merge-stream@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" @@ -21224,6 +21300,13 @@ node-notifier@^5.4.2: shellwords "^0.1.1" which "^1.3.0" +node-preload@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/node-preload/-/node-preload-0.2.1.tgz#c03043bb327f417a18fee7ab7ee57b408a144301" + integrity sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ== + dependencies: + process-on-spawn "^1.0.0" + node-releases@^1.1.25, node-releases@^1.1.46: version "1.1.47" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.47.tgz#c59ef739a1fd7ecbd9f0b7cf5b7871e8a8b591e4" @@ -21490,36 +21573,37 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== -nyc@^14.1.1: - version "14.1.1" - resolved "https://registry.yarnpkg.com/nyc/-/nyc-14.1.1.tgz#151d64a6a9f9f5908a1b73233931e4a0a3075eeb" - integrity sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw== +nyc@^15.0.1: + version "15.0.1" + resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.0.1.tgz#bd4d5c2b17f2ec04370365a5ca1fc0ed26f9f93d" + integrity sha512-n0MBXYBYRqa67IVt62qW1r/d9UH/Qtr7SF1w/nQLJ9KxvWF6b2xCHImRAixHN9tnMMYHC2P14uo6KddNGwMgGg== dependencies: - archy "^1.0.0" - caching-transform "^3.0.2" - convert-source-map "^1.6.0" - cp-file "^6.2.0" - find-cache-dir "^2.1.0" - find-up "^3.0.0" - foreground-child "^1.5.6" - glob "^7.1.3" - istanbul-lib-coverage "^2.0.5" - istanbul-lib-hook "^2.0.7" - istanbul-lib-instrument "^3.3.0" - istanbul-lib-report "^2.0.8" - istanbul-lib-source-maps "^3.0.6" - istanbul-reports "^2.2.4" - js-yaml "^3.13.1" - make-dir "^2.1.0" - merge-source-map "^1.1.0" - resolve-from "^4.0.0" - rimraf "^2.6.3" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + caching-transform "^4.0.0" + convert-source-map "^1.7.0" + decamelize "^1.2.0" + find-cache-dir "^3.2.0" + find-up "^4.1.0" + foreground-child "^2.0.0" + glob "^7.1.6" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-hook "^3.0.0" + istanbul-lib-instrument "^4.0.0" + istanbul-lib-processinfo "^2.0.2" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.0.2" + make-dir "^3.0.0" + node-preload "^0.2.1" + p-map "^3.0.0" + process-on-spawn "^1.0.0" + resolve-from "^5.0.0" + rimraf "^3.0.0" signal-exit "^3.0.2" - spawn-wrap "^1.4.2" - test-exclude "^5.2.3" - uuid "^3.3.2" - yargs "^13.2.2" - yargs-parser "^13.0.0" + spawn-wrap "^2.0.0" + test-exclude "^6.0.0" + yargs "^15.0.2" oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" @@ -21958,7 +22042,7 @@ os-browserify@^0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= -os-homedir@^1.0.0, os-homedir@^1.0.1: +os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= @@ -22200,13 +22284,13 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -package-hash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-3.0.0.tgz#50183f2d36c9e3e528ea0a8605dff57ce976f88e" - integrity sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA== +package-hash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-4.0.0.tgz#3537f654665ec3cc38827387fc904c163c54f506" + integrity sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ== dependencies: graceful-fs "^4.1.15" - hasha "^3.0.0" + hasha "^5.0.0" lodash.flattendeep "^4.4.0" release-zalgo "^1.0.0" @@ -23202,6 +23286,13 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process-on-spawn@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/process-on-spawn/-/process-on-spawn-1.0.0.tgz#95b05a23073d30a17acfdc92a440efd2baefdc93" + integrity sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg== + dependencies: + fromentries "^1.2.0" + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -27207,17 +27298,17 @@ spawn-sync@^1.0.15: concat-stream "^1.4.7" os-shim "^0.1.2" -spawn-wrap@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.2.tgz#cff58e73a8224617b6561abdc32586ea0c82248c" - integrity sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg== +spawn-wrap@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-2.0.0.tgz#103685b8b8f9b79771318827aa78650a610d457e" + integrity sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg== dependencies: - foreground-child "^1.5.6" - mkdirp "^0.5.0" - os-homedir "^1.0.1" - rimraf "^2.6.2" + foreground-child "^2.0.0" + is-windows "^1.0.2" + make-dir "^3.0.0" + rimraf "^3.0.0" signal-exit "^3.0.2" - which "^1.3.0" + which "^2.0.1" spdx-compare@^0.1.2: version "0.1.2" @@ -28536,7 +28627,7 @@ terser@^4.4.3: source-map "~0.6.1" source-map-support "~0.5.12" -test-exclude@^5.0.0, test-exclude@^5.2.3: +test-exclude@^5.0.0: version "5.2.3" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== @@ -28546,6 +28637,15 @@ test-exclude@^5.0.0, test-exclude@^5.2.3: read-pkg-up "^4.0.0" require-main-filename "^2.0.0" +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" @@ -29688,7 +29788,7 @@ type-fest@^0.6.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== -type-fest@^0.8.1: +type-fest@^0.8.0, type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== @@ -29726,6 +29826,13 @@ typed-styles@^0.0.7: resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q== +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + typedarray@^0.0.6, typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -30453,6 +30560,11 @@ uuid@^3.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" integrity sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g== +uuid@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + v8-compile-cache@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" @@ -31713,6 +31825,16 @@ write-file-atomic@^2.4.2: imurmurhash "^0.1.4" signal-exit "^3.0.2" +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + write-json-file@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-3.2.0.tgz#65bbdc9ecd8a1458e15952770ccbadfcff5fe62a" @@ -31954,7 +32076,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@13.1.2, yargs-parser@^13.0.0, yargs-parser@^13.1.0, yargs-parser@^13.1.1, yargs-parser@^13.1.2: +yargs-parser@13.1.2, yargs-parser@^13.1.0, yargs-parser@^13.1.1, yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== @@ -32121,7 +32243,7 @@ yargs@^13.2.2, yargs@^13.3.0: y18n "^4.0.0" yargs-parser "^13.1.1" -yargs@^15.3.1: +yargs@^15.0.2, yargs@^15.3.1: version "15.3.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" integrity sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA== From 97d1685c3dea682f80fd1a907bbc1d6f3702ea85 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 9 Apr 2020 23:18:18 -0400 Subject: [PATCH 78/81] Sharing saved-objects phase 1 (#54605) Co-authored-by: kobelb --- docs/api/saved-objects/bulk_create.asciidoc | 2 +- .../kibana-plugin-core-public.savedobject.md | 1 + ...ugin-core-public.savedobject.namespaces.md | 13 + ...in-core-server.isavedobjecttyperegistry.md | 2 +- .../core/server/kibana-plugin-core-server.md | 3 + .../kibana-plugin-core-server.savedobject.md | 1 + ...ugin-core-server.savedobject.namespaces.md | 13 + ...rver.savedobjectsaddtonamespacesoptions.md | 20 + ...edobjectsaddtonamespacesoptions.refresh.md | 13 + ...edobjectsaddtonamespacesoptions.version.md | 13 + ...rver.savedobjectsclient.addtonamespaces.md | 27 + ...savedobjectsclient.deletefromnamespaces.md | 27 + ...a-plugin-core-server.savedobjectsclient.md | 2 + ...savedobjectsdeletefromnamespacesoptions.md | 19 + ...ectsdeletefromnamespacesoptions.refresh.md | 13 + ...objectserrorhelpers.createconflicterror.md | 23 + ...pers.decorateescannotexecutescripterror.md | 23 + ...rorhelpers.isescannotexecutescripterror.md | 22 + ...in-core-server.savedobjectserrorhelpers.md | 3 + ...n-core-server.savedobjectsnamespacetype.md | 15 + ....savedobjectsrepository.addtonamespaces.md | 27 + ...dobjectsrepository.deletefromnamespaces.md | 27 + ...ugin-core-server.savedobjectsrepository.md | 2 + ...r.savedobjectsservicesetup.registertype.md | 2 +- ...ana-plugin-core-server.savedobjectstype.md | 3 +- ...rver.savedobjectstype.namespaceagnostic.md | 9 +- ...e-server.savedobjectstype.namespacetype.md | 13 + ...avedobjecttyperegistry.ismultinamespace.md | 24 + ...dobjecttyperegistry.isnamespaceagnostic.md | 2 +- ...vedobjecttyperegistry.issinglenamespace.md | 24 + ...gin-core-server.savedobjecttyperegistry.md | 4 +- src/core/public/public.api.md | 1 + src/core/server/index.ts | 3 + .../__snapshots__/utils.test.ts.snap | 48 +- src/core/server/saved_objects/index.ts | 1 + .../build_active_mappings.test.ts.snap | 8 + .../migrations/core/build_active_mappings.ts | 3 + .../migrations/core/index_migrator.test.ts | 4 + .../kibana_migrator.test.ts.snap | 4 + .../routes/integration_tests/import.test.ts | 2 +- .../saved_objects_service.test.ts | 4 +- .../saved_objects/saved_objects_service.ts | 2 +- .../saved_objects_type_registry.mock.ts | 6 + .../saved_objects_type_registry.test.ts | 98 +- .../saved_objects_type_registry.ts | 37 +- .../saved_objects/schema/schema.test.ts | 98 +- .../server/saved_objects/schema/schema.ts | 33 +- .../serialization/serializer.test.ts | 1710 +++--- .../saved_objects/serialization/serializer.ts | 15 +- .../saved_objects/serialization/types.ts | 2 + .../lib/__snapshots__/repository.test.js.snap | 5 - .../service/lib/decorate_es_error.test.ts | 32 + .../service/lib/decorate_es_error.ts | 10 +- .../saved_objects/service/lib/errors.test.ts | 172 +- .../saved_objects/service/lib/errors.ts | 16 + .../service/lib/included_fields.test.ts | 26 +- .../service/lib/included_fields.ts | 1 + .../service/lib/repository.mock.ts | 2 + .../service/lib/repository.test.js | 4970 +++++++++-------- .../saved_objects/service/lib/repository.ts | 769 ++- .../lib/search_dsl/query_params.test.ts | 1460 +---- .../service/lib/search_dsl/query_params.ts | 14 +- .../service/saved_objects_client.mock.ts | 2 + .../service/saved_objects_client.test.js | 34 + .../service/saved_objects_client.ts | 54 + src/core/server/saved_objects/types.ts | 24 +- src/core/server/saved_objects/utils.test.ts | 63 +- src/core/server/saved_objects/utils.ts | 11 +- src/core/server/server.api.md | 37 +- src/core/types/saved_objects.ts | 2 + .../apis/saved_objects/bulk_create.js | 4 +- .../apis/saved_objects/bulk_get.js | 13 +- .../apis/saved_objects/export.js | 3 +- ...ypted_saved_objects_client_wrapper.test.ts | 308 +- .../encrypted_saved_objects_client_wrapper.ts | 42 +- .../server/saved_objects/index.ts | 3 +- .../server/audit/audit_logger.test.ts | 36 +- .../security/server/audit/audit_logger.ts | 17 +- .../check_privileges.test.ts.snap | 27 - .../authorization/check_privileges.test.ts | 1216 ++-- .../server/authorization/check_privileges.ts | 95 +- .../check_privileges_dynamically.ts | 4 +- .../check_saved_objects_privileges.test.ts | 132 +- .../check_saved_objects_privileges.ts | 34 +- .../disable_ui_capabilities.test.ts | 47 +- .../authorization/disable_ui_capabilities.ts | 8 +- x-pack/plugins/security/server/plugin.ts | 1 + .../security/server/saved_objects/index.ts | 10 +- ...ecure_saved_objects_client_wrapper.test.ts | 1448 +++-- .../secure_saved_objects_client_wrapper.ts | 212 +- .../edit_space/delete_spaces_button.tsx | 11 +- .../spaces_grid/spaces_grid_page.tsx | 11 +- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 6 + .../resolve_copy_conflicts.test.ts | 6 + .../lib/spaces_client/spaces_client.test.ts | 24 +- .../server/lib/spaces_client/spaces_client.ts | 14 +- .../server/routes/api/external/delete.test.ts | 30 +- .../server/routes/api/external/delete.ts | 10 +- .../server/routes/api/external/index.ts | 4 + .../routes/api/external/share_add_spaces.ts | 62 + .../api/external/share_remove_spaces.ts | 62 + .../spaces_saved_objects_client.test.ts.snap | 29 - .../spaces_saved_objects_client.test.ts | 314 +- .../spaces_saved_objects_client.ts | 46 + .../plugins/uptime/server/rest_api/types.ts | 15 +- .../apis/spaces/saved_objects.ts | 2 +- .../common/config.ts | 2 + .../saved_objects/spaces/data.json | 103 +- .../saved_objects/spaces/mappings.json | 78 +- .../fixtures/hidden_type_plugin/index.js | 2 + .../fixtures/hidden_type_plugin/mappings.json | 2 +- .../fixtures/isolated_type_plugin/index.js | 26 + .../isolated_type_plugin/mappings.json | 31 + .../isolated_type_plugin/package.json | 7 + .../mappings.json | 2 +- .../fixtures/shared_type_plugin/index.js | 27 + .../fixtures/shared_type_plugin/mappings.json | 15 + .../fixtures/shared_type_plugin/package.json | 7 + .../common/lib/saved_object_test_cases.ts | 40 + .../common/lib/saved_object_test_utils.ts | 302 + .../common/lib/space_test_utils.ts | 24 - .../common/lib/types.ts | 27 +- .../common/suites/bulk_create.ts | 293 +- .../common/suites/bulk_get.ts | 257 +- .../common/suites/bulk_update.ts | 291 +- .../common/suites/create.ts | 259 +- .../common/suites/delete.ts | 190 +- .../common/suites/export.ts | 291 +- .../common/suites/find.ts | 401 +- .../common/suites/get.ts | 226 +- .../common/suites/import.ts | 285 +- .../common/suites/resolve_import_errors.ts | 312 +- .../common/suites/update.ts | 246 +- .../security_and_spaces/apis/bulk_create.ts | 278 +- .../security_and_spaces/apis/bulk_get.ts | 260 +- .../security_and_spaces/apis/bulk_update.ts | 347 +- .../security_and_spaces/apis/create.ts | 313 +- .../security_and_spaces/apis/delete.ts | 337 +- .../security_and_spaces/apis/export.ts | 314 +- .../security_and_spaces/apis/find.ts | 766 +-- .../security_and_spaces/apis/get.ts | 339 +- .../security_and_spaces/apis/import.ts | 301 +- .../security_and_spaces/apis/index.ts | 2 +- .../apis/resolve_import_errors.ts | 325 +- .../security_and_spaces/apis/update.ts | 338 +- .../security_only/apis/bulk_create.ts | 232 +- .../security_only/apis/bulk_get.ts | 226 +- .../security_only/apis/bulk_update.ts | 323 +- .../security_only/apis/create.ts | 275 +- .../security_only/apis/delete.ts | 313 +- .../security_only/apis/export.ts | 299 +- .../security_only/apis/find.ts | 791 +-- .../security_only/apis/get.ts | 312 +- .../security_only/apis/import.ts | 278 +- .../security_only/apis/index.ts | 2 +- .../apis/resolve_import_errors.ts | 277 +- .../security_only/apis/update.ts | 314 +- .../spaces_only/apis/bulk_create.ts | 123 +- .../spaces_only/apis/bulk_get.ts | 68 +- .../spaces_only/apis/bulk_update.ts | 110 +- .../spaces_only/apis/create.ts | 118 +- .../spaces_only/apis/delete.ts | 109 +- .../spaces_only/apis/export.ts | 65 +- .../spaces_only/apis/find.ts | 157 +- .../spaces_only/apis/get.ts | 110 +- .../spaces_only/apis/import.ts | 74 +- .../spaces_only/apis/index.ts | 2 +- .../spaces_only/apis/resolve_import_errors.ts | 85 +- .../spaces_only/apis/update.ts | 110 +- .../spaces_api_integration/common/config.ts | 1 + .../saved_objects/spaces/data.json | 120 + .../saved_objects/spaces/mappings.json | 16 + .../fixtures/shared_type_plugin/index.js | 27 + .../fixtures/shared_type_plugin/mappings.json | 15 + .../fixtures/shared_type_plugin/package.json | 7 + .../common/lib/saved_object_test_cases.ts | 40 + .../common/suites/delete.ts | 20 + .../common/suites/share_add.ts | 118 + .../common/suites/share_remove.ts | 116 + .../security_and_spaces/apis/index.ts | 2 + .../security_and_spaces/apis/share_add.ts | 124 + .../security_and_spaces/apis/share_remove.ts | 99 + .../spaces_only/apis/index.ts | 2 + .../spaces_only/apis/share_add.ts | 84 + .../spaces_only/apis/share_remove.ts | 99 + 185 files changed, 12261 insertions(+), 15549 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobject.namespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobject.namespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsnamespacetype.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespacetype.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md delete mode 100644 src/core/server/saved_objects/service/lib/__snapshots__/repository.test.js.snap delete mode 100644 x-pack/plugins/security/server/authorization/__snapshots__/check_privileges.test.ts.snap create mode 100644 x-pack/plugins/spaces/server/routes/api/external/share_add_spaces.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/external/share_remove_spaces.ts delete mode 100644 x-pack/plugins/spaces/server/saved_objects/__snapshots__/spaces_saved_objects_client.test.ts.snap create mode 100644 x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/index.js create mode 100644 x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/mappings.json create mode 100644 x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/package.json create mode 100644 x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/index.js create mode 100644 x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/mappings.json create mode 100644 x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/package.json create mode 100644 x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts create mode 100644 x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts delete mode 100644 x-pack/test/saved_object_api_integration/common/lib/space_test_utils.ts create mode 100644 x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/index.js create mode 100644 x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/mappings.json create mode 100644 x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/package.json create mode 100644 x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts create mode 100644 x-pack/test/spaces_api_integration/common/suites/share_add.ts create mode 100644 x-pack/test/spaces_api_integration/common/suites/share_remove.ts create mode 100644 x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts create mode 100644 x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts create mode 100644 x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts create mode 100644 x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts diff --git a/docs/api/saved-objects/bulk_create.asciidoc b/docs/api/saved-objects/bulk_create.asciidoc index 9daba224b317c..fbd4c6e77f8bf 100644 --- a/docs/api/saved-objects/bulk_create.asciidoc +++ b/docs/api/saved-objects/bulk_create.asciidoc @@ -104,7 +104,7 @@ The API returns the following: "type": "dashboard", "error": { "statusCode": 409, - "message": "version conflict, document already exists" + "message": "Saved object [dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab] conflict" } } ] diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.md index c6bc13b98bc06..b67d0536fb336 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.md @@ -18,6 +18,7 @@ export interface SavedObject | [error](./kibana-plugin-core-public.savedobject.error.md) | {
message: string;
statusCode: number;
} | | | [id](./kibana-plugin-core-public.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | | [migrationVersion](./kibana-plugin-core-public.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [namespaces](./kibana-plugin-core-public.savedobject.namespaces.md) | string[] | Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. | | [references](./kibana-plugin-core-public.savedobject.references.md) | SavedObjectReference[] | A reference to another saved object. | | [type](./kibana-plugin-core-public.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | | [updated\_at](./kibana-plugin-core-public.savedobject.updated_at.md) | string | Timestamp of the last time this document had been updated. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.namespaces.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.namespaces.md new file mode 100644 index 0000000000000..257df45934506 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.namespaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObject](./kibana-plugin-core-public.savedobject.md) > [namespaces](./kibana-plugin-core-public.savedobject.namespaces.md) + +## SavedObject.namespaces property + +Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. + +Signature: + +```typescript +namespaces?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjecttyperegistry.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjecttyperegistry.md index 245cb1a56439f..f9c621885c001 100644 --- a/docs/development/core/server/kibana-plugin-core-server.isavedobjecttyperegistry.md +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjecttyperegistry.md @@ -9,5 +9,5 @@ See [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistr Signature: ```typescript -export declare type ISavedObjectTypeRegistry = Pick; +export declare type ISavedObjectTypeRegistry = Omit; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index accab9bf0cb36..5c0f10cac5179 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -130,6 +130,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectMigrationContext](./kibana-plugin-core-server.savedobjectmigrationcontext.md) | Migration context provided when invoking a [migration handler](./kibana-plugin-core-server.savedobjectmigrationfn.md) | | [SavedObjectMigrationMap](./kibana-plugin-core-server.savedobjectmigrationmap.md) | A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions.For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. | | [SavedObjectReference](./kibana-plugin-core-server.savedobjectreference.md) | A reference to another saved object. | +| [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) | | | [SavedObjectsBaseOptions](./kibana-plugin-core-server.savedobjectsbaseoptions.md) | | | [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) | | | [SavedObjectsBulkGetObject](./kibana-plugin-core-server.savedobjectsbulkgetobject.md) | | @@ -143,6 +144,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | | | [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md) | | +| [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) | | | [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | | | [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) | Options controlling the export operation. | | [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | @@ -262,6 +264,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md). | | [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | | [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation | +| [SavedObjectsNamespaceType](./kibana-plugin-core-server.savedobjectsnamespacetype.md) | The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global.Note: do not write logic that uses this value directly; instead, use the appropriate accessors in the [type registry](./kibana-plugin-core-server.savedobjecttyperegistry.md). | | [ScopeableRequest](./kibana-plugin-core-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.See [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md). | | [ServiceStatusLevel](./kibana-plugin-core-server.servicestatuslevel.md) | A convenience type that represents the union of each value in [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md). | | [SharedGlobalConfig](./kibana-plugin-core-server.sharedglobalconfig.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.md index 0df97b0d4221a..94d1c378899df 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.md @@ -18,6 +18,7 @@ export interface SavedObject | [error](./kibana-plugin-core-server.savedobject.error.md) | {
message: string;
statusCode: number;
} | | | [id](./kibana-plugin-core-server.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | | [migrationVersion](./kibana-plugin-core-server.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [namespaces](./kibana-plugin-core-server.savedobject.namespaces.md) | string[] | Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. | | [references](./kibana-plugin-core-server.savedobject.references.md) | SavedObjectReference[] | A reference to another saved object. | | [type](./kibana-plugin-core-server.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | | [updated\_at](./kibana-plugin-core-server.savedobject.updated_at.md) | string | Timestamp of the last time this document had been updated. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.namespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.namespaces.md new file mode 100644 index 0000000000000..2a555db01df3b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.namespaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObject](./kibana-plugin-core-server.savedobject.md) > [namespaces](./kibana-plugin-core-server.savedobject.namespaces.md) + +## SavedObject.namespaces property + +Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. + +Signature: + +```typescript +namespaces?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md new file mode 100644 index 0000000000000..711588bdd608c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) + +## SavedObjectsAddToNamespacesOptions interface + + +Signature: + +```typescript +export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | +| [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md new file mode 100644 index 0000000000000..c0a1008ab5331 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md) + +## SavedObjectsAddToNamespacesOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md new file mode 100644 index 0000000000000..9432b4bf80da6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) > [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md) + +## SavedObjectsAddToNamespacesOptions.version property + +An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. + +Signature: + +```typescript +version?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md new file mode 100644 index 0000000000000..45c9c39f9626a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [addToNamespaces](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md) + +## SavedObjectsClient.addToNamespaces() method + +Adds namespaces to a SavedObject + +Signature: + +```typescript +addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| namespaces | string[] | | +| options | SavedObjectsAddToNamespacesOptions | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md new file mode 100644 index 0000000000000..80b58d29d393b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) + +## SavedObjectsClient.deleteFromNamespaces() method + +Removes namespaces from a SavedObject + +Signature: + +```typescript +deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| namespaces | string[] | | +| options | SavedObjectsDeleteFromNamespacesOptions | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md index 5a8a213d2bccc..7038c0c07012f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md @@ -25,11 +25,13 @@ The constructor for this class is marked as internal. Third-party code should no | Method | Modifiers | Description | | --- | --- | --- | +| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md) | | Adds namespaces to a SavedObject | | [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkcreate.md) | | Persists multiple documents batched together as a single request | | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkupdate.md) | | Bulk Updates multiple SavedObject at once | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.create.md) | | Persists a SavedObject | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.delete.md) | | Deletes a SavedObject | +| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) | | Removes namespaces from a SavedObject | | [find(options)](./kibana-plugin-core-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.get.md) | | Retrieves a single object | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.update.md) | | Updates an SavedObject | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md new file mode 100644 index 0000000000000..8a2afe6656fa4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) + +## SavedObjectsDeleteFromNamespacesOptions interface + + +Signature: + +```typescript +export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md new file mode 100644 index 0000000000000..1175b79bc1abd --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md) + +## SavedObjectsDeleteFromNamespacesOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md new file mode 100644 index 0000000000000..8e04282ce0c71 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [createConflictError](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) + +## SavedObjectsErrorHelpers.createConflictError() method + +Signature: + +```typescript +static createConflictError(type: string, id: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md new file mode 100644 index 0000000000000..94060bba50067 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [decorateEsCannotExecuteScriptError](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md) + +## SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError() method + +Signature: + +```typescript +static decorateEsCannotExecuteScriptError(error: Error, reason?: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| reason | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md new file mode 100644 index 0000000000000..debb94fe4f8d8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [isEsCannotExecuteScriptError](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) + +## SavedObjectsErrorHelpers.isEsCannotExecuteScriptError() method + +Signature: + +```typescript +static isEsCannotExecuteScriptError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md index 55a42e4f4eb7a..250b9d3899670 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md @@ -16,12 +16,14 @@ export declare class SavedObjectsErrorHelpers | Method | Modifiers | Description | | --- | --- | --- | | [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | static | | +| [createConflictError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | static | | | [createEsAutoCreateIndexError()](./kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md) | static | | | [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | static | | | [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | static | | | [createUnsupportedTypeError(type)](./kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) | static | | | [decorateBadRequestError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratebadrequesterror.md) | static | | | [decorateConflictError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateconflicterror.md) | static | | +| [decorateEsCannotExecuteScriptError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md) | static | | | [decorateEsUnavailableError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateesunavailableerror.md) | static | | | [decorateForbiddenError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateforbiddenerror.md) | static | | | [decorateGeneralError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md) | static | | @@ -30,6 +32,7 @@ export declare class SavedObjectsErrorHelpers | [isBadRequestError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isbadrequesterror.md) | static | | | [isConflictError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isconflicterror.md) | static | | | [isEsAutoCreateIndexError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md) | static | | +| [isEsCannotExecuteScriptError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) | static | | | [isEsUnavailableError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md) | static | | | [isForbiddenError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md) | static | | | [isInvalidVersionError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isinvalidversionerror.md) | static | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsnamespacetype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsnamespacetype.md new file mode 100644 index 0000000000000..173b9e19321d0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsnamespacetype.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsNamespaceType](./kibana-plugin-core-server.savedobjectsnamespacetype.md) + +## SavedObjectsNamespaceType type + +The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global. + +Note: do not write logic that uses this value directly; instead, use the appropriate accessors in the [type registry](./kibana-plugin-core-server.savedobjecttyperegistry.md). + +Signature: + +```typescript +export declare type SavedObjectsNamespaceType = 'single' | 'multiple' | 'agnostic'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md new file mode 100644 index 0000000000000..bbb20d2bc3b96 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [addToNamespaces](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) + +## SavedObjectsRepository.addToNamespaces() method + +Adds one or more namespaces to a given multi-namespace saved object. This method and \[`deleteFromNamespaces`\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. + +Signature: + +```typescript +addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| namespaces | string[] | | +| options | SavedObjectsAddToNamespacesOptions | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md new file mode 100644 index 0000000000000..471c3e3c5092d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) + +## SavedObjectsRepository.deleteFromNamespaces() method + +Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[`addToNamespaces`\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. + +Signature: + +```typescript +deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| namespaces | string[] | | +| options | SavedObjectsDeleteFromNamespacesOptions | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index 37547af2497e9..bd86ff3abbe9b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -15,12 +15,14 @@ export declare class SavedObjectsRepository | Method | Modifiers | Description | | --- | --- | --- | +| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) | | Adds one or more namespaces to a given multi-namespace saved object. This method and \[deleteFromNamespaces\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md) | | Creates multiple documents at once | | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md) | | Updates multiple objects in bulk | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.create.md) | | Persists an object | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | +| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[addToNamespaces\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | | [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md index c24fa7e7038c6..57c9e04966c1b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md @@ -29,7 +29,7 @@ import * as migrations from './migrations'; export const myType: SavedObjectsType = { name: 'MyType', hidden: false, - namespaceAgnostic: true, + namespaceType: 'multiple', mappings: { properties: { textField: { diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md index ad7bf9a0f00d0..d8202545f0eae 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md @@ -25,5 +25,6 @@ This is only internal for now, and will only be public when we expose the regist | [mappings](./kibana-plugin-core-server.savedobjectstype.mappings.md) | SavedObjectsTypeMappingDefinition | The [mapping definition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) for the type. | | [migrations](./kibana-plugin-core-server.savedobjectstype.migrations.md) | SavedObjectMigrationMap | An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type. | | [name](./kibana-plugin-core-server.savedobjectstype.name.md) | string | The name of the type, which is also used as the internal id. | -| [namespaceAgnostic](./kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md) | boolean | Is the type global (true), or namespaced (false). | +| [namespaceAgnostic](./kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md) | boolean | Is the type global (true), or not (false). | +| [namespaceType](./kibana-plugin-core-server.savedobjectstype.namespacetype.md) | SavedObjectsNamespaceType | The [namespace type](./kibana-plugin-core-server.savedobjectsnamespacetype.md) for the type. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md index 8f43db86449d0..e347421590482 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md @@ -4,10 +4,15 @@ ## SavedObjectsType.namespaceAgnostic property -Is the type global (true), or namespaced (false). +> Warning: This API is now obsolete. +> +> Use `namespaceType` instead. +> + +Is the type global (true), or not (false). Signature: ```typescript -namespaceAgnostic: boolean; +namespaceAgnostic?: boolean; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespacetype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespacetype.md new file mode 100644 index 0000000000000..69912f9144980 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespacetype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) > [namespaceType](./kibana-plugin-core-server.savedobjectstype.namespacetype.md) + +## SavedObjectsType.namespaceType property + +The [namespace type](./kibana-plugin-core-server.savedobjectsnamespacetype.md) for the type. + +Signature: + +```typescript +namespaceType?: SavedObjectsNamespaceType; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md new file mode 100644 index 0000000000000..6532c5251d816 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) > [isMultiNamespace](./kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md) + +## SavedObjectTypeRegistry.isMultiNamespace() method + +Returns whether the type is multi-namespace (shareable); resolves to `false` if the type is not registered + +Signature: + +```typescript +isMultiNamespace(type: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md index 92dfa5465235a..859c7b9711816 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md @@ -4,7 +4,7 @@ ## SavedObjectTypeRegistry.isNamespaceAgnostic() method -Returns the `namespaceAgnostic` property for given type, or `false` if the type is not registered. +Returns whether the type is namespace-agnostic (global); resolves to `false` if the type is not registered Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md new file mode 100644 index 0000000000000..18146b2fd6ea1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) > [isSingleNamespace](./kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md) + +## SavedObjectTypeRegistry.isSingleNamespace() method + +Returns whether the type is single-namespace (isolated); resolves to `true` if the type is not registered + +Signature: + +```typescript +isSingleNamespace(type: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md index 410b709252b72..69a94e4ad8c88 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md @@ -22,6 +22,8 @@ export declare class SavedObjectTypeRegistry | [getType(type)](./kibana-plugin-core-server.savedobjecttyperegistry.gettype.md) | | Return the [type](./kibana-plugin-core-server.savedobjectstype.md) definition for given type name. | | [isHidden(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ishidden.md) | | Returns the hidden property for given type, or false if the type is not registered. | | [isImportableAndExportable(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isimportableandexportable.md) | | Returns the management.importableAndExportable property for given type, or false if the type is not registered or does not define a management section. | -| [isNamespaceAgnostic(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md) | | Returns the namespaceAgnostic property for given type, or false if the type is not registered. | +| [isMultiNamespace(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md) | | Returns whether the type is multi-namespace (shareable); resolves to false if the type is not registered | +| [isNamespaceAgnostic(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md) | | Returns whether the type is namespace-agnostic (global); resolves to false if the type is not registered | +| [isSingleNamespace(type)](./kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md) | | Returns whether the type is single-namespace (isolated); resolves to true if the type is not registered | | [registerType(type)](./kibana-plugin-core-server.savedobjecttyperegistry.registertype.md) | | Register a [type](./kibana-plugin-core-server.savedobjectstype.md) inside the registry. A type can only be registered once. subsequent calls with the same type name will throw an error. | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 9f7f649f1e2a5..a5aa37becabc2 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -956,6 +956,7 @@ export interface SavedObject { }; id: string; migrationVersion?: SavedObjectsMigrationVersion; + namespaces?: string[]; references: SavedObjectReference[]; type: string; updated_at?: string; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index a298f80f96d8f..039988fa08968 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -227,6 +227,8 @@ export { SavedObjectsLegacyService, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, + SavedObjectsAddToNamespacesOptions, + SavedObjectsDeleteFromNamespacesOptions, SavedObjectsServiceStart, SavedObjectsServiceSetup, SavedObjectStatusMeta, @@ -242,6 +244,7 @@ export { SavedObjectsMappingProperties, SavedObjectTypeRegistry, ISavedObjectTypeRegistry, + SavedObjectsNamespaceType, SavedObjectsType, SavedObjectsTypeManagementDefinition, SavedObjectMigrationMap, diff --git a/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap b/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap index 5431d2ca47892..7cd0297e57857 100644 --- a/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap +++ b/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap @@ -16,7 +16,7 @@ Array [ }, "migrations": Object {}, "name": "typeA", - "namespaceAgnostic": false, + "namespaceType": "single", }, Object { "convertToAliasScript": undefined, @@ -32,7 +32,7 @@ Array [ }, "migrations": Object {}, "name": "typeB", - "namespaceAgnostic": false, + "namespaceType": "single", }, Object { "convertToAliasScript": undefined, @@ -48,7 +48,7 @@ Array [ }, "migrations": Object {}, "name": "typeC", - "namespaceAgnostic": false, + "namespaceType": "single", }, ] `; @@ -72,7 +72,7 @@ Array [ "2.0.4": [Function], }, "name": "typeA", - "namespaceAgnostic": true, + "namespaceType": "agnostic", }, Object { "convertToAliasScript": "some alias script", @@ -91,7 +91,7 @@ Array [ }, "migrations": Object {}, "name": "typeB", - "namespaceAgnostic": false, + "namespaceType": "single", }, Object { "convertToAliasScript": undefined, @@ -109,7 +109,7 @@ Array [ "1.5.3": [Function], }, "name": "typeC", - "namespaceAgnostic": false, + "namespaceType": "single", }, ] `; @@ -130,7 +130,23 @@ Array [ }, "migrations": Object {}, "name": "typeA", - "namespaceAgnostic": true, + "namespaceType": "agnostic", + }, + Object { + "convertToAliasScript": undefined, + "hidden": false, + "indexPattern": "barBaz", + "management": undefined, + "mappings": Object { + "properties": Object { + "fieldB": Object { + "type": "text", + }, + }, + }, + "migrations": Object {}, + "name": "typeB", + "namespaceType": "multiple", }, Object { "convertToAliasScript": undefined, @@ -146,7 +162,23 @@ Array [ }, "migrations": Object {}, "name": "typeC", - "namespaceAgnostic": false, + "namespaceType": "single", + }, + Object { + "convertToAliasScript": undefined, + "hidden": false, + "indexPattern": "bazQux", + "management": undefined, + "mappings": Object { + "properties": Object { + "fieldD": Object { + "type": "text", + }, + }, + }, + "migrations": Object {}, + "name": "typeD", + "namespaceType": "agnostic", }, ] `; diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index fe4795cad11a5..a294b28753f7b 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -69,6 +69,7 @@ export { } from './migrations'; export { + SavedObjectsNamespaceType, SavedObjectStatusMeta, SavedObjectsType, SavedObjectsTypeManagementDefinition, diff --git a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap index fc26d7e9cf6e9..bc9a66926e880 100644 --- a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap +++ b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap @@ -8,6 +8,7 @@ Object { "bbb": "18c78c995965207ed3f6e7fc5c6e55fe", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", "references": "7997cf5a56cc02bdc9c93361bde732b0", "type": "2f4316de49999235636386fe51dc06c1", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", @@ -28,6 +29,9 @@ Object { "namespace": Object { "type": "keyword", }, + "namespaces": Object { + "type": "keyword", + }, "references": Object { "properties": Object { "id": Object { @@ -59,6 +63,7 @@ Object { "firstType": "635418ab953d81d93f1190b70a8d3f57", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", "references": "7997cf5a56cc02bdc9c93361bde732b0", "secondType": "72d57924f415fbadb3ee293b67d233ab", "thirdType": "510f1f0adb69830cf8a1c5ce2923ed82", @@ -83,6 +88,9 @@ Object { "namespace": Object { "type": "keyword", }, + "namespaces": Object { + "type": "keyword", + }, "references": Object { "properties": Object { "id": Object { diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts index 4d1a607414ca6..418ed95f14e05 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts @@ -142,6 +142,9 @@ function defaultMapping(): IndexMapping { namespace: { type: 'keyword', }, + namespaces: { + type: 'keyword', + }, updated_at: { type: 'date', }, diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 1c2d3f501ff80..19208e6c83596 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -61,6 +61,7 @@ describe('IndexMigrator', () => { foo: '18c78c995965207ed3f6e7fc5c6e55fe', migrationVersion: '4a1746014a75ade3a714e1db5763276f', namespace: '2f4316de49999235636386fe51dc06c1', + namespaces: '2f4316de49999235636386fe51dc06c1', references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', @@ -70,6 +71,7 @@ describe('IndexMigrator', () => { foo: { type: 'long' }, migrationVersion: { dynamic: 'true', type: 'object' }, namespace: { type: 'keyword' }, + namespaces: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, references: { @@ -178,6 +180,7 @@ describe('IndexMigrator', () => { foo: '625b32086eb1d1203564cf85062dd22e', migrationVersion: '4a1746014a75ade3a714e1db5763276f', namespace: '2f4316de49999235636386fe51dc06c1', + namespaces: '2f4316de49999235636386fe51dc06c1', references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', @@ -188,6 +191,7 @@ describe('IndexMigrator', () => { foo: { type: 'text' }, migrationVersion: { dynamic: 'true', type: 'object' }, namespace: { type: 'keyword' }, + namespaces: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, references: { diff --git a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap index 507c0b0d9339f..3453f3fc80310 100644 --- a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap +++ b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap @@ -8,6 +8,7 @@ Object { "bmap": "510f1f0adb69830cf8a1c5ce2923ed82", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", "references": "7997cf5a56cc02bdc9c93361bde732b0", "type": "2f4316de49999235636386fe51dc06c1", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", @@ -36,6 +37,9 @@ Object { "namespace": Object { "type": "keyword", }, + "namespaces": Object { + "type": "keyword", + }, "references": Object { "properties": Object { "id": Object { diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index c72d3e241b882..c4a03a0e2e7d2 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -187,7 +187,7 @@ describe('POST /internal/saved_objects/_import', () => { references: [], error: { statusCode: 409, - message: 'version conflict, document already exists', + message: 'Saved object [index-pattern/my-pattern] conflict', }, }, { diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 018117776dcc8..819d79803f371 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -138,7 +138,7 @@ describe('SavedObjectsService', () => { const type = { name: 'someType', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single' as 'single', mappings: { properties: {} }, }; setup.registerType(type); @@ -251,7 +251,7 @@ describe('SavedObjectsService', () => { setup.registerType({ name: 'someType', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single' as 'single', mappings: { properties: {} }, }); }).toThrowErrorMatchingInlineSnapshot( diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 62027928c0bb5..ed4ffef5729ab 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -124,7 +124,7 @@ export interface SavedObjectsServiceSetup { * export const myType: SavedObjectsType = { * name: 'MyType', * hidden: false, - * namespaceAgnostic: true, + * namespaceType: 'multiple', * mappings: { * properties: { * textField: { diff --git a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts index 8c8458d7a5ce4..8bb66859feca2 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts @@ -27,6 +27,8 @@ const createRegistryMock = (): jest.Mocked type === 'global'); + mock.isSingleNamespace.mockImplementation( + (type: string) => type !== 'global' && type !== 'shared' + ); + mock.isMultiNamespace.mockImplementation((type: string) => type === 'shared'); mock.isImportableAndExportable.mockReturnValue(true); return mock; diff --git a/src/core/server/saved_objects/saved_objects_type_registry.test.ts b/src/core/server/saved_objects/saved_objects_type_registry.test.ts index 4d1d5c1eacc25..84337474f3ee3 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.test.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.test.ts @@ -23,7 +23,7 @@ import { SavedObjectsType } from './types'; const createType = (type: Partial): SavedObjectsType => ({ name: 'unknown', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single' as 'single', mappings: { properties: {} }, migrations: {}, ...type, @@ -164,18 +164,92 @@ describe('SavedObjectTypeRegistry', () => { }); describe('#isNamespaceAgnostic', () => { - it('returns correct value for the type', () => { - registry.registerType(createType({ name: 'typeA', namespaceAgnostic: true })); - registry.registerType(createType({ name: 'typeB', namespaceAgnostic: false })); + const expectResult = (expected: boolean, schemaDefinition?: Partial) => { + registry = new SavedObjectTypeRegistry(); + registry.registerType(createType({ name: 'foo', ...schemaDefinition })); + expect(registry.isNamespaceAgnostic('foo')).toBe(expected); + }; - expect(registry.isNamespaceAgnostic('typeA')).toEqual(true); - expect(registry.isNamespaceAgnostic('typeB')).toEqual(false); + it(`returns false when the type is not registered`, () => { + expect(registry.isNamespaceAgnostic('unknownType')).toEqual(false); }); - it('returns false when the type is not registered', () => { - registry.registerType(createType({ name: 'typeA', namespaceAgnostic: true })); - registry.registerType(createType({ name: 'typeB', namespaceAgnostic: false })); - expect(registry.isNamespaceAgnostic('unknownType')).toEqual(false); + it(`returns true for namespaceType 'agnostic'`, () => { + expectResult(true, { namespaceType: 'agnostic' }); + }); + + it(`returns false for other namespaceType`, () => { + expectResult(false, { namespaceType: 'multiple' }); + expectResult(false, { namespaceType: 'single' }); + expectResult(false, { namespaceType: undefined }); + }); + + // deprecated test cases + it(`returns true when namespaceAgnostic is true`, () => { + expectResult(true, { namespaceAgnostic: true, namespaceType: 'agnostic' }); + expectResult(true, { namespaceAgnostic: true, namespaceType: 'multiple' }); + expectResult(true, { namespaceAgnostic: true, namespaceType: 'single' }); + expectResult(true, { namespaceAgnostic: true, namespaceType: undefined }); + }); + }); + + describe('#isSingleNamespace', () => { + const expectResult = (expected: boolean, schemaDefinition?: Partial) => { + registry = new SavedObjectTypeRegistry(); + registry.registerType(createType({ name: 'foo', ...schemaDefinition })); + expect(registry.isSingleNamespace('foo')).toBe(expected); + }; + + it(`returns true when the type is not registered`, () => { + expect(registry.isSingleNamespace('unknownType')).toEqual(true); + }); + + it(`returns true for namespaceType 'single'`, () => { + expectResult(true, { namespaceType: 'single' }); + expectResult(true, { namespaceType: undefined }); + }); + + it(`returns false for other namespaceType`, () => { + expectResult(false, { namespaceType: 'agnostic' }); + expectResult(false, { namespaceType: 'multiple' }); + }); + + // deprecated test cases + it(`returns false when namespaceAgnostic is true`, () => { + expectResult(false, { namespaceAgnostic: true, namespaceType: 'agnostic' }); + expectResult(false, { namespaceAgnostic: true, namespaceType: 'multiple' }); + expectResult(false, { namespaceAgnostic: true, namespaceType: 'single' }); + expectResult(false, { namespaceAgnostic: true, namespaceType: undefined }); + }); + }); + + describe('#isMultiNamespace', () => { + const expectResult = (expected: boolean, schemaDefinition?: Partial) => { + registry = new SavedObjectTypeRegistry(); + registry.registerType(createType({ name: 'foo', ...schemaDefinition })); + expect(registry.isMultiNamespace('foo')).toBe(expected); + }; + + it(`returns false when the type is not registered`, () => { + expect(registry.isMultiNamespace('unknownType')).toEqual(false); + }); + + it(`returns true for namespaceType 'multiple'`, () => { + expectResult(true, { namespaceType: 'multiple' }); + }); + + it(`returns false for other namespaceType`, () => { + expectResult(false, { namespaceType: 'agnostic' }); + expectResult(false, { namespaceType: 'single' }); + expectResult(false, { namespaceType: undefined }); + }); + + // deprecated test cases + it(`returns false when namespaceAgnostic is true`, () => { + expectResult(false, { namespaceAgnostic: true, namespaceType: 'agnostic' }); + expectResult(false, { namespaceAgnostic: true, namespaceType: 'multiple' }); + expectResult(false, { namespaceAgnostic: true, namespaceType: 'single' }); + expectResult(false, { namespaceAgnostic: true, namespaceType: undefined }); }); }); @@ -206,8 +280,8 @@ describe('SavedObjectTypeRegistry', () => { expect(registry.getIndex('typeWithNoIndex')).toBeUndefined(); }); it('returns undefined when the type is not registered', () => { - registry.registerType(createType({ name: 'typeA', namespaceAgnostic: true })); - registry.registerType(createType({ name: 'typeB', namespaceAgnostic: false })); + registry.registerType(createType({ name: 'typeA', namespaceType: 'agnostic' })); + registry.registerType(createType({ name: 'typeB', namespaceType: 'single' })); expect(registry.getIndex('unknownType')).toBeUndefined(); }); diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts index 5580ce3815d0d..be3fdb86a994c 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.ts @@ -25,16 +25,7 @@ import { SavedObjectsType } from './types'; * * @public */ -export type ISavedObjectTypeRegistry = Pick< - SavedObjectTypeRegistry, - | 'getType' - | 'getAllTypes' - | 'getIndex' - | 'isNamespaceAgnostic' - | 'isHidden' - | 'getImportableAndExportableTypes' - | 'isImportableAndExportable' ->; +export type ISavedObjectTypeRegistry = Omit; /** * Registry holding information about all the registered {@link SavedObjectsType | saved object types}. @@ -77,11 +68,31 @@ export class SavedObjectTypeRegistry { } /** - * Returns the `namespaceAgnostic` property for given type, or `false` if - * the type is not registered. + * Returns whether the type is namespace-agnostic (global); + * resolves to `false` if the type is not registered */ public isNamespaceAgnostic(type: string) { - return this.types.get(type)?.namespaceAgnostic ?? false; + return ( + this.types.get(type)?.namespaceType === 'agnostic' || + this.types.get(type)?.namespaceAgnostic || + false + ); + } + + /** + * Returns whether the type is single-namespace (isolated); + * resolves to `true` if the type is not registered + */ + public isSingleNamespace(type: string) { + return !this.isNamespaceAgnostic(type) && !this.isMultiNamespace(type); + } + + /** + * Returns whether the type is multi-namespace (shareable); + * resolves to `false` if the type is not registered + */ + public isMultiNamespace(type: string) { + return !this.isNamespaceAgnostic(type) && this.types.get(type)?.namespaceType === 'multiple'; } /** diff --git a/src/core/server/saved_objects/schema/schema.test.ts b/src/core/server/saved_objects/schema/schema.test.ts index 43cf27fbae790..f2daa13e43fce 100644 --- a/src/core/server/saved_objects/schema/schema.test.ts +++ b/src/core/server/saved_objects/schema/schema.test.ts @@ -17,32 +17,90 @@ * under the License. */ -import { SavedObjectsSchema } from './schema'; +import { SavedObjectsSchema, SavedObjectsSchemaDefinition } from './schema'; describe('#isNamespaceAgnostic', () => { + const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => { + const schema = new SavedObjectsSchema(schemaDefinition); + const result = schema.isNamespaceAgnostic('foo'); + expect(result).toBe(expected); + }; + + it(`returns false when no schema is defined`, () => { + expectResult(false); + }); + it(`returns false for unknown types`, () => { - const schema = new SavedObjectsSchema(); - const result = schema.isNamespaceAgnostic('bar'); - expect(result).toBe(false); + expectResult(false, { bar: {} }); }); - it(`returns true for explicitly namespace agnostic type`, () => { - const schema = new SavedObjectsSchema({ - foo: { - isNamespaceAgnostic: true, - }, - }); - const result = schema.isNamespaceAgnostic('foo'); - expect(result).toBe(true); + it(`returns false for non-namespace-agnostic type`, () => { + expectResult(false, { foo: { isNamespaceAgnostic: false } }); + expectResult(false, { foo: { isNamespaceAgnostic: undefined } }); }); - it(`returns false for explicitly namespaced type`, () => { - const schema = new SavedObjectsSchema({ - foo: { - isNamespaceAgnostic: false, - }, - }); - const result = schema.isNamespaceAgnostic('foo'); - expect(result).toBe(false); + it(`returns true for explicitly namespace-agnostic type`, () => { + expectResult(true, { foo: { isNamespaceAgnostic: true } }); + }); +}); + +describe('#isSingleNamespace', () => { + const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => { + const schema = new SavedObjectsSchema(schemaDefinition); + const result = schema.isSingleNamespace('foo'); + expect(result).toBe(expected); + }; + + it(`returns true when no schema is defined`, () => { + expectResult(true); + }); + + it(`returns true for unknown types`, () => { + expectResult(true, { bar: {} }); + }); + + it(`returns false for explicitly namespace-agnostic type`, () => { + expectResult(false, { foo: { isNamespaceAgnostic: true } }); + }); + + it(`returns false for explicitly multi-namespace type`, () => { + expectResult(false, { foo: { multiNamespace: true } }); + }); + + it(`returns true for non-namespace-agnostic and non-multi-namespace type`, () => { + expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: false } }); + expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: undefined } }); + expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: false } }); + expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: undefined } }); + }); +}); + +describe('#isMultiNamespace', () => { + const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => { + const schema = new SavedObjectsSchema(schemaDefinition); + const result = schema.isMultiNamespace('foo'); + expect(result).toBe(expected); + }; + + it(`returns false when no schema is defined`, () => { + expectResult(false); + }); + + it(`returns false for unknown types`, () => { + expectResult(false, { bar: {} }); + }); + + it(`returns false for explicitly namespace-agnostic type`, () => { + expectResult(false, { foo: { isNamespaceAgnostic: true } }); + }); + + it(`returns false for non-multi-namespace type`, () => { + expectResult(false, { foo: { multiNamespace: false } }); + expectResult(false, { foo: { multiNamespace: undefined } }); + }); + + it(`returns true for non-namespace-agnostic and explicitly multi-namespace type`, () => { + expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: true } }); + expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: true } }); }); }); diff --git a/src/core/server/saved_objects/schema/schema.ts b/src/core/server/saved_objects/schema/schema.ts index 17ca406ea109a..ba1905158e822 100644 --- a/src/core/server/saved_objects/schema/schema.ts +++ b/src/core/server/saved_objects/schema/schema.ts @@ -24,7 +24,8 @@ import { LegacyConfig } from '../../legacy'; * @internal **/ interface SavedObjectsSchemaTypeDefinition { - isNamespaceAgnostic: boolean; + isNamespaceAgnostic?: boolean; + multiNamespace?: boolean; hidden?: boolean; indexPattern?: ((config: LegacyConfig) => string) | string; convertToAliasScript?: string; @@ -72,7 +73,7 @@ export class SavedObjectsSchema { } public isNamespaceAgnostic(type: string) { - // if no plugins have registered a uiExports.savedObjectSchemas, + // if no plugins have registered a Saved Objects Schema, // this.schema will be undefined, and no types are namespace agnostic if (!this.definition) { return false; @@ -84,4 +85,32 @@ export class SavedObjectsSchema { } return Boolean(typeSchema.isNamespaceAgnostic); } + + public isSingleNamespace(type: string) { + // if no plugins have registered a Saved Objects Schema, + // this.schema will be undefined, and all types are namespace isolated + if (!this.definition) { + return true; + } + + const typeSchema = this.definition[type]; + if (!typeSchema) { + return true; + } + return !Boolean(typeSchema.isNamespaceAgnostic) && !Boolean(typeSchema.multiNamespace); + } + + public isMultiNamespace(type: string) { + // if no plugins have registered a Saved Objects Schema, + // this.schema will be undefined, and no types are multi-namespace + if (!this.definition) { + return false; + } + + const typeSchema = this.definition[type]; + if (!typeSchema) { + return false; + } + return !Boolean(typeSchema.isNamespaceAgnostic) && Boolean(typeSchema.multiNamespace); + } } diff --git a/src/core/server/saved_objects/serialization/serializer.test.ts b/src/core/server/saved_objects/serialization/serializer.test.ts index 8f09b25bb3908..1a7dfdd2d130e 100644 --- a/src/core/server/saved_objects/serialization/serializer.test.ts +++ b/src/core/server/saved_objects/serialization/serializer.test.ts @@ -19,101 +19,101 @@ import _ from 'lodash'; import { SavedObjectsSerializer } from './serializer'; +import { SavedObjectsRawDoc } from './types'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import { encodeVersion } from '../version'; -describe('saved object conversion', () => { - let typeRegistry: ReturnType; - - beforeEach(() => { - typeRegistry = typeRegistryMock.create(); - typeRegistry.isNamespaceAgnostic.mockReturnValue(false); +let typeRegistry = typeRegistryMock.create(); +typeRegistry.isNamespaceAgnostic.mockReturnValue(true); +typeRegistry.isSingleNamespace.mockReturnValue(false); +typeRegistry.isMultiNamespace.mockReturnValue(false); +const namespaceAgnosticSerializer = new SavedObjectsSerializer(typeRegistry); + +typeRegistry = typeRegistryMock.create(); +typeRegistry.isNamespaceAgnostic.mockReturnValue(false); +typeRegistry.isSingleNamespace.mockReturnValue(true); +typeRegistry.isMultiNamespace.mockReturnValue(false); +const singleNamespaceSerializer = new SavedObjectsSerializer(typeRegistry); + +typeRegistry = typeRegistryMock.create(); +typeRegistry.isNamespaceAgnostic.mockReturnValue(false); +typeRegistry.isSingleNamespace.mockReturnValue(false); +typeRegistry.isMultiNamespace.mockReturnValue(true); +const multiNamespaceSerializer = new SavedObjectsSerializer(typeRegistry); + +const sampleTemplate = { + _id: 'foo:bar', + _source: { + type: 'foo', + }, +}; +const createSampleDoc = (raw: any, template = sampleTemplate): SavedObjectsRawDoc => + _.defaultsDeep(raw, template); + +describe('#rawToSavedObject', () => { + test('it copies the _source.type property to type', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + }, + }); + expect(actual).toHaveProperty('type', 'foo'); }); - describe('#rawToSavedObject', () => { - test('it copies the _source.type property to type', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - }, - }); - expect(actual).toHaveProperty('type', 'foo'); + test('it copies the _source.references property to references', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + references: [{ name: 'ref_0', type: 'index-pattern', id: 'pattern*' }], + }, }); + expect(actual).toHaveProperty('references', [ + { + name: 'ref_0', + type: 'index-pattern', + id: 'pattern*', + }, + ]); + }); - test('it copies the _source.references property to references', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - references: [{ name: 'ref_0', type: 'index-pattern', id: 'pattern*' }], - }, - }); - expect(actual).toHaveProperty('references', [ - { - name: 'ref_0', - type: 'index-pattern', - id: 'pattern*', + test('if specified it copies the _source.migrationVersion property to migrationVersion', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + migrationVersion: { + hello: '1.2.3', + acl: '33.3.5', }, - ]); + }, }); - - test('if specified it copies the _source.migrationVersion property to migrationVersion', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - migrationVersion: { - hello: '1.2.3', - acl: '33.3.5', - }, - }, - }); - expect(actual).toHaveProperty('migrationVersion', { - hello: '1.2.3', - acl: '33.3.5', - }); + expect(actual).toHaveProperty('migrationVersion', { + hello: '1.2.3', + acl: '33.3.5', }); + }); - test(`if _source.migrationVersion is unspecified it doesn't set migrationVersion`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - }, - }); - expect(actual).not.toHaveProperty('migrationVersion'); + test(`if _source.migrationVersion is unspecified it doesn't set migrationVersion`, () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + }, }); + expect(actual).not.toHaveProperty('migrationVersion'); + }); - test('it converts the id and type properties, and retains migrationVersion', () => { - const now = String(new Date()); - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'hello:world', - _seq_no: 3, - _primary_term: 1, - _source: { - type: 'hello', - hello: { - a: 'b', - c: 'd', - }, - migrationVersion: { - hello: '1.2.3', - acl: '33.3.5', - }, - updated_at: now, - }, - }); - const expected = { - id: 'world', + test('it converts the id and type properties, and retains migrationVersion', () => { + const now = String(new Date()); + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'hello:world', + _seq_no: 3, + _primary_term: 1, + _source: { type: 'hello', - version: encodeVersion(3, 1), - attributes: { + hello: { a: 'b', c: 'd', }, @@ -122,909 +122,937 @@ describe('saved object conversion', () => { acl: '33.3.5', }, updated_at: now, - references: [], - }; - expect(expected).toEqual(actual); + }, + }); + const expected = { + id: 'world', + type: 'hello', + version: encodeVersion(3, 1), + attributes: { + a: 'b', + c: 'd', + }, + migrationVersion: { + hello: '1.2.3', + acl: '33.3.5', + }, + updated_at: now, + references: [], + }; + expect(expected).toEqual(actual); + }); + + test(`if version is unspecified it doesn't set version`, () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + hello: {}, + }, }); + expect(actual).not.toHaveProperty('version'); + }); - test(`if version is unspecified it doesn't set version`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ + test(`if specified it encodes _seq_no and _primary_term to version`, () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _seq_no: 4, + _primary_term: 1, + _source: { + type: 'foo', + hello: {}, + }, + }); + expect(actual).toHaveProperty('version', encodeVersion(4, 1)); + }); + + test(`if only _seq_no is specified it throws`, () => { + expect(() => + singleNamespaceSerializer.rawToSavedObject({ _id: 'foo:bar', + _seq_no: 4, _source: { type: 'foo', hello: {}, }, - }); - expect(actual).not.toHaveProperty('version'); - }); + }) + ).toThrowErrorMatchingInlineSnapshot(`"_primary_term from elasticsearch must be an integer"`); + }); - test(`if specified it encodes _seq_no and _primary_term to version`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ + test(`if only _primary_term is throws`, () => { + expect(() => + singleNamespaceSerializer.rawToSavedObject({ _id: 'foo:bar', - _seq_no: 4, _primary_term: 1, _source: { type: 'foo', hello: {}, }, - }); - expect(actual).toHaveProperty('version', encodeVersion(4, 1)); - }); + }) + ).toThrowErrorMatchingInlineSnapshot(`"_seq_no from elasticsearch must be an integer"`); + }); - test(`if only _seq_no is specified it throws`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect(() => - serializer.rawToSavedObject({ - _id: 'foo:bar', - _seq_no: 4, - _source: { - type: 'foo', - hello: {}, - }, - }) - ).toThrowErrorMatchingInlineSnapshot(`"_primary_term from elasticsearch must be an integer"`); + test('if specified it copies the _source.updated_at property to updated_at', () => { + const now = Date(); + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + updated_at: now, + }, }); + expect(actual).toHaveProperty('updated_at', now); + }); - test(`if only _primary_term is throws`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect(() => - serializer.rawToSavedObject({ - _id: 'foo:bar', - _primary_term: 1, - _source: { - type: 'foo', - hello: {}, - }, - }) - ).toThrowErrorMatchingInlineSnapshot(`"_seq_no from elasticsearch must be an integer"`); + test(`if _source.updated_at is unspecified it doesn't set updated_at`, () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + }, }); + expect(actual).not.toHaveProperty('updated_at'); + }); - test('if specified it copies the _source.updated_at property to updated_at', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const now = Date(); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - updated_at: now, + test('it does not pass unknown properties through', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'universe', + _source: { + type: 'hello', + hello: { + world: 'earth', }, - }); - expect(actual).toHaveProperty('updated_at', now); + banjo: 'Steve Martin', + }, + }); + expect(actual).toEqual({ + id: 'universe', + type: 'hello', + attributes: { + world: 'earth', + }, + references: [], }); + }); - test(`if _source.updated_at is unspecified it doesn't set updated_at`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - }, - }); - expect(actual).not.toHaveProperty('updated_at'); + test('it does not create attributes if [type] is missing', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'universe', + _source: { + type: 'hello', + }, }); + expect(actual).toEqual({ + id: 'universe', + type: 'hello', + references: [], + }); + }); - test('it does not pass unknown properties through', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ + test('it fails for documents which do not specify a type', () => { + expect(() => + singleNamespaceSerializer.rawToSavedObject({ _id: 'universe', _source: { - type: 'hello', hello: { world: 'earth', }, - banjo: 'Steve Martin', + } as any, + }) + ).toThrow(/Expected "undefined" to be a saved object type/); + }); + + test('it is complimentary with savedObjectToRaw', () => { + const raw = { + _id: 'foo-namespace:foo:bar', + _primary_term: 24, + _seq_no: 42, + _source: { + type: 'foo', + foo: { + meaning: 42, + nested: { stuff: 'here' }, }, - }); - expect(actual).toEqual({ - id: 'universe', - type: 'hello', - attributes: { - world: 'earth', + migrationVersion: { + foo: '1.2.3', + bar: '9.8.7', }, + namespace: 'foo-namespace', + updated_at: String(new Date()), references: [], - }); - }); + }, + }; + + expect( + singleNamespaceSerializer.savedObjectToRaw( + singleNamespaceSerializer.rawToSavedObject(_.cloneDeep(raw)) + ) + ).toEqual(raw); + }); - test('it does not create attributes if [type] is missing', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'universe', - _source: { - type: 'hello', - }, - }); - expect(actual).toEqual({ - id: 'universe', + test('it handles unprefixed ids', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'universe', + _source: { type: 'hello', - references: [], - }); + }, }); - test('it fails for documents which do not specify a type', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect(() => - serializer.rawToSavedObject({ - _id: 'universe', - _source: { - hello: { - world: 'earth', - }, - } as any, - }) - ).toThrow(/Expected "undefined" to be a saved object type/); + expect(actual).toHaveProperty('id', 'universe'); + }); + + describe('namespace-agnostic type with a namespace', () => { + const raw = createSampleDoc({ _source: { namespace: 'baz' } }); + const actual = namespaceAgnosticSerializer.rawToSavedObject(raw); + + test(`removes type prefix from _id`, () => { + expect(actual).toHaveProperty('id', 'bar'); }); - test('it is complimentary with savedObjectToRaw', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const raw = { - _id: 'foo-namespace:foo:bar', - _primary_term: 24, - _seq_no: 42, - _source: { - type: 'foo', - foo: { - meaning: 42, - nested: { stuff: 'here' }, - }, - migrationVersion: { - foo: '1.2.3', - bar: '9.8.7', - }, - namespace: 'foo-namespace', - updated_at: String(new Date()), - references: [], - }, - }; + test(`copies _id to id if prefixed by namespace and type`, () => { + const _id = `${raw._source.namespace}:${raw._id}`; + const _actual = namespaceAgnosticSerializer.rawToSavedObject({ ...raw, _id }); + expect(_actual).toHaveProperty('id', _id); + }); - expect(serializer.savedObjectToRaw(serializer.rawToSavedObject(_.cloneDeep(raw)))).toEqual( - raw - ); + test(`doesn't copy _source.namespace to namespace`, () => { + expect(actual).not.toHaveProperty('namespace'); }); + }); - test('it handles unprefixed ids', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'universe', - _source: { - type: 'hello', - }, - }); + describe('namespace-agnostic type with namespaces', () => { + const raw = createSampleDoc({ _source: { namespaces: ['baz'] } }); + const actual = namespaceAgnosticSerializer.rawToSavedObject(raw); - expect(actual).toHaveProperty('id', 'universe'); + test(`doesn't copy _source.namespaces to namespaces`, () => { + expect(actual).not.toHaveProperty('namespaces'); }); + }); - describe('namespaced type without a namespace', () => { - test(`removes type prefix from _id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - }, - }); + describe('single-namespace type without a namespace', () => { + const raw = createSampleDoc({}); + const actual = singleNamespaceSerializer.rawToSavedObject(raw); - expect(actual).toHaveProperty('id', 'bar'); - }); + test(`removes type prefix from _id`, () => { + expect(actual).toHaveProperty('id', 'bar'); + }); - test(`if prefixed by random prefix and type it copies _id to id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'random:foo:bar', - _source: { - type: 'foo', - }, - }); + test(`copies _id to id if prefixed by random prefix and type`, () => { + const _id = `random:${raw._id}`; + const _actual = singleNamespaceSerializer.rawToSavedObject({ ...raw, _id }); + expect(_actual).toHaveProperty('id', _id); + }); - expect(actual).toHaveProperty('id', 'random:foo:bar'); - }); + test(`doesn't specify namespace`, () => { + expect(actual).not.toHaveProperty('namespace'); + }); + }); - test(`doesn't specify namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - }, - }); + describe('single-namespace type with a namespace', () => { + const namespace = 'baz'; + const raw = createSampleDoc({ _source: { namespace } }); + const actual = singleNamespaceSerializer.rawToSavedObject(raw); - expect(actual).not.toHaveProperty('namespace'); - }); + test(`removes type and namespace prefix from _id`, () => { + const _id = `${namespace}:${raw._id}`; + const _actual = singleNamespaceSerializer.rawToSavedObject({ ...raw, _id }); + expect(_actual).toHaveProperty('id', 'bar'); }); - describe('namespaced type with a namespace', () => { - test(`removes type and namespace prefix from _id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'baz:foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); + test(`copies _id to id if prefixed only by type`, () => { + expect(actual).toHaveProperty('id', raw._id); + }); - expect(actual).toHaveProperty('id', 'bar'); - }); + test(`copies _id to id if prefixed by random prefix and type`, () => { + const _id = `random:${raw._id}`; + const _actual = singleNamespaceSerializer.rawToSavedObject({ ...raw, _id }); + expect(_actual).toHaveProperty('id', _id); + }); - test(`if prefixed by only type it copies _id to id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); + test(`copies _source.namespace to namespace`, () => { + expect(actual).toHaveProperty('namespace', 'baz'); + }); + }); - expect(actual).toHaveProperty('id', 'foo:bar'); - }); + describe('single-namespace type with namespaces', () => { + const raw = createSampleDoc({ _source: { namespaces: ['baz'] } }); + const actual = singleNamespaceSerializer.rawToSavedObject(raw); - test(`if prefixed by random prefix and type it copies _id to id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'random:foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); + test(`doesn't copy _source.namespaces to namespaces`, () => { + expect(actual).not.toHaveProperty('namespaces'); + }); + }); - expect(actual).toHaveProperty('id', 'random:foo:bar'); - }); + describe('multi-namespace type with a namespace', () => { + const raw = createSampleDoc({ _source: { namespace: 'baz' } }); + const actual = multiNamespaceSerializer.rawToSavedObject(raw); - test(`copies _source.namespace to namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'baz:foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); + test(`removes type prefix from _id`, () => { + expect(actual).toHaveProperty('id', 'bar'); + }); - expect(actual).toHaveProperty('namespace', 'baz'); - }); + test(`copies _id to id if prefixed by namespace and type`, () => { + const _id = `${raw._source.namespace}:${raw._id}`; + const _actual = multiNamespaceSerializer.rawToSavedObject({ ...raw, _id }); + expect(_actual).toHaveProperty('id', _id); }); - describe('namespace agnostic type with a namespace', () => { - beforeEach(() => { - typeRegistry.isNamespaceAgnostic.mockReturnValue(true); - }); + test(`doesn't copy _source.namespace to namespace`, () => { + expect(actual).not.toHaveProperty('namespace'); + }); + }); - test(`removes type prefix from _id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); + describe('multi-namespace type with namespaces', () => { + const raw = createSampleDoc({ _source: { namespaces: ['baz'] } }); + const actual = multiNamespaceSerializer.rawToSavedObject(raw); - expect(actual).toHaveProperty('id', 'bar'); - }); + test(`copies _source.namespaces to namespaces`, () => { + expect(actual).toHaveProperty('namespaces', ['baz']); + }); + }); +}); - test(`if prefixed by namespace and type it copies _id to id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'baz:foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); +describe('#savedObjectToRaw', () => { + test('it copies the type property to _source.type and uses the ROOT_TYPE as _type', () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + attributes: {}, + } as any); - expect(actual).toHaveProperty('id', 'baz:foo:bar'); - }); + expect(actual._source).toHaveProperty('type', 'foo'); + }); - test(`doesn't copy _source.namespace to namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'baz:foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); + test('it copies the references property to _source.references', () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + id: '1', + type: 'foo', + attributes: {}, + references: [{ name: 'ref_0', type: 'index-pattern', id: 'pattern*' }], + }); + expect(actual._source).toHaveProperty('references', [ + { + name: 'ref_0', + type: 'index-pattern', + id: 'pattern*', + }, + ]); + }); + + test('if specified it copies the updated_at property to _source.updated_at', () => { + const now = new Date(); + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + updated_at: now, + } as any); + + expect(actual._source).toHaveProperty('updated_at', now); + }); + + test(`if unspecified it doesn't add updated_at property to _source`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + } as any); - expect(actual).not.toHaveProperty('namespace'); - }); + expect(actual._source).not.toHaveProperty('updated_at'); + }); + + test('it copies the migrationVersion property to _source.migrationVersion', () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + migrationVersion: { + foo: '1.2.3', + bar: '9.8.7', + }, + } as any); + + expect(actual._source).toHaveProperty('migrationVersion', { + foo: '1.2.3', + bar: '9.8.7', }); }); - describe('#savedObjectToRaw', () => { - test('it copies the type property to _source.type and uses the ROOT_TYPE as _type', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: 'foo', + test(`if unspecified it doesn't add migrationVersion property to _source`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + } as any); + + expect(actual._source).not.toHaveProperty('migrationVersion'); + }); + + test('it decodes the version property to _seq_no and _primary_term', () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + version: encodeVersion(1, 2), + } as any); + + expect(actual).toHaveProperty('_seq_no', 1); + expect(actual).toHaveProperty('_primary_term', 2); + }); + + test(`if unspecified it doesn't add _seq_no or _primary_term properties`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + } as any); + + expect(actual).not.toHaveProperty('_seq_no'); + expect(actual).not.toHaveProperty('_primary_term'); + }); + + test(`if version invalid it throws`, () => { + expect(() => + singleNamespaceSerializer.savedObjectToRaw({ + type: '', attributes: {}, - } as any); + version: 'foo', + } as any) + ).toThrowErrorMatchingInlineSnapshot(`"Invalid version [foo]"`); + }); - expect(actual._source).toHaveProperty('type', 'foo'); + test('it copies attributes to _source[type]', () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + attributes: { + foo: true, + bar: 'quz', + }, + } as any); + + expect(actual._source).toHaveProperty('foo', { + foo: true, + bar: 'quz', }); + }); - test('it copies the references property to _source.references', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - id: '1', + describe('single-namespace type without a namespace', () => { + test('generates an id prefixed with type, if no id is specified', () => { + const v1 = singleNamespaceSerializer.savedObjectToRaw({ type: 'foo', - attributes: {}, - references: [{ name: 'ref_0', type: 'index-pattern', id: 'pattern*' }], - }); - expect(actual._source).toHaveProperty('references', [ - { - name: 'ref_0', - type: 'index-pattern', - id: 'pattern*', - }, - ]); + attributes: { bar: true }, + } as any); + + const v2 = singleNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + attributes: { bar: true }, + } as any); + + expect(v1._id).toMatch(/^foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); }); - test('if specified it copies the updated_at property to _source.updated_at', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const now = new Date(); - const actual = serializer.savedObjectToRaw({ + test(`doesn't specify _source.namespace`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ type: '', attributes: {}, - updated_at: now, } as any); - expect(actual._source).toHaveProperty('updated_at', now); + expect(actual._source).not.toHaveProperty('namespace'); }); + }); - test(`if unspecified it doesn't add updated_at property to _source`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: '', + describe('single-namespace type with a namespace', () => { + test('generates an id prefixed with namespace and type, if no id is specified', () => { + const v1 = singleNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', + attributes: { bar: true }, + } as any); + + const v2 = singleNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', + attributes: { bar: true }, + } as any); + + expect(v1._id).toMatch(/^bar\:foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); + }); + + test(`it copies namespace to _source.namespace`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: 'foo', attributes: {}, + namespace: 'bar', } as any); - expect(actual._source).not.toHaveProperty('updated_at'); + expect(actual._source).toHaveProperty('namespace', 'bar'); }); + }); - test('it copies the migrationVersion property to _source.migrationVersion', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: '', + describe('single-namespace type with namespaces', () => { + test('generates an id prefixed with type, if no id is specified', () => { + const v1 = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], + attributes: { bar: true }, + } as any); + + const v2 = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], + attributes: { bar: true }, + } as any); + + expect(v1._id).toMatch(/^foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); + }); + + test(`doesn't specify _source.namespaces`, () => { + const actual = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], attributes: {}, - migrationVersion: { - foo: '1.2.3', - bar: '9.8.7', - }, } as any); - expect(actual._source).toHaveProperty('migrationVersion', { - foo: '1.2.3', - bar: '9.8.7', - }); + expect(actual._source).not.toHaveProperty('namespaces'); }); + }); - test(`if unspecified it doesn't add migrationVersion property to _source`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: '', + describe('namespace-agnostic type with a namespace', () => { + test('generates an id prefixed with type, if no id is specified', () => { + const v1 = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', + attributes: { bar: true }, + } as any); + + const v2 = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', + attributes: { bar: true }, + } as any); + + expect(v1._id).toMatch(/^foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); + }); + + test(`doesn't specify _source.namespace`, () => { + const actual = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', attributes: {}, } as any); - expect(actual._source).not.toHaveProperty('migrationVersion'); + expect(actual._source).not.toHaveProperty('namespace'); }); + }); - test('it decodes the version property to _seq_no and _primary_term', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: '', + describe('namespace-agnostic type with namespaces', () => { + test('generates an id prefixed with type, if no id is specified', () => { + const v1 = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], + attributes: { bar: true }, + } as any); + + const v2 = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], + attributes: { bar: true }, + } as any); + + expect(v1._id).toMatch(/^foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); + }); + + test(`doesn't specify _source.namespaces`, () => { + const actual = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], attributes: {}, - version: encodeVersion(1, 2), } as any); - expect(actual).toHaveProperty('_seq_no', 1); - expect(actual).toHaveProperty('_primary_term', 2); + expect(actual._source).not.toHaveProperty('namespaces'); }); + }); - test(`if unspecified it doesn't add _seq_no or _primary_term properties`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: '', + describe('multi-namespace type with a namespace', () => { + test('generates an id prefixed with type, if no id is specified', () => { + const v1 = multiNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', + attributes: { bar: true }, + } as any); + + const v2 = multiNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', + attributes: { bar: true }, + } as any); + + expect(v1._id).toMatch(/^foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); + }); + + test(`doesn't specify _source.namespace`, () => { + const actual = multiNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', attributes: {}, } as any); - expect(actual).not.toHaveProperty('_seq_no'); - expect(actual).not.toHaveProperty('_primary_term'); + expect(actual._source).not.toHaveProperty('namespace'); }); + }); + + describe('multi-namespace type with namespaces', () => { + test('generates an id prefixed with type, if no id is specified', () => { + const v1 = multiNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], + attributes: { bar: true }, + } as any); + + const v2 = multiNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], + attributes: { bar: true }, + } as any); - test(`if version invalid it throws`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect(() => - serializer.savedObjectToRaw({ - type: '', - attributes: {}, - version: 'foo', - } as any) - ).toThrowErrorMatchingInlineSnapshot(`"Invalid version [foo]"`); + expect(v1._id).toMatch(/^foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); }); - test('it copies attributes to _source[type]', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ + test(`it copies namespaces to _source.namespaces`, () => { + const actual = multiNamespaceSerializer.savedObjectToRaw({ type: 'foo', - attributes: { - foo: true, - bar: 'quz', - }, + namespaces: ['bar'], + attributes: {}, } as any); - expect(actual._source).toHaveProperty('foo', { - foo: true, - bar: 'quz', - }); + expect(actual._source).toHaveProperty('namespaces', ['bar']); }); + }); +}); - describe('namespaced type without a namespace', () => { - test('generates an id prefixed with type, if no id is specified', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const v1 = serializer.savedObjectToRaw({ - type: 'foo', - attributes: { - bar: true, +describe('#isRawSavedObject', () => { + describe('single-namespace type without a namespace', () => { + test('is true if the id is prefixed and the type matches', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + hello: {}, }, - } as any); + }) + ).toBeTruthy(); + }); - const v2 = serializer.savedObjectToRaw({ - type: 'foo', - attributes: { - bar: true, + test('is false if the id is not prefixed', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'world', + _source: { + type: 'hello', + hello: {}, }, - } as any); + }) + ).toBeFalsy(); + }); - expect(v1._id).toMatch(/foo\:[\w-]+$/); - expect(v1._id).not.toEqual(v2._id); - }); + test('is false if the type attribute is missing', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + hello: {}, + } as any, + }) + ).toBeFalsy(); + }); - test(`doesn't specify _source.namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: '', - attributes: {}, - } as any); + test(`is false if the type prefix omits the :`, () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'helloworld', + _source: { + type: 'hello', + hello: {}, + }, + }) + ).toBeFalsy(); + }); - expect(actual._source).not.toHaveProperty('namespace'); - }); + test('is false if the type attribute does not match the id', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'jam', + jam: {}, + hello: {}, + }, + }) + ).toBeFalsy(); }); - describe('namespaced type with a namespace', () => { - test('generates an id prefixed with namespace and type, if no id is specified', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const v1 = serializer.savedObjectToRaw({ - type: 'foo', - namespace: 'bar', - attributes: { - bar: true, + test('is false if there is no [type] attribute', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + jam: {}, }, - } as any); + }) + ).toBeFalsy(); + }); + }); - const v2 = serializer.savedObjectToRaw({ - type: 'foo', - namespace: 'bar', - attributes: { - bar: true, + describe('single-namespace type with a namespace', () => { + test('is true if the id is prefixed with type and namespace and the type matches', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'foo:hello:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', }, - } as any); + }) + ).toBeTruthy(); + }); - expect(v1._id).toMatch(/bar\:foo\:[\w-]+$/); - expect(v1._id).not.toEqual(v2._id); - }); + test('is false if the id is not prefixed by anything', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); - test(`it copies namespace to _source.namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: 'foo', - attributes: {}, - namespace: 'bar', - } as any); + test('is false if the id is prefixed only with type and the type matches', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test('is false if the id is prefixed only with namespace and the namespace matches', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'foo:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); - expect(actual._source).toHaveProperty('namespace', 'bar'); - }); + test(`is false if the id prefix omits the trailing :`, () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'foo:helloworld', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); }); - describe('namespace agnostic type with a namespace', () => { - beforeEach(() => { - typeRegistry.isNamespaceAgnostic.mockReturnValue(true); - }); + test('is false if the type attribute is missing', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'foo:hello:world', + _source: { + hello: {}, + namespace: 'foo', + } as any, + }) + ).toBeFalsy(); + }); - test('generates an id prefixed with type, if no id is specified', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const v1 = serializer.savedObjectToRaw({ - type: 'foo', - namespace: 'bar', - attributes: { - bar: true, + test('is false if the type attribute does not match the id', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'foo:hello:world', + _source: { + type: 'jam', + jam: {}, + hello: {}, + namespace: 'foo', }, - } as any); + }) + ).toBeFalsy(); + }); - const v2 = serializer.savedObjectToRaw({ - type: 'foo', - namespace: 'bar', - attributes: { - bar: true, + test('is false if the namespace attribute does not match the id', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'bar:jam:world', + _source: { + type: 'jam', + jam: {}, + hello: {}, + namespace: 'foo', }, - } as any); + }) + ).toBeFalsy(); + }); - expect(v1._id).toMatch(/foo\:[\w-]+$/); - expect(v1._id).not.toEqual(v2._id); - }); + test('is false if there is no [type] attribute', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + jam: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + }); - test(`doesn't specify _source.namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: 'foo', - namespace: 'bar', - attributes: {}, - } as any); + describe('namespace-agnostic type with a namespace', () => { + test('is true if the id is prefixed with type and the type matches', () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeTruthy(); + }); + + test('is false if the id is not prefixed', () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test('is false if the id is prefixed with type and namespace', () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'foo:hello:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test(`is false if the type prefix omits the :`, () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'helloworld', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test('is false if the type attribute is missing', () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + hello: {}, + namespace: 'foo', + } as any, + }) + ).toBeFalsy(); + }); + + test('is false if the type attribute does not match the id', () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'jam', + jam: {}, + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test('is false if there is no [type] attribute', () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + jam: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + }); +}); - expect(actual._source).not.toHaveProperty('namespace'); - }); +describe('#generateRawId', () => { + describe('single-namespace type without a namespace', () => { + test('generates an id if none is specified', () => { + const id = singleNamespaceSerializer.generateRawId('', 'goodbye'); + expect(id).toMatch(/^goodbye\:[\w-]+$/); + }); + + test('uses the id that is specified', () => { + const id = singleNamespaceSerializer.generateRawId('', 'hello', 'world'); + expect(id).toEqual('hello:world'); }); }); - describe('#isRawSavedObject', () => { - describe('namespaced type without a namespace', () => { - test('is true if the id is prefixed and the type matches', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'hello', - hello: {}, - }, - }) - ).toBeTruthy(); - }); - - test('is false if the id is not prefixed', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'world', - _source: { - type: 'hello', - hello: {}, - }, - }) - ).toBeFalsy(); - }); - - test('is false if the type attribute is missing', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - hello: {}, - } as any, - }) - ).toBeFalsy(); - }); - - test(`is false if the type prefix omits the :`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'helloworld', - _source: { - type: 'hello', - hello: {}, - }, - }) - ).toBeFalsy(); - }); - - test('is false if the type attribute does not match the id', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'jam', - jam: {}, - hello: {}, - }, - }) - ).toBeFalsy(); - }); - - test('is false if there is no [type] attribute', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'hello', - jam: {}, - }, - }) - ).toBeFalsy(); - }); - }); - - describe('namespaced type with a namespace', () => { - test('is true if the id is prefixed with type and namespace and the type matches', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'foo:hello:world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeTruthy(); - }); - - test('is false if the id is not prefixed by anything', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if the id is prefixed only with type and the type matches', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if the id is prefixed only with namespace and the namespace matches', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'foo:world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test(`is false if the id prefix omits the trailing :`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'foo:helloworld', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if the type attribute is missing', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'foo:hello:world', - _source: { - hello: {}, - namespace: 'foo', - } as any, - }) - ).toBeFalsy(); - }); - - test('is false if the type attribute does not match the id', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'foo:hello:world', - _source: { - type: 'jam', - jam: {}, - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if the namespace attribute does not match the id', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'bar:jam:world', - _source: { - type: 'jam', - jam: {}, - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if there is no [type] attribute', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'hello', - jam: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - }); - - describe('namespace agnostic type with a namespace', () => { - beforeEach(() => { - typeRegistry.isNamespaceAgnostic.mockReturnValue(true); - }); - - test('is true if the id is prefixed with type and the type matches', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeTruthy(); - }); - - test('is false if the id is not prefixed', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if the id is prefixed with type and namespace', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'foo:hello:world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test(`is false if the type prefix omits the :`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'helloworld', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if the type attribute is missing', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - hello: {}, - namespace: 'foo', - } as any, - }) - ).toBeFalsy(); - }); - - test('is false if the type attribute does not match the id', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'jam', - jam: {}, - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if there is no [type] attribute', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'hello', - jam: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); + describe('single-namespace type with a namespace', () => { + test('generates an id if none is specified and prefixes namespace', () => { + const id = singleNamespaceSerializer.generateRawId('foo', 'goodbye'); + expect(id).toMatch(/^foo:goodbye\:[\w-]+$/); + }); + + test('uses the id that is specified and prefixes the namespace', () => { + const id = singleNamespaceSerializer.generateRawId('foo', 'hello', 'world'); + expect(id).toEqual('foo:hello:world'); }); }); - describe('#generateRawId', () => { - describe('namespaced type without a namespace', () => { - test('generates an id if none is specified', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const id = serializer.generateRawId('', 'goodbye'); - expect(id).toMatch(/goodbye\:[\w-]+$/); - }); - - test('uses the id that is specified', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const id = serializer.generateRawId('', 'hello', 'world'); - expect(id).toMatch('hello:world'); - }); - }); - - describe('namespaced type with a namespace', () => { - test('generates an id if none is specified and prefixes namespace', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const id = serializer.generateRawId('foo', 'goodbye'); - expect(id).toMatch(/foo:goodbye\:[\w-]+$/); - }); - - test('uses the id that is specified and prefixes the namespace', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const id = serializer.generateRawId('foo', 'hello', 'world'); - expect(id).toMatch('foo:hello:world'); - }); - }); - - describe('namespace agnostic type with a namespace', () => { - test(`generates an id if none is specified and doesn't prefix namespace`, () => { - typeRegistry.isNamespaceAgnostic.mockReturnValue(true); - - const serializer = new SavedObjectsSerializer(typeRegistry); - const id = serializer.generateRawId('foo', 'goodbye'); - expect(id).toMatch(/goodbye\:[\w-]+$/); - }); - - test(`uses the id that is specified and doesn't prefix the namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const id = serializer.generateRawId('foo', 'hello', 'world'); - expect(id).toMatch('hello:world'); - }); + describe('namespace-agnostic type with a namespace', () => { + test(`generates an id if none is specified and doesn't prefix namespace`, () => { + const id = namespaceAgnosticSerializer.generateRawId('foo', 'goodbye'); + expect(id).toMatch(/^goodbye\:[\w-]+$/); + }); + + test(`uses the id that is specified and doesn't prefix the namespace`, () => { + const id = namespaceAgnosticSerializer.generateRawId('foo', 'hello', 'world'); + expect(id).toEqual('hello:world'); }); }); }); diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts index 99d6b0c6b59f9..3b19d494d8ecf 100644 --- a/src/core/server/saved_objects/serialization/serializer.ts +++ b/src/core/server/saved_objects/serialization/serializer.ts @@ -49,7 +49,7 @@ export class SavedObjectsSerializer { public isRawSavedObject(rawDoc: SavedObjectsRawDoc) { const { type, namespace } = rawDoc._source; const namespacePrefix = - namespace && !this.registry.isNamespaceAgnostic(type) ? `${namespace}:` : ''; + namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : ''; return Boolean( type && rawDoc._id.startsWith(`${namespacePrefix}${type}:`) && @@ -64,7 +64,7 @@ export class SavedObjectsSerializer { */ public rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc { const { _id, _source, _seq_no, _primary_term } = doc; - const { type, namespace } = _source; + const { type, namespace, namespaces } = _source; const version = _seq_no != null || _primary_term != null @@ -74,7 +74,8 @@ export class SavedObjectsSerializer { return { type, id: this.trimIdPrefix(namespace, type, _id), - ...(namespace && !this.registry.isNamespaceAgnostic(type) && { namespace }), + ...(namespace && this.registry.isSingleNamespace(type) && { namespace }), + ...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }), attributes: _source[type], references: _source.references || [], ...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }), @@ -93,6 +94,7 @@ export class SavedObjectsSerializer { id, type, namespace, + namespaces, attributes, migrationVersion, updated_at, @@ -103,7 +105,8 @@ export class SavedObjectsSerializer { [type]: attributes, type, references, - ...(namespace && !this.registry.isNamespaceAgnostic(type) && { namespace }), + ...(namespace && this.registry.isSingleNamespace(type) && { namespace }), + ...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }), ...(migrationVersion && { migrationVersion }), ...(updated_at && { updated_at }), }; @@ -124,7 +127,7 @@ export class SavedObjectsSerializer { */ public generateRawId(namespace: string | undefined, type: string, id?: string) { const namespacePrefix = - namespace && !this.registry.isNamespaceAgnostic(type) ? `${namespace}:` : ''; + namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : ''; return `${namespacePrefix}${type}:${id || uuid.v1()}`; } @@ -133,7 +136,7 @@ export class SavedObjectsSerializer { assertNonEmptyString(type, 'saved object type'); const namespacePrefix = - namespace && !this.registry.isNamespaceAgnostic(type) ? `${namespace}:` : ''; + namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : ''; const prefix = `${namespacePrefix}${type}:`; if (!id.startsWith(prefix)) { diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index dfaec127ba159..7ea61f67e9496 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -36,6 +36,7 @@ export interface SavedObjectsRawDoc { export interface SavedObjectsRawDocSource { type: string; namespace?: string; + namespaces?: string[]; migrationVersion?: SavedObjectsMigrationVersion; updated_at?: string; references?: SavedObjectReference[]; @@ -54,6 +55,7 @@ interface SavedObjectDoc { id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional type: string; namespace?: string; + namespaces?: string[]; migrationVersion?: SavedObjectsMigrationVersion; version?: string; updated_at?: string; diff --git a/src/core/server/saved_objects/service/lib/__snapshots__/repository.test.js.snap b/src/core/server/saved_objects/service/lib/__snapshots__/repository.test.js.snap deleted file mode 100644 index 609906c97d599..0000000000000 --- a/src/core/server/saved_objects/service/lib/__snapshots__/repository.test.js.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SavedObjectsRepository #deleteByNamespace requires namespace to be a string 1`] = `"namespace is required, and must be a string"`; - -exports[`SavedObjectsRepository #deleteByNamespace requires namespace to be defined 1`] = `"namespace is required, and must be a string"`; diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts index 2fd9b487f470a..1fdebd87397eb 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts @@ -100,6 +100,38 @@ describe('savedObjectsClient/decorateEsError', () => { expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(true); }); + describe('when es.BadRequest has a reason', () => { + it('makes a SavedObjectsClient/esCannotExecuteScriptError error when script context is disabled', () => { + const error = new esErrors.BadRequest(); + (error as Record).body = { + error: { reason: 'cannot execute scripts using [update] context' }, + }; + expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false); + }); + + it('makes a SavedObjectsClient/esCannotExecuteScriptError error when inline scripts are disabled', () => { + const error = new esErrors.BadRequest(); + (error as Record).body = { + error: { reason: 'cannot execute [inline] scripts' }, + }; + expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false); + }); + + it('makes a SavedObjectsClient/BadRequest error for any other reason', () => { + const error = new esErrors.BadRequest(); + (error as Record).body = { error: { reason: 'some other reason' } }; + expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(true); + }); + }); + it('returns other errors as Boom errors', () => { const error = new Error(); expect(error).not.toHaveProperty('isBoom'); diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.ts index eb9bc89636435..e57f08aa7a527 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.ts @@ -35,6 +35,8 @@ const { NotFound, BadRequest, } = legacyElasticsearch.errors; +const SCRIPT_CONTEXT_DISABLED_REGEX = /(?:cannot execute scripts using \[)([a-z]*)(?:\] context)/; +const INLINE_SCRIPTS_DISABLED_MESSAGE = 'cannot execute [inline] scripts'; import { SavedObjectsErrorHelpers } from './errors'; @@ -43,7 +45,7 @@ export function decorateEsError(error: Error) { throw new Error('Expected an instance of Error'); } - const { reason } = get(error, 'body.error', { reason: undefined }); + const { reason } = get(error, 'body.error', { reason: undefined }) as { reason?: string }; if ( error instanceof ConnectionFault || error instanceof ServiceUnavailable || @@ -74,6 +76,12 @@ export function decorateEsError(error: Error) { } if (error instanceof BadRequest) { + if ( + SCRIPT_CONTEXT_DISABLED_REGEX.test(reason || '') || + reason === INLINE_SCRIPTS_DISABLED_MESSAGE + ) { + return SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(error, reason); + } return SavedObjectsErrorHelpers.decorateBadRequestError(error, reason); } diff --git a/src/core/server/saved_objects/service/lib/errors.test.ts b/src/core/server/saved_objects/service/lib/errors.test.ts index 12fc913f93090..4a43835d795d1 100644 --- a/src/core/server/saved_objects/service/lib/errors.test.ts +++ b/src/core/server/saved_objects/service/lib/errors.test.ts @@ -34,6 +34,7 @@ describe('savedObjectsClient/errorTypes', () => { }); it('has boom properties', () => { + expect(errorObj).toHaveProperty('isBoom', true); expect(errorObj.output.payload).toMatchObject({ statusCode: 400, message: "Unsupported saved object type: 'someType': Bad Request", @@ -57,6 +58,7 @@ describe('savedObjectsClient/errorTypes', () => { }); it('has boom properties', () => { + expect(errorObj).toHaveProperty('isBoom', true); expect(errorObj.output.payload).toMatchObject({ statusCode: 400, message: 'test reason message: Bad Request', @@ -80,14 +82,7 @@ describe('savedObjectsClient/errorTypes', () => { it('adds boom properties', () => { const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(400); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - SavedObjectsErrorHelpers.decorateBadRequestError(error); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -95,6 +90,7 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); + it('prefixes message with passed reason', () => { const error = SavedObjectsErrorHelpers.decorateBadRequestError( new Error('foobar'), @@ -102,13 +98,21 @@ describe('savedObjectsClient/errorTypes', () => { ); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); + it('sets statusCode to 400', () => { const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 400); }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateBadRequestError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); }); }); }); + describe('NotAuthorized error', () => { describe('decorateNotAuthorizedError', () => { it('returns original object', () => { @@ -125,14 +129,7 @@ describe('savedObjectsClient/errorTypes', () => { it('adds boom properties', () => { const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(401); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - SavedObjectsErrorHelpers.decorateNotAuthorizedError(error); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -140,6 +137,7 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); + it('prefixes message with passed reason', () => { const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError( new Error('foobar'), @@ -147,13 +145,21 @@ describe('savedObjectsClient/errorTypes', () => { ); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); + it('sets statusCode to 401', () => { const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 401); }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateNotAuthorizedError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); }); }); }); + describe('Forbidden error', () => { describe('decorateForbiddenError', () => { it('returns original object', () => { @@ -170,14 +176,7 @@ describe('savedObjectsClient/errorTypes', () => { it('adds boom properties', () => { const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(403); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - SavedObjectsErrorHelpers.decorateForbiddenError(error); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -185,17 +184,26 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); + it('prefixes message with passed reason', () => { const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error('foobar'), 'biz'); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); + it('sets statusCode to 403', () => { const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 403); }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateForbiddenError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); }); }); }); + describe('NotFound error', () => { describe('createGenericNotFoundError', () => { it('makes an error identifiable as a NotFound error', () => { @@ -203,11 +211,9 @@ describe('savedObjectsClient/errorTypes', () => { expect(SavedObjectsErrorHelpers.isNotFoundError(error)).toBe(true); }); - it('is a boom error, has boom properties', () => { + it('returns a boom error', () => { const error = SavedObjectsErrorHelpers.createGenericNotFoundError(); - expect(error).toHaveProperty('isBoom'); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -215,6 +221,7 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.createGenericNotFoundError(); expect(error.output.payload).toHaveProperty('message', 'Not Found'); }); + it('sets statusCode to 404', () => { const error = SavedObjectsErrorHelpers.createGenericNotFoundError(); expect(error.output).toHaveProperty('statusCode', 404); @@ -222,6 +229,7 @@ describe('savedObjectsClient/errorTypes', () => { }); }); }); + describe('Conflict error', () => { describe('decorateConflictError', () => { it('returns original object', () => { @@ -238,14 +246,7 @@ describe('savedObjectsClient/errorTypes', () => { it('adds boom properties', () => { const error = SavedObjectsErrorHelpers.decorateConflictError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(409); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - SavedObjectsErrorHelpers.decorateConflictError(error); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -253,17 +254,77 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.decorateConflictError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); + it('prefixes message with passed reason', () => { const error = SavedObjectsErrorHelpers.decorateConflictError(new Error('foobar'), 'biz'); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); + it('sets statusCode to 409', () => { const error = SavedObjectsErrorHelpers.decorateConflictError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 409); }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateConflictError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); + }); + }); + }); + + describe('EsCannotExecuteScript error', () => { + describe('decorateEsCannotExecuteScriptError', () => { + it('returns original object', () => { + const error = new Error(); + expect(SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(error)).toBe(error); + }); + + it('makes the error identifiable as a EsCannotExecuteScript error', () => { + const error = new Error(); + expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(false); + SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(error); + expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(true); + }); + + it('adds boom properties', () => { + const error = SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(new Error()); + expect(error).toHaveProperty('isBoom', true); + }); + + describe('error.output', () => { + it('defaults to message of error', () => { + const error = SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError( + new Error('foobar') + ); + expect(error.output.payload).toHaveProperty('message', 'foobar'); + }); + + it('prefixes message with passed reason', () => { + const error = SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError( + new Error('foobar'), + 'biz' + ); + expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); + }); + + it('sets statusCode to 501', () => { + const error = SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError( + new Error('foo') + ); + expect(error.output).toHaveProperty('statusCode', 400); + }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); }); }); }); + describe('EsUnavailable error', () => { describe('decorateEsUnavailableError', () => { it('returns original object', () => { @@ -280,14 +341,7 @@ describe('savedObjectsClient/errorTypes', () => { it('adds boom properties', () => { const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(503); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - SavedObjectsErrorHelpers.decorateEsUnavailableError(error); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -295,6 +349,7 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); + it('prefixes message with passed reason', () => { const error = SavedObjectsErrorHelpers.decorateEsUnavailableError( new Error('foobar'), @@ -302,13 +357,21 @@ describe('savedObjectsClient/errorTypes', () => { ); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); + it('sets statusCode to 503', () => { const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 503); }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateEsUnavailableError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); }); }); }); + describe('General error', () => { describe('decorateGeneralError', () => { it('returns original object', () => { @@ -318,14 +381,7 @@ describe('savedObjectsClient/errorTypes', () => { it('adds boom properties', () => { const error = SavedObjectsErrorHelpers.decorateGeneralError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(500); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - SavedObjectsErrorHelpers.decorateGeneralError(error); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -333,10 +389,17 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.decorateGeneralError(new Error('foobar')); expect(error.output.payload.message).toMatch(/internal server error/i); }); + it('sets statusCode to 500', () => { const error = SavedObjectsErrorHelpers.decorateGeneralError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 500); }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateGeneralError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); }); }); }); @@ -363,9 +426,7 @@ describe('savedObjectsClient/errorTypes', () => { it('returns a boom error', () => { const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); - expect(error).toHaveProperty('isBoom'); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(503); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -373,6 +434,7 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); expect(error.output.payload).toHaveProperty('message', 'Automatic index creation failed'); }); + it('sets statusCode to 503', () => { const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); expect(error.output).toHaveProperty('statusCode', 503); diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts index e9138e9b8a347..478c6b6d26d53 100644 --- a/src/core/server/saved_objects/service/lib/errors.ts +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -33,6 +33,8 @@ const CODE_REQUEST_ENTITY_TOO_LARGE = 'SavedObjectsClient/requestEntityTooLarge' const CODE_NOT_FOUND = 'SavedObjectsClient/notFound'; // 409 - Conflict const CODE_CONFLICT = 'SavedObjectsClient/conflict'; +// 400 - Es Cannot Execute Script +const CODE_ES_CANNOT_EXECUTE_SCRIPT = 'SavedObjectsClient/esCannotExecuteScript'; // 503 - Es Unavailable const CODE_ES_UNAVAILABLE = 'SavedObjectsClient/esUnavailable'; // 503 - Unable to automatically create index because of action.auto_create_index setting @@ -152,10 +154,24 @@ export class SavedObjectsErrorHelpers { return decorate(error, CODE_CONFLICT, 409, reason); } + public static createConflictError(type: string, id: string) { + return SavedObjectsErrorHelpers.decorateConflictError( + Boom.conflict(`Saved object [${type}/${id}] conflict`) + ); + } + public static isConflictError(error: Error | DecoratedError) { return isSavedObjectsClientError(error) && error[code] === CODE_CONFLICT; } + public static decorateEsCannotExecuteScriptError(error: Error, reason?: string) { + return decorate(error, CODE_ES_CANNOT_EXECUTE_SCRIPT, 400, reason); + } + + public static isEsCannotExecuteScriptError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_ES_CANNOT_EXECUTE_SCRIPT; + } + public static decorateEsUnavailableError(error: Error, reason?: string) { return decorate(error, CODE_ES_UNAVAILABLE, 503, reason); } diff --git a/src/core/server/saved_objects/service/lib/included_fields.test.ts b/src/core/server/saved_objects/service/lib/included_fields.test.ts index 40d6552c2ad5f..ced99361f1ea0 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.test.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.test.ts @@ -26,7 +26,7 @@ describe('includedFields', () => { it('accepts type string', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('type'); }); @@ -37,6 +37,7 @@ Array [ "config.foo", "secret.foo", "namespace", + "namespaces", "type", "references", "migrationVersion", @@ -48,14 +49,14 @@ Array [ it('accepts field as string', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('config.foo'); }); it('accepts fields as an array', () => { const fields = includedFields('config', ['foo', 'bar']); - expect(fields).toHaveLength(9); + expect(fields).toHaveLength(10); expect(fields).toContain('config.foo'); expect(fields).toContain('config.bar'); }); @@ -69,6 +70,7 @@ Array [ "secret.foo", "secret.bar", "namespace", + "namespaces", "type", "references", "migrationVersion", @@ -81,31 +83,37 @@ Array [ it('includes namespace', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('namespace'); }); + it('includes namespaces', () => { + const fields = includedFields('config', 'foo'); + expect(fields).toHaveLength(8); + expect(fields).toContain('namespaces'); + }); + it('includes references', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('references'); }); it('includes migrationVersion', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('migrationVersion'); }); it('includes updated_at', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('updated_at'); }); it('uses wildcard when type is not provided', () => { const fields = includedFields(undefined, 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('*.foo'); }); @@ -113,7 +121,7 @@ Array [ it('includes legacy field path', () => { const fields = includedFields('config', ['foo', 'bar']); - expect(fields).toHaveLength(9); + expect(fields).toHaveLength(10); expect(fields).toContain('foo'); expect(fields).toContain('bar'); }); diff --git a/src/core/server/saved_objects/service/lib/included_fields.ts b/src/core/server/saved_objects/service/lib/included_fields.ts index f372db5a1a635..c50ac22594008 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.ts @@ -37,6 +37,7 @@ export function includedFields(type: string | string[] = '*', fields?: string[] return [...acc, ...sourceFields.map(f => `${t}.${f}`)]; }, []) .concat('namespace') + .concat('namespaces') .concat('type') .concat('references') .concat('migrationVersion') diff --git a/src/core/server/saved_objects/service/lib/repository.mock.ts b/src/core/server/saved_objects/service/lib/repository.mock.ts index e69c0ff37d1be..afef378b7307b 100644 --- a/src/core/server/saved_objects/service/lib/repository.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.mock.ts @@ -28,6 +28,8 @@ const create = (): jest.Mocked => ({ find: jest.fn(), get: jest.fn(), update: jest.fn(), + addToNamespaces: jest.fn(), + deleteFromNamespaces: jest.fn(), deleteByNamespace: jest.fn(), incrementCounter: jest.fn(), }); diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 2e5eeec04e0a8..927171438ae99 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import _ from 'lodash'; import { SavedObjectsRepository } from './repository'; import * as getSearchDslNS from './search_dsl/search_dsl'; @@ -30,162 +29,31 @@ jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. +const createBadRequestError = (...args) => + SavedObjectsErrorHelpers.createBadRequestError(...args).output.payload; +const createConflictError = (...args) => + SavedObjectsErrorHelpers.createConflictError(...args).output.payload; +const createGenericNotFoundError = (...args) => + SavedObjectsErrorHelpers.createGenericNotFoundError(...args).output.payload; +const createUnsupportedTypeError = (...args) => + SavedObjectsErrorHelpers.createUnsupportedTypeError(...args).output.payload; + describe('SavedObjectsRepository', () => { let callAdminCluster; let savedObjectsRepository; let migrator; + let serializer; const mockTimestamp = '2017-08-14T15:49:14.886Z'; const mockTimestampFields = { updated_at: mockTimestamp }; const mockVersionProps = { _seq_no: 1, _primary_term: 1 }; const mockVersion = encodeHitVersion(mockVersionProps); - const noNamespaceSearchResults = { - hits: { - total: 4, - hits: [ - { - _index: '.kibana', - _id: 'index-pattern:logstash-*', - _score: 1, - ...mockVersionProps, - _source: { - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { - title: 'logstash-*', - timeFieldName: '@timestamp', - notExpandable: true, - }, - }, - }, - { - _index: '.kibana', - _id: 'config:6.0.0-alpha1', - _score: 1, - ...mockVersionProps, - _source: { - type: 'config', - ...mockTimestampFields, - config: { - buildNum: 8467, - defaultIndex: 'logstash-*', - }, - }, - }, - { - _index: '.kibana', - _id: 'index-pattern:stocks-*', - _score: 1, - ...mockVersionProps, - _source: { - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { - title: 'stocks-*', - timeFieldName: '@timestamp', - notExpandable: true, - }, - }, - }, - { - _index: '.kibana', - _id: 'globaltype:something', - _score: 1, - ...mockVersionProps, - _source: { - type: 'globaltype', - ...mockTimestampFields, - globaltype: { - name: 'bar', - }, - }, - }, - ], - }, - }; - - const namespacedSearchResults = { - hits: { - total: 4, - hits: [ - { - _index: '.kibana', - _id: 'foo-namespace:index-pattern:logstash-*', - _score: 1, - ...mockVersionProps, - _source: { - namespace: 'foo-namespace', - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { - title: 'logstash-*', - timeFieldName: '@timestamp', - notExpandable: true, - }, - }, - }, - { - _index: '.kibana', - _id: 'foo-namespace:config:6.0.0-alpha1', - _score: 1, - ...mockVersionProps, - _source: { - namespace: 'foo-namespace', - type: 'config', - ...mockTimestampFields, - config: { - buildNum: 8467, - defaultIndex: 'logstash-*', - }, - }, - }, - { - _index: '.kibana', - _id: 'foo-namespace:index-pattern:stocks-*', - _score: 1, - ...mockVersionProps, - _source: { - namespace: 'foo-namespace', - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { - title: 'stocks-*', - timeFieldName: '@timestamp', - notExpandable: true, - }, - }, - }, - { - _index: '.kibana', - _id: 'globaltype:something', - _score: 1, - ...mockVersionProps, - _source: { - type: 'globaltype', - ...mockTimestampFields, - globaltype: { - name: 'bar', - }, - }, - }, - ], - }, - }; - const deleteByQueryResults = { - took: 27, - timed_out: false, - total: 23, - deleted: 23, - batches: 1, - version_conflicts: 0, - noops: 0, - retries: { bulk: 0, search: 0 }, - throttled_millis: 0, - requests_per_second: -1, - throttled_until_millis: 0, - failures: [], - }; + const CUSTOM_INDEX_TYPE = 'customIndex'; + const NAMESPACE_AGNOSTIC_TYPE = 'globalType'; + const MULTI_NAMESPACE_TYPE = 'shareableType'; + const MULTI_NAMESPACE_CUSTOM_INDEX_TYPE = 'shareableTypeCustomIndex'; + const HIDDEN_TYPE = 'hiddenType'; const mappings = { properties: { @@ -194,43 +62,47 @@ describe('SavedObjectsRepository', () => { type: 'keyword', }, }, - foo: { + 'index-pattern': { properties: { - type: 'keyword', + someField: { + type: 'keyword', + }, }, }, - bar: { + dashboard: { properties: { - type: 'keyword', + otherField: { + type: 'keyword', + }, }, }, - baz: { + [CUSTOM_INDEX_TYPE]: { properties: { type: 'keyword', }, }, - 'index-pattern': { + [NAMESPACE_AGNOSTIC_TYPE]: { properties: { - someField: { + yetAnotherField: { type: 'keyword', }, }, }, - dashboard: { + [MULTI_NAMESPACE_TYPE]: { properties: { - otherField: { + evenYetAnotherField: { type: 'keyword', }, }, }, - globaltype: { + [MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]: { properties: { - yetAnotherField: { + evenYetAnotherField: { type: 'keyword', }, }, }, - hiddenType: { + [HIDDEN_TYPE]: { properties: { someField: { type: 'keyword', @@ -240,96 +112,97 @@ describe('SavedObjectsRepository', () => { }, }; - const typeRegistry = new SavedObjectTypeRegistry(); - typeRegistry.registerType({ - name: 'config', - hidden: false, - namespaceAgnostic: false, - mappings: { - properties: { - type: 'keyword', - }, - }, + const createType = type => ({ + name: type, + mappings: { properties: mappings.properties[type].properties }, }); - typeRegistry.registerType({ - name: 'index-pattern', - hidden: false, - namespaceAgnostic: false, - mappings: { - properties: { - someField: { - type: 'keyword', - }, - }, - }, + + const registry = new SavedObjectTypeRegistry(); + registry.registerType(createType('config')); + registry.registerType(createType('index-pattern')); + registry.registerType(createType('dashboard')); + registry.registerType({ + ...createType(CUSTOM_INDEX_TYPE), + indexPattern: 'custom', }); - typeRegistry.registerType({ - name: 'dashboard', - hidden: false, - namespaceAgnostic: false, - mappings: { - properties: { - otherField: { - type: 'keyword', - }, - }, - }, + registry.registerType({ + ...createType(NAMESPACE_AGNOSTIC_TYPE), + namespaceType: 'agnostic', }); - typeRegistry.registerType({ - name: 'globaltype', - hidden: false, - namespaceAgnostic: true, - mappings: { - properties: { - yetAnotherField: { - type: 'keyword', - }, - }, - }, + registry.registerType({ + ...createType(MULTI_NAMESPACE_TYPE), + namespaceType: 'multiple', }); - typeRegistry.registerType({ - name: 'foo', - hidden: false, - namespaceAgnostic: true, - mappings: { - properties: { - type: 'keyword', - }, - }, + registry.registerType({ + ...createType(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE), + namespaceType: 'multiple', + indexPattern: 'custom', }); - typeRegistry.registerType({ - name: 'bar', - hidden: false, - namespaceAgnostic: true, - mappings: { - properties: { - type: 'keyword', - }, - }, + registry.registerType({ + ...createType(HIDDEN_TYPE), + hidden: true, + namespaceType: 'agnostic', }); - typeRegistry.registerType({ - name: 'baz', - hidden: false, - namespaceAgnostic: false, - indexPattern: 'beats', - mappings: { - properties: { - type: 'keyword', - }, + + const getMockGetResponse = ({ type, id, references, namespace }) => ({ + // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these + found: true, + _id: `${registry.isSingleNamespace(type) && namespace ? `${namespace}:` : ''}${type}:${id}`, + ...mockVersionProps, + _source: { + ...(registry.isSingleNamespace(type) && { namespace }), + ...(registry.isMultiNamespace(type) && { namespaces: [namespace ?? 'default'] }), + type, + [type]: { title: 'Testing' }, + references, + specialProperty: 'specialValue', + ...mockTimestampFields, }, }); - typeRegistry.registerType({ - name: 'hiddenType', - hidden: true, - namespaceAgnostic: true, - mappings: { - properties: { - someField: { - type: 'keyword', - }, - }, + + const getMockMgetResponse = (objects, namespace) => ({ + status: 200, + docs: objects.map(obj => + obj.found === false ? obj : getMockGetResponse({ ...obj, namespace }) + ), + }); + + const expectClusterCalls = (...actions) => { + for (let i = 0; i < actions.length; i++) { + expect(callAdminCluster).toHaveBeenNthCalledWith(i + 1, actions[i], expect.any(Object)); + } + expect(callAdminCluster).toHaveBeenCalledTimes(actions.length); + }; + const expectClusterCallArgs = (args, n = 1) => { + expect(callAdminCluster).toHaveBeenNthCalledWith( + n, + expect.any(String), + expect.objectContaining(args) + ); + }; + + expect.extend({ + toBeDocumentWithoutError(received, type, id) { + if (received.type === type && received.id === id && !received.error) { + return { message: () => `expected type and id not to match without error`, pass: true }; + } else { + return { message: () => `expected type and id to match without error`, pass: false }; + } }, }); + const expectSuccess = ({ type, id }) => expect.toBeDocumentWithoutError(type, id); + const expectError = ({ type, id }) => ({ type, id, error: expect.any(Object) }); + const expectErrorResult = ({ type, id }, error) => ({ type, id, error }); + const expectErrorNotFound = obj => + expectErrorResult(obj, createGenericNotFoundError(obj.type, obj.id)); + const expectErrorConflict = obj => expectErrorResult(obj, createConflictError(obj.type, obj.id)); + const expectErrorInvalidType = obj => + expectErrorResult(obj, createUnsupportedTypeError(obj.type, obj.id)); + + const expectMigrationArgs = (args, contains = true, n = 1) => { + const obj = contains ? expect.objectContaining(args) : expect.not.objectContaining(args); + expect(migrator.migrateDocument).toHaveBeenNthCalledWith(n, obj); + }; beforeEach(() => { callAdminCluster = jest.fn(); @@ -338,16 +211,28 @@ describe('SavedObjectsRepository', () => { runMigrations: async () => ({ status: 'skipped' }), }; - const serializer = new SavedObjectsSerializer(typeRegistry); - const allTypes = typeRegistry.getAllTypes().map(type => type.name); - const allowedTypes = [...new Set(allTypes.filter(type => !typeRegistry.isHidden(type)))]; + // create a mock serializer "shim" so we can track function calls, but use the real serializer's implementation + serializer = { + isRawSavedObject: jest.fn(), + rawToSavedObject: jest.fn(), + savedObjectToRaw: jest.fn(), + generateRawId: jest.fn(), + trimIdPrefix: jest.fn(), + }; + const _serializer = new SavedObjectsSerializer(registry); + Object.keys(serializer).forEach(key => { + serializer[key].mockImplementation((...args) => _serializer[key](...args)); + }); + + const allTypes = registry.getAllTypes().map(type => type.name); + const allowedTypes = [...new Set(allTypes.filter(type => !registry.isHidden(type)))]; savedObjectsRepository = new SavedObjectsRepository({ index: '.kibana-test', mappings, callCluster: callAdminCluster, migrator, - typeRegistry, + typeRegistry: registry, serializer, allowedTypes, }); @@ -356,2644 +241,2819 @@ describe('SavedObjectsRepository', () => { getSearchDslNS.getSearchDsl.mockReset(); }); - describe('#create', () => { - beforeEach(() => { - callAdminCluster.mockImplementation((method, params) => ({ - _id: params.id, - ...mockVersionProps, - })); - }); + const mockMigrationVersion = { foo: '2.3.4' }; + const mockMigrateDocument = doc => ({ + ...doc, + attributes: { + ...doc.attributes, + ...(doc.attributes?.title && { title: `${doc.attributes.title}!!` }), + }, + migrationVersion: mockMigrationVersion, + references: [{ name: 'search_0', type: 'search', id: '123' }], + }); - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); + describe('#addToNamespaces', () => { + const id = 'some-id'; + const type = MULTI_NAMESPACE_TYPE; + const currentNs1 = 'default'; + const currentNs2 = 'foo-namespace'; + const newNs1 = 'bar-namespace'; + const newNs2 = 'baz-namespace'; + + const mockGetResponse = (type, id) => { + // mock a document that exists in two namespaces + const mockResponse = getMockGetResponse({ type, id }); + mockResponse._source.namespaces = [currentNs1, currentNs2]; + callAdminCluster.mockResolvedValueOnce(mockResponse); // this._callCluster('get', ...) + }; - await expect( - savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { - id: 'logstash-*', - namespace: 'foo-namespace', - } - ) - ).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); + const addToNamespacesSuccess = async (type, id, namespaces, options) => { + mockGetResponse(type, id); // this._callCluster('get', ...) + callAdminCluster.mockResolvedValue({ + _id: `${type}:${id}`, + ...mockVersionProps, + result: 'updated', + }); // this._writeToCluster('update', ...) + const result = await savedObjectsRepository.addToNamespaces(type, id, namespaces, options); + expect(callAdminCluster).toHaveBeenCalledTimes(2); + return result; + }; - it('formats Elasticsearch response', async () => { - const response = await savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { - id: 'logstash-*', - namespace: 'foo-namespace', - references: [ - { - name: 'ref_0', - type: 'test', - id: '123', - }, - ], - } - ); + describe('cluster calls', () => { + it(`should use ES get action then update action`, async () => { + await addToNamespacesSuccess(type, id, [newNs1, newNs2]); + expectClusterCalls('get', 'update'); + }); - expect(response).toEqual({ - type: 'index-pattern', - id: 'logstash-*', - ...mockTimestampFields, - version: mockVersion, - attributes: { - title: 'Logstash', - }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '123', - }, - ], + it(`defaults to the version of the existing document`, async () => { + await addToNamespacesSuccess(type, id, [newNs1, newNs2]); + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClusterCallArgs(versionProperties, 2); + }); + + it(`accepts version`, async () => { + await addToNamespacesSuccess(type, id, [newNs1, newNs2], { + version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), + }); + expectClusterCallArgs({ if_seq_no: 100, if_primary_term: 200 }, 2); }); - }); - it('should use ES index action', async () => { - await savedObjectsRepository.create('index-pattern', { - id: 'logstash-*', - title: 'Logstash', + it(`defaults to a refresh setting of wait_for`, async () => { + await addToNamespacesSuccess(type, id, [newNs1, newNs2]); + expectClusterCallArgs({ refresh: 'wait_for' }, 2); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('index', expect.any(Object)); + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await addToNamespacesSuccess(type, id, [newNs1, newNs2], { refresh }); + expectClusterCallArgs({ refresh }, 2); + }); }); - it('should use default index', async () => { - await savedObjectsRepository.create('index-pattern', { - id: 'logstash-*', - title: 'Logstash', + describe('errors', () => { + const expectNotFoundError = async (type, id, namespaces, options) => { + await expect( + savedObjectsRepository.addToNamespaces(type, id, namespaces, options) + ).rejects.toThrowError(createGenericNotFoundError(type, id)); + }; + const expectBadRequestError = async (type, id, namespaces, message) => { + await expect( + savedObjectsRepository.addToNamespaces(type, id, namespaces) + ).rejects.toThrowError(createBadRequestError(message)); + }; + + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id, [newNs1, newNs2]); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'index', - expect.objectContaining({ - index: '.kibana-test', - }) - ); - }); + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id, [newNs1, newNs2]); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - it('should use custom index', async () => { - await savedObjectsRepository.create('baz', { - id: 'logstash-*', - title: 'Logstash', + it(`throws when type is not multi-namespace`, async () => { + const test = async type => { + const message = `${type} doesn't support multiple namespaces`; + await expectBadRequestError(type, id, [newNs1, newNs2], message); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; + await test('index-pattern'); + await test(NAMESPACE_AGNOSTIC_TYPE); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'index', - expect.objectContaining({ - index: 'beats', - }) - ); - }); + it(`throws when namespaces is an empty array`, async () => { + const test = async namespaces => { + const message = 'namespaces must be a non-empty array of strings'; + await expectBadRequestError(type, id, namespaces, message); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; + await test([]); + }); - it('migrates the doc', async () => { - migrator.migrateDocument = doc => { - doc.attributes.title = doc.attributes.title + '!!'; - doc.migrationVersion = { foo: '2.3.4' }; - doc.references = [{ name: 'search_0', type: 'search', id: '123' }]; - return doc; - }; + it(`throws when ES is unable to find the document during get`, async () => { + callAdminCluster.mockResolvedValue({ found: false }); // this._callCluster('get', ...) + await expectNotFoundError(type, id, [newNs1, newNs2]); + expectClusterCalls('get'); + }); - await savedObjectsRepository.create('index-pattern', { - id: 'logstash-*', - title: 'Logstash', + it(`throws when ES is unable to find the index during get`, async () => { + callAdminCluster.mockResolvedValue({ status: 404 }); // this._callCluster('get', ...) + await expectNotFoundError(type, id, [newNs1, newNs2]); + expectClusterCalls('get'); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - body: { - 'index-pattern': { id: 'logstash-*', title: 'Logstash!!' }, - migrationVersion: { foo: '2.3.4' }, - type: 'index-pattern', - updated_at: '2017-08-14T15:49:14.886Z', - references: [{ name: 'search_0', type: 'search', id: '123' }], - }, + it(`throws when the document exists, but not in this namespace`, async () => { + mockGetResponse(type, id); // this._callCluster('get', ...) + await expectNotFoundError(type, id, [newNs1, newNs2], { + namespace: 'some-other-namespace', + }); + expectClusterCalls('get'); }); - }); - it('defaults to a refresh setting of `wait_for`', async () => { - await savedObjectsRepository.create('index-pattern', { - id: 'logstash-*', - title: 'Logstash', + it(`throws when ES is unable to find the document during update`, async () => { + mockGetResponse(type, id); // this._callCluster('get', ...) + callAdminCluster.mockResolvedValue({ status: 404 }); // this._writeToCluster('update', ...) + await expectNotFoundError(type, id, [newNs1, newNs2]); + expectClusterCalls('get', 'update'); }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: 'wait_for', + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + let callAdminClusterCount = 0; + migrator.runMigrations = jest.fn(async () => + // runMigrations should resolve before callAdminCluster is initiated + expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) + ); + await expect(addToNamespacesSuccess(type, id, [newNs1, newNs2])).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveReturnedTimes(2); }); }); - it('accepts custom refresh settings', async () => { - await savedObjectsRepository.create( - 'index-pattern', - { - id: 'logstash-*', - title: 'Logstash', - }, - { - refresh: true, - } - ); + describe('returns', () => { + it(`returns an empty object on success`, async () => { + const result = await addToNamespacesSuccess(type, id, [newNs1, newNs2]); + expect(result).toEqual({}); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: true, + it(`succeeds when adding existing namespaces`, async () => { + const result = await addToNamespacesSuccess(type, id, [currentNs1]); + expect(result).toEqual({}); }); }); + }); - it('should use create action if ID defined and overwrite=false', async () => { - await savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { - id: 'logstash-*', - } - ); + describe('#bulkCreate', () => { + const obj1 = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + }; + const obj2 = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; + const namespace = 'foo-namespace'; - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('create', expect.any(Object)); - }); + const getMockBulkCreateResponse = (objects, namespace) => { + return { + items: objects.map(({ type, id }) => ({ + create: { + _id: `${namespace ? `${namespace}:` : ''}${type}:${id}`, + ...mockVersionProps, + }, + })), + }; + }; - it('allows for id to be provided', async () => { - await savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { id: 'logstash-*' } - ); + const bulkCreateSuccess = async (objects, options) => { + const multiNamespaceObjects = + options?.overwrite && + objects.filter(({ type, id }) => registry.isMultiNamespace(type) && id); + if (multiNamespaceObjects?.length) { + const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace); + callAdminCluster.mockResolvedValueOnce(response); // this._callCluster('mget', ...) + } + const response = getMockBulkCreateResponse(objects, options?.namespace); + callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + const result = await savedObjectsRepository.bulkCreate(objects, options); + expect(callAdminCluster).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 2 : 1); + return result; + }; - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: 'index-pattern:logstash-*', - }) - ); + // bulk create calls have two objects for each source -- the action, and the source + const expectClusterCallArgsAction = ( + objects, + { method, _index = expect.any(String), getId = () => expect.any(String) } + ) => { + const body = []; + for (const { type, id } of objects) { + body.push({ [method]: { _index, _id: getId(type, id) } }); + body.push(expect.any(Object)); + } + expectClusterCallArgs({ body }); + }; + + const expectObjArgs = ({ type, attributes, references }, overrides) => [ + expect.any(Object), + expect.objectContaining({ + [type]: attributes, + references, + type, + ...overrides, + ...mockTimestampFields, + }), + ]; + + const expectSuccessResult = obj => ({ + ...obj, + migrationVersion: undefined, + version: mockVersion, + ...mockTimestampFields, }); - it('self-generates an ID', async () => { - await savedObjectsRepository.create('index-pattern', { - title: 'Logstash', + describe('cluster calls', () => { + it(`should use the ES bulk action by default`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectClusterCalls('bulk'); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/), - }) - ); - }); + it(`should use the ES mget action before bulk action for any types that are multi-namespace, when overwrite=true`, async () => { + const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_TYPE }]; + await bulkCreateSuccess(objects, { overwrite: true }); + expectClusterCalls('mget', 'bulk'); + const docs = [expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj2.id}` })]; + expectClusterCallArgs({ body: { docs } }, 1); + }); - it('prepends namespace to the id and adds namespace to body when providing namespace for namespaced type', async () => { - await savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { - id: 'foo-id', - namespace: 'foo-namespace', - } - ); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: `foo-namespace:index-pattern:foo-id`, - body: expect.objectContaining({ - [`index-pattern`]: { title: 'Logstash' }, - namespace: 'foo-namespace', - type: 'index-pattern', - updated_at: '2017-08-14T15:49:14.886Z', - }), - }) - ); - }); + it(`should use the ES create method if ID is undefined and overwrite=true`, async () => { + const objects = [obj1, obj2].map(obj => ({ ...obj, id: undefined })); + await bulkCreateSuccess(objects, { overwrite: true }); + expectClusterCallArgsAction(objects, { method: 'create' }); + }); - it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { - await savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { - id: 'foo-id', - } - ); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: `index-pattern:foo-id`, - body: expect.objectContaining({ - [`index-pattern`]: { title: 'Logstash' }, - type: 'index-pattern', - updated_at: '2017-08-14T15:49:14.886Z', - }), - }) - ); - }); + it(`should use the ES create method if ID is undefined and overwrite=false`, async () => { + const objects = [obj1, obj2].map(obj => ({ ...obj, id: undefined })); + await bulkCreateSuccess(objects); + expectClusterCallArgsAction(objects, { method: 'create' }); + }); - it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { - await savedObjectsRepository.create( - 'globaltype', - { - title: 'Logstash', - }, - { - id: 'foo-id', - namespace: 'foo-namespace', - } - ); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: `globaltype:foo-id`, - body: expect.objectContaining({ - [`globaltype`]: { title: 'Logstash' }, - type: 'globaltype', - updated_at: '2017-08-14T15:49:14.886Z', - }), - }) - ); - }); - - it('defaults to empty references array if none are provided', async () => { - await savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { - id: 'logstash-*', - } - ); + it(`should use the ES index method if ID is defined and overwrite=true`, async () => { + await bulkCreateSuccess([obj1, obj2], { overwrite: true }); + expectClusterCallArgsAction([obj1, obj2], { method: 'index' }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: expect.objectContaining({ - references: [], - }), - }) - ); - }); - }); + it(`should use the ES create method if ID is defined and overwrite=false`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectClusterCallArgsAction([obj1, obj2], { method: 'create' }); + }); - describe('#bulkCreate', () => { - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); - callAdminCluster.mockReturnValue({ - items: [ - { create: { type: 'config', id: 'config:one', _primary_term: 1, _seq_no: 1 } }, - { - create: { - type: 'index-pattern', - id: 'index-pattern:two', - _primary_term: 1, - _seq_no: 1, - }, - }, - ], + it(`formats the ES request`, async () => { + await bulkCreateSuccess([obj1, obj2]); + const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expectClusterCallArgs({ body }); }); - await expect( - savedObjectsRepository.bulkCreate([ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }, - ]) - ).resolves.toBeDefined(); + it(`adds namespace to request body for any types that are single-namespace`, async () => { + await bulkCreateSuccess([obj1, obj2], { namespace }); + const expected = expect.objectContaining({ namespace }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }); + }); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); + it(`doesn't add namespace to request body for any types that are not single-namespace`, async () => { + const objects = [ + { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_TYPE }, + ]; + await bulkCreateSuccess(objects, { namespace }); + const expected = expect.not.objectContaining({ namespace: expect.anything() }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }); + }); - it('formats Elasticsearch request', async () => { - callAdminCluster.mockReturnValue({ - items: [ - { create: { type: 'config', id: 'config:one', _primary_term: 1, _seq_no: 1 } }, - { create: { type: 'index-pattern', id: 'config:two', _primary_term: 1, _seq_no: 1 } }, - ], + it(`adds namespaces to request body for any types that are multi-namespace`, async () => { + const test = async namespace => { + const objects = [obj1, obj2].map(x => ({ ...x, type: MULTI_NAMESPACE_TYPE })); + const namespaces = [namespace ?? 'default']; + await bulkCreateSuccess(objects, { namespace, overwrite: true }); + const expected = expect.objectContaining({ namespaces }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }, 2); + callAdminCluster.mockReset(); + }; + await test(undefined); + await test(namespace); }); - await savedObjectsRepository.bulkCreate([ - { - type: 'config', - id: 'one', - attributes: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - }, - { - type: 'index-pattern', - id: 'two', - attributes: { title: 'Test Two' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - }, - ]); + it(`doesn't add namespaces to request body for any types that are not multi-namespace`, async () => { + const test = async namespace => { + const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkCreateSuccess(objects, { namespace, overwrite: true }); + const expected = expect.not.objectContaining({ namespaces: expect.anything() }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }); + callAdminCluster.mockReset(); + }; + await test(undefined); + await test(namespace); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - const bulkCalls = callAdminCluster.mock.calls.filter(([path]) => path === 'bulk'); + it(`defaults to a refresh setting of wait_for`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectClusterCallArgs({ refresh: 'wait_for' }); + }); - expect(bulkCalls.length).toEqual(1); + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await bulkCreateSuccess([obj1, obj2], { refresh }); + expectClusterCallArgs({ refresh }); + }); - expect(bulkCalls[0][1].body).toEqual([ - { create: { _index: '.kibana-test', _id: 'config:one' } }, - { - type: 'config', - ...mockTimestampFields, - config: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - }, - { create: { _index: '.kibana-test', _id: 'index-pattern:two' } }, - { - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { title: 'Test Two' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - }, - ]); - }); + it(`should use default index`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectClusterCallArgsAction([obj1, obj2], { method: 'create', _index: '.kibana-test' }); + }); - it('defaults to a refresh setting of `wait_for`', async () => { - callAdminCluster.mockReturnValue({ - items: [{ create: { type: 'config', id: 'config:one', _primary_term: 1, _seq_no: 1 } }], + it(`should use custom index`, async () => { + await bulkCreateSuccess([obj1, obj2].map(x => ({ ...x, type: CUSTOM_INDEX_TYPE }))); + expectClusterCallArgsAction([obj1, obj2], { method: 'create', _index: 'custom' }); }); - await savedObjectsRepository.bulkCreate([ - { - type: 'config', - id: 'one', - attributes: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - }, - ]); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + const getId = (type, id) => `${namespace}:${type}:${id}`; + await bulkCreateSuccess([obj1, obj2], { namespace }); + expectClusterCallArgsAction([obj1, obj2], { method: 'create', getId }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + await bulkCreateSuccess([obj1, obj2]); + expectClusterCallArgsAction([obj1, obj2], { method: 'create', getId }); + }); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: 'wait_for', + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + const objects = [ + { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_TYPE }, + ]; + await bulkCreateSuccess(objects, { namespace }); + expectClusterCallArgsAction(objects, { method: 'create', getId }); }); }); - it('accepts a custom refresh setting', async () => { - callAdminCluster.mockReturnValue({ - items: [ - { create: { type: 'config', id: 'config:one', _primary_term: 1, _seq_no: 1 } }, - { create: { type: 'index-pattern', id: 'config:two', _primary_term: 1, _seq_no: 1 } }, - ], - }); + describe('errors', () => { + const obj3 = { + type: 'dashboard', + id: 'three', + attributes: { title: 'Test Three' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; - await savedObjectsRepository.bulkCreate( - [ - { - type: 'config', - id: 'one', - attributes: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - }, - { - type: 'index-pattern', - id: 'two', - attributes: { title: 'Test Two' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - }, - ], - { - refresh: true, + const bulkCreateError = async (obj, esError, expectedError) => { + const objects = [obj1, obj, obj2]; + const response = getMockBulkCreateResponse(objects); + if (esError) { + response.items[1].create = { error: esError }; } - ); - - expect(callAdminCluster).toHaveBeenCalledTimes(1); + callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + + const result = await savedObjectsRepository.bulkCreate(objects); + expectClusterCalls('bulk'); + const objCall = esError ? expectObjArgs(obj) : []; + const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; + expectClusterCallArgs({ body }); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], + }); + }; - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: true, + it(`returns error when type is invalid`, async () => { + const obj = { ...obj3, type: 'unknownType' }; + await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); }); - }); - it('migrates the docs', async () => { - callAdminCluster.mockReturnValue({ - items: [ - { - create: { - error: false, - _id: '1', - _seq_no: 1, - _primary_term: 1, - }, - }, - { - create: { - error: false, - _id: '2', - _seq_no: 1, - _primary_term: 1, - }, - }, - ], + it(`returns error when type is hidden`, async () => { + const obj = { ...obj3, type: HIDDEN_TYPE }; + await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); }); - migrator.migrateDocument = doc => { - doc.attributes.title = doc.attributes.title + '!!'; - doc.migrationVersion = { foo: '2.3.4' }; - doc.references = [{ name: 'search_0', type: 'search', id: '123' }]; - return doc; - }; - - const bulkCreateResp = await savedObjectsRepository.bulkCreate([ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }, - ]); - - expect(callAdminCluster).toHaveBeenCalledWith( - 'bulk', - expect.objectContaining({ - body: [ - { create: { _index: '.kibana-test', _id: 'config:one' } }, - { - type: 'config', - ...mockTimestampFields, - config: { title: 'Test One!!' }, - migrationVersion: { foo: '2.3.4' }, - references: [{ name: 'search_0', type: 'search', id: '123' }], - }, - { create: { _index: '.kibana-test', _id: 'index-pattern:two' } }, + it(`returns error when there is a conflict with an existing multi-namespace saved object (get)`, async () => { + const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE }; + const response1 = { + status: 200, + docs: [ { - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { title: 'Test Two!!' }, - migrationVersion: { foo: '2.3.4' }, - references: [{ name: 'search_0', type: 'search', id: '123' }], + found: true, + _source: { + type: obj.type, + namespaces: ['bar-namespace'], + }, }, ], - }) - ); - - expect(bulkCreateResp).toEqual({ - saved_objects: [ - { - id: 'one', - type: 'config', - version: mockVersion, - updated_at: mockTimestamp, - attributes: { - title: 'Test One!!', - }, - references: [{ name: 'search_0', type: 'search', id: '123' }], - }, - { - id: 'two', - type: 'index-pattern', - version: mockVersion, - updated_at: mockTimestamp, - attributes: { - title: 'Test Two!!', - }, - references: [{ name: 'search_0', type: 'search', id: '123' }], - }, - ], + }; + callAdminCluster.mockResolvedValueOnce(response1); // this._callCluster('mget', ...) + const response2 = getMockBulkCreateResponse([obj1, obj2]); + callAdminCluster.mockResolvedValue(response2); // this._writeToCluster('bulk', ...) + + const options = { overwrite: true }; + const result = await savedObjectsRepository.bulkCreate([obj1, obj, obj2], options); + expectClusterCalls('mget', 'bulk'); + const body1 = { docs: [expect.objectContaining({ _id: `${obj.type}:${obj.id}` })] }; + expectClusterCallArgs({ body: body1 }, 1); + const body2 = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expectClusterCallArgs({ body: body2 }, 2); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectErrorConflict(obj), expectSuccess(obj2)], + }); }); - }); - it('should overwrite objects if overwrite is truthy', async () => { - callAdminCluster.mockReturnValue({ - items: [{ create: { type: 'foo', id: 'bar', _primary_term: 1, _seq_no: 1 } }], + it(`returns error when there is a version conflict (bulk)`, async () => { + const esError = { type: 'version_conflict_engine_exception' }; + await bulkCreateError(obj3, esError, expectErrorConflict(obj3)); }); - await savedObjectsRepository.bulkCreate([{ type: 'foo', id: 'bar', attributes: {} }], { - overwrite: false, + it(`returns error when document is missing`, async () => { + const esError = { type: 'document_missing_exception' }; + await bulkCreateError(obj3, esError, expectErrorNotFound(obj3)); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'bulk', - expect.objectContaining({ - body: [ - // uses create because overwriting is not allowed - { create: { _index: '.kibana-test', _id: 'foo:bar' } }, - { type: 'foo', ...mockTimestampFields, foo: {}, references: [] }, - ], - }) - ); - - callAdminCluster.mockReset(); - callAdminCluster.mockReturnValue({ - items: [{ create: { type: 'foo', id: 'bar', _primary_term: 1, _seq_no: 1 } }], + it(`returns error reason for other errors`, async () => { + const esError = { reason: 'some_other_error' }; + await bulkCreateError(obj3, esError, expectErrorResult(obj3, { message: esError.reason })); }); - await savedObjectsRepository.bulkCreate([{ type: 'foo', id: 'bar', attributes: {} }], { - overwrite: true, + it(`returns error string for other errors if no reason is defined`, async () => { + const esError = { foo: 'some_other_error' }; + const expectedError = expectErrorResult(obj3, { message: JSON.stringify(esError) }); + await bulkCreateError(obj3, esError, expectedError); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'bulk', - expect.objectContaining({ - body: [ - // uses index because overwriting is allowed - { index: { _index: '.kibana-test', _id: 'foo:bar' } }, - { type: 'foo', ...mockTimestampFields, foo: {}, references: [] }, - ], - }) - ); }); - it('mockReturnValue document errors', async () => { - callAdminCluster.mockResolvedValue({ - errors: false, - items: [ - { - create: { - _id: 'config:one', - error: { - reason: 'type[config] missing', - }, - }, - }, - { - create: { - _id: 'index-pattern:two', - ...mockVersionProps, - }, - }, - ], + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(bulkCreateSuccess([obj1, obj2])).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); - const response = await savedObjectsRepository.bulkCreate([ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }, - ]); + it(`migrates the docs and serializes the migrated docs`, async () => { + migrator.migrateDocument.mockImplementation(mockMigrateDocument); + await bulkCreateSuccess([obj1, obj2]); + const docs = [obj1, obj2].map(x => ({ ...x, ...mockTimestampFields })); + expectMigrationArgs(docs[0], true, 1); + expectMigrationArgs(docs[1], true, 2); - expect(response).toEqual({ - saved_objects: [ - { - id: 'one', - type: 'config', - error: { message: 'type[config] missing' }, - }, - { - id: 'two', - type: 'index-pattern', - version: mockVersion, - ...mockTimestampFields, - attributes: { title: 'Test Two' }, - references: [], - }, - ], + const migratedDocs = docs.map(x => migrator.migrateDocument(x)); + expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(1, migratedDocs[0]); + expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(2, migratedDocs[1]); }); - }); - it('formats Elasticsearch response', async () => { - callAdminCluster.mockResolvedValue({ - errors: false, - items: [ - { - create: { - _id: 'config:one', - ...mockVersionProps, - }, - }, - { - create: { - _id: 'index-pattern:two', - ...mockVersionProps, - }, - }, - ], + it(`adds namespace to body when providing namespace for single-namespace type`, async () => { + await bulkCreateSuccess([obj1, obj2], { namespace }); + expectMigrationArgs({ namespace }, true, 1); + expectMigrationArgs({ namespace }, true, 2); }); - const response = await savedObjectsRepository.bulkCreate( - [ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }, - ], - { - namespace: 'foo-namespace', - } - ); + it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectMigrationArgs({ namespace: expect.anything() }, false, 1); + expectMigrationArgs({ namespace: expect.anything() }, false, 2); + }); - expect(response).toEqual({ - saved_objects: [ - { - id: 'one', - type: 'config', - version: mockVersion, - ...mockTimestampFields, - attributes: { title: 'Test One' }, - references: [], - }, - { - id: 'two', - type: 'index-pattern', - version: mockVersion, - ...mockTimestampFields, - attributes: { title: 'Test Two' }, - references: [], - }, - ], + it(`doesn't add namespace to body when not using single-namespace type`, async () => { + const objects = [ + { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_TYPE }, + ]; + await bulkCreateSuccess(objects, { namespace }); + expectMigrationArgs({ namespace: expect.anything() }, false, 1); + expectMigrationArgs({ namespace: expect.anything() }, false, 2); }); - }); - it('prepends namespace to the id and adds namespace to body when providing namespace for namespaced type', async () => { - callAdminCluster.mockReturnValue({ - items: [ - { - create: { - _id: 'foo-namespace:config:one', - _index: '.kibana-test', - _primary_term: 1, - _seq_no: 2, - }, - }, - { - create: { - _id: 'foo-namespace:index-pattern:two', - _primary_term: 1, - _seq_no: 2, - }, - }, - ], + it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => { + const objects = [obj1, obj2].map(obj => ({ ...obj, type: MULTI_NAMESPACE_TYPE })); + await bulkCreateSuccess(objects, { namespace }); + expectMigrationArgs({ namespaces: [namespace] }, true, 1); + expectMigrationArgs({ namespaces: [namespace] }, true, 2); }); - await savedObjectsRepository.bulkCreate( - [ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }, - ], - { - namespace: 'foo-namespace', - } - ); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'bulk', - expect.objectContaining({ - body: [ - { create: { _index: '.kibana-test', _id: 'foo-namespace:config:one' } }, - { - namespace: 'foo-namespace', - type: 'config', - ...mockTimestampFields, - config: { title: 'Test One' }, - references: [], - }, - { create: { _index: '.kibana-test', _id: 'foo-namespace:index-pattern:two' } }, - { - namespace: 'foo-namespace', - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { title: 'Test Two' }, - references: [], - }, - ], - }) - ); - }); - it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { - callAdminCluster.mockResolvedValue({ - errors: false, - items: [ - { - create: { - _id: 'config:one', - ...mockVersionProps, - }, - }, - { - create: { - _id: 'index-pattern:two', - ...mockVersionProps, - }, - }, - ], + it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => { + const objects = [obj1, obj2].map(obj => ({ ...obj, type: MULTI_NAMESPACE_TYPE })); + await bulkCreateSuccess(objects); + expectMigrationArgs({ namespaces: ['default'] }, true, 1); + expectMigrationArgs({ namespaces: ['default'] }, true, 2); }); - await savedObjectsRepository.bulkCreate([ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }, - ]); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'bulk', - expect.objectContaining({ - body: [ - { create: { _id: 'config:one', _index: '.kibana-test' } }, - { - type: 'config', - ...mockTimestampFields, - config: { title: 'Test One' }, - references: [], - }, - { create: { _id: 'index-pattern:two', _index: '.kibana-test' } }, - { - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { title: 'Test Two' }, - references: [], - }, - ], - }) - ); - }); - it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { - callAdminCluster.mockReturnValue({ - items: [{ create: { _type: '_doc', _id: 'globaltype:one', _primary_term: 1, _seq_no: 2 } }], + it(`doesn't add namespaces to body when not using multi-namespace type`, async () => { + const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkCreateSuccess(objects); + expectMigrationArgs({ namespaces: expect.anything() }, false, 1); + expectMigrationArgs({ namespaces: expect.anything() }, false, 2); }); - await savedObjectsRepository.bulkCreate( - [{ type: 'globaltype', id: 'one', attributes: { title: 'Test One' } }], - { - namespace: 'foo-namespace', - } - ); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'bulk', - expect.objectContaining({ - body: [ - { create: { _id: 'globaltype:one', _index: '.kibana-test' } }, - { - type: 'globaltype', - ...mockTimestampFields, - globaltype: { title: 'Test One' }, - references: [], - }, - ], - }) - ); }); - it('should return objects in the same order regardless of type', () => {}); - }); + describe('returns', () => { + it(`formats the ES response`, async () => { + const result = await bulkCreateSuccess([obj1, obj2]); + expect(result).toEqual({ + saved_objects: [obj1, obj2].map(x => expectSuccessResult(x)), + }); + }); - describe('#delete', () => { - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); - callAdminCluster.mockReturnValue({ result: 'deleted' }); - await expect( - savedObjectsRepository.delete('index-pattern', 'logstash-*', { - namespace: 'foo-namespace', - }) - ).resolves.toBeDefined(); - - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + it(`should return objects in the same order regardless of type`, async () => { + // TODO + }); + + it(`handles a mix of successful creates and errors`, async () => { + const obj = { + type: 'unknownType', + id: 'three', + }; + const objects = [obj1, obj, obj2]; + const response = getMockBulkCreateResponse(objects); + callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + const result = await savedObjectsRepository.bulkCreate(objects); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [expectSuccessResult(obj1), expectError(obj), expectSuccessResult(obj2)], + }); + }); }); + }); - it('throws notFound when ES is unable to find the document', async () => { - expect.assertions(1); + describe('#bulkGet', () => { + const obj1 = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Testing' }, + references: [ + { + name: 'ref_0', + type: 'test', + id: '1', + }, + ], + }; + const obj2 = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Testing' }, + references: [ + { + name: 'ref_0', + type: 'test', + id: '2', + }, + ], + }; + const namespace = 'foo-namespace'; - callAdminCluster.mockResolvedValue({ result: 'not_found' }); + const bulkGet = async (objects, options) => + savedObjectsRepository.bulkGet( + objects.map(({ type, id }) => ({ type, id })), // bulkGet only uses type and id + options + ); + const bulkGetSuccess = async (objects, options) => { + const response = getMockMgetResponse(objects, options?.namespace); + callAdminCluster.mockReturnValue(response); + const result = await bulkGet(objects, options); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + return result; + }; - try { - await savedObjectsRepository.delete('index-pattern', 'logstash-*'); - } catch (e) { - expect(e.output.statusCode).toEqual(404); - } - }); + const _expectClusterCallArgs = ( + objects, + { _index = expect.any(String), getId = () => expect.any(String) } + ) => { + expectClusterCallArgs({ + body: { + docs: objects.map(({ type, id }) => + expect.objectContaining({ + _index, + _id: getId(type, id), + }) + ), + }, + }); + }; - it(`prepends namespace to the id when providing namespace for namespaced type`, async () => { - callAdminCluster.mockReturnValue({ result: 'deleted' }); - await savedObjectsRepository.delete('index-pattern', 'logstash-*', { - namespace: 'foo-namespace', + describe('cluster calls', () => { + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + const getId = (type, id) => `${namespace}:${type}:${id}`; + await bulkGetSuccess([obj1, obj2], { namespace }); + _expectClusterCallArgs([obj1, obj2], { getId }); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('delete', { - id: 'foo-namespace:index-pattern:logstash-*', - refresh: 'wait_for', - index: '.kibana-test', - ignore: [404], + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + await bulkGetSuccess([obj1, obj2]); + _expectClusterCallArgs([obj1, obj2], { getId }); }); - }); - it(`doesn't prepend namespace to the id when providing no namespace for namespaced type`, async () => { - callAdminCluster.mockReturnValue({ result: 'deleted' }); - await savedObjectsRepository.delete('index-pattern', 'logstash-*'); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + let objects = [obj1, obj2].map(obj => ({ ...obj, type: NAMESPACE_AGNOSTIC_TYPE })); + await bulkGetSuccess(objects, { namespace }); + _expectClusterCallArgs(objects, { getId }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('delete', { - id: 'index-pattern:logstash-*', - refresh: 'wait_for', - index: '.kibana-test', - ignore: [404], + callAdminCluster.mockReset(); + objects = [obj1, obj2].map(obj => ({ ...obj, type: MULTI_NAMESPACE_TYPE })); + await bulkGetSuccess(objects, { namespace }); + _expectClusterCallArgs(objects, { getId }); }); }); - it(`doesn't prepend namespace to the id when providing namespace for namespace agnostic type`, async () => { - callAdminCluster.mockReturnValue({ result: 'deleted' }); - await savedObjectsRepository.delete('globaltype', 'logstash-*', { - namespace: 'foo-namespace', - }); + describe('errors', () => { + const bulkGetErrorInvalidType = async ([obj1, obj, obj2]) => { + const response = getMockMgetResponse([obj1, obj2]); + callAdminCluster.mockResolvedValue(response); + const result = await bulkGet([obj1, obj, obj2]); + expectClusterCalls('mget'); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectErrorInvalidType(obj), expectSuccess(obj2)], + }); + }; - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('delete', { - id: 'globaltype:logstash-*', - refresh: 'wait_for', - index: '.kibana-test', - ignore: [404], - }); - }); + const bulkGetErrorNotFound = async ([obj1, obj, obj2], options, response) => { + callAdminCluster.mockResolvedValue(response); + const result = await bulkGet([obj1, obj, obj2], options); + expectClusterCalls('mget'); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectErrorNotFound(obj), expectSuccess(obj2)], + }); + }; - it('defaults to a refresh setting of `wait_for`', async () => { - callAdminCluster.mockReturnValue({ result: 'deleted' }); - await savedObjectsRepository.delete('globaltype', 'logstash-*'); + it(`returns error when type is invalid`, async () => { + const obj = { type: 'unknownType', id: 'three' }; + await bulkGetErrorInvalidType([obj1, obj, obj2]); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: 'wait_for', + it(`returns error when type is hidden`, async () => { + const obj = { type: HIDDEN_TYPE, id: 'three' }; + await bulkGetErrorInvalidType([obj1, obj, obj2]); }); - }); - it(`accepts a custom refresh setting`, async () => { - callAdminCluster.mockReturnValue({ result: 'deleted' }); - await savedObjectsRepository.delete('globaltype', 'logstash-*', { - refresh: false, + it(`returns error when document is not found`, async () => { + const obj = { type: 'dashboard', id: 'three', found: false }; + const response = getMockMgetResponse([obj1, obj, obj2]); + await bulkGetErrorNotFound([obj1, obj, obj2], undefined, response); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: false, + it(`handles missing ids gracefully`, async () => { + const obj = { type: 'dashboard', id: undefined, found: false }; + const response = getMockMgetResponse([obj1, obj, obj2]); + await bulkGetErrorNotFound([obj1, obj, obj2], undefined, response); }); - }); - }); - describe('#deleteByNamespace', () => { - it('requires namespace to be defined', async () => { - callAdminCluster.mockReturnValue(deleteByQueryResults); - expect(savedObjectsRepository.deleteByNamespace()).rejects.toThrowErrorMatchingSnapshot(); - expect(callAdminCluster).not.toHaveBeenCalled(); + it(`returns error when type is multi-namespace and the document exists, but not in this namespace`, async () => { + const obj = { type: MULTI_NAMESPACE_TYPE, id: 'three' }; + const response = getMockMgetResponse([obj1, obj, obj2]); + response.docs[1].namespaces = ['bar-namespace']; + await bulkGetErrorNotFound([obj1, obj, obj2], { namespace }, response); + }); }); - it('requires namespace to be a string', async () => { - callAdminCluster.mockReturnValue(deleteByQueryResults); - expect( - savedObjectsRepository.deleteByNamespace(['namespace-1', 'namespace-2']) - ).rejects.toThrowErrorMatchingSnapshot(); - expect(callAdminCluster).not.toHaveBeenCalled(); + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(bulkGetSuccess([obj1, obj2])).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + }); }); - it('constructs a deleteByQuery call using all types that are namespace aware', async () => { - callAdminCluster.mockReturnValue(deleteByQueryResults); - const result = await savedObjectsRepository.deleteByNamespace('my-namespace'); - - expect(result).toEqual(deleteByQueryResults); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, typeRegistry, { - namespace: 'my-namespace', - type: ['config', 'baz', 'index-pattern', 'dashboard'], + describe('returns', () => { + const expectSuccessResult = ({ type, id }, doc) => ({ + type, + id, + ...(doc._source.namespaces && { namespaces: doc._source.namespaces }), + ...(doc._source.updated_at && { updated_at: doc._source.updated_at }), + version: encodeHitVersion(doc), + attributes: doc._source[type], + references: doc._source.references || [], + migrationVersion: doc._source.migrationVersion, }); - expect(callAdminCluster).toHaveBeenCalledWith('deleteByQuery', { - body: { conflicts: 'proceed' }, - ignore: [404], - index: ['.kibana-test', 'beats'], - refresh: 'wait_for', + it(`returns early for empty objects argument`, async () => { + const result = await bulkGet([]); + expect(result).toEqual({ saved_objects: [] }); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - }); - - it('defaults to a refresh setting of `wait_for`', async () => { - callAdminCluster.mockReturnValue(deleteByQueryResults); - await savedObjectsRepository.deleteByNamespace('my-namespace'); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: 'wait_for', + it(`formats the ES response`, async () => { + const response = getMockMgetResponse([obj1, obj2]); + callAdminCluster.mockResolvedValue(response); + const result = await bulkGet([obj1, obj2]); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [ + expectSuccessResult(obj1, response.docs[0]), + expectSuccessResult(obj2, response.docs[1]), + ], + }); }); - }); - it('accepts a custom refresh setting', async () => { - callAdminCluster.mockReturnValue(deleteByQueryResults); - await savedObjectsRepository.deleteByNamespace('my-namespace', { refresh: true }); + it(`handles a mix of successful gets and errors`, async () => { + const response = getMockMgetResponse([obj1, obj2]); + callAdminCluster.mockResolvedValue(response); + const obj = { type: 'unknownType', id: 'three' }; + const result = await bulkGet([obj1, obj, obj2]); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [ + expectSuccessResult(obj1, response.docs[0]), + expectError(obj), + expectSuccessResult(obj2, response.docs[1]), + ], + }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: true, + it(`includes namespaces property for multi-namespace documents`, async () => { + const obj = { type: MULTI_NAMESPACE_TYPE, id: 'three' }; + const result = await bulkGetSuccess([obj1, obj]); + expect(result).toEqual({ + saved_objects: [ + expect.not.objectContaining({ namespaces: expect.anything() }), + expect.objectContaining({ namespaces: expect.any(Array) }), + ], + }); }); }); }); - describe('#find', () => { - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); - - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - await expect(savedObjectsRepository.find({ type: 'foo' })).resolves.toBeDefined(); - - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); - - it('requires type to be defined', async () => { - await expect(savedObjectsRepository.find({})).rejects.toThrow(/options\.type must be/); - expect(callAdminCluster).not.toHaveBeenCalled(); + describe('#bulkUpdate', () => { + const obj1 = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + }; + const obj2 = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + }; + const references = [{ name: 'ref_0', type: 'test', id: '1' }]; + const namespace = 'foo-namespace'; + + const getMockBulkUpdateResponse = (objects, options) => ({ + items: objects.map(({ type, id }) => ({ + update: { + _id: `${ + registry.isSingleNamespace(type) && options?.namespace ? `${options?.namespace}:` : '' + }${type}:${id}`, + ...mockVersionProps, + result: 'updated', + }, + })), }); - it('requires searchFields be an array if defined', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - try { - await savedObjectsRepository.find({ type: 'foo', searchFields: 'string' }); - throw new Error('expected find() to reject'); - } catch (error) { - expect(callAdminCluster).not.toHaveBeenCalled(); - expect(error.message).toMatch('must be an array'); + const bulkUpdateSuccess = async (objects, options) => { + const multiNamespaceObjects = objects.filter(({ type }) => registry.isMultiNamespace(type)); + if (multiNamespaceObjects?.length) { + const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace); + callAdminCluster.mockResolvedValueOnce(response); // this._callCluster('mget', ...) } - }); + const response = getMockBulkUpdateResponse(objects, options?.namespace); + callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + const result = await savedObjectsRepository.bulkUpdate(objects, options); + expect(callAdminCluster).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 2 : 1); + return result; + }; - it('requires fields be an array if defined', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - try { - await savedObjectsRepository.find({ type: 'foo', fields: 'string' }); - throw new Error('expected find() to reject'); - } catch (error) { - expect(callAdminCluster).not.toHaveBeenCalled(); - expect(error.message).toMatch('must be an array'); + // bulk create calls have two objects for each source -- the action, and the source + const expectClusterCallArgsAction = ( + objects, + { method, _index = expect.any(String), getId = () => expect.any(String), overrides }, + n + ) => { + const body = []; + for (const { type, id } of objects) { + body.push({ + [method]: { + _index, + _id: getId(type, id), + ...overrides, + }, + }); + body.push(expect.any(Object)); } - }); - - it('passes mappings, schema, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl', async () => { - callAdminCluster.mockReturnValue(namespacedSearchResults); - const relevantOpts = { - namespace: 'foo-namespace', - search: 'foo*', - searchFields: ['foo'], - type: ['bar'], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - kueryNode: undefined, - }; + expectClusterCallArgs({ body }, n); + }; - await savedObjectsRepository.find(relevantOpts); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( - mappings, - typeRegistry, - relevantOpts - ); - }); + const expectObjArgs = ({ type, attributes }) => [ + expect.any(Object), + { + doc: expect.objectContaining({ + [type]: attributes, + ...mockTimestampFields, + }), + }, + ]; - it('accepts KQL filter and passes keuryNode to getSearchDsl', async () => { - callAdminCluster.mockReturnValue(namespacedSearchResults); - const findOpts = { - namespace: 'foo-namespace', - search: 'foo*', - searchFields: ['foo'], - type: ['dashboard'], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - indexPattern: undefined, - filter: 'dashboard.attributes.otherField: *', + describe('cluster calls', () => { + it(`should use the ES bulk action by default`, async () => { + await bulkUpdateSuccess([obj1, obj2]); + expectClusterCalls('bulk'); + }); + + it(`should use the ES mget action before bulk action for any types that are multi-namespace`, async () => { + const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_TYPE }]; + await bulkUpdateSuccess(objects); + expectClusterCalls('mget', 'bulk'); + const docs = [expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj2.id}` })]; + expectClusterCallArgs({ body: { docs } }, 1); + }); + + it(`formats the ES request`, async () => { + await bulkUpdateSuccess([obj1, obj2]); + const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expectClusterCallArgs({ body }); + }); + + it(`formats the ES request for any types that are multi-namespace`, async () => { + const _obj2 = { ...obj2, type: MULTI_NAMESPACE_TYPE }; + await bulkUpdateSuccess([obj1, _obj2]); + const body = [...expectObjArgs(obj1), ...expectObjArgs(_obj2)]; + expectClusterCallArgs({ body }, 2); + }); + + it(`doesnt call Elasticsearch if there are no valid objects to update`, async () => { + const objects = [obj1, obj2].map(x => ({ ...x, type: 'unknownType' })); + await savedObjectsRepository.bulkUpdate(objects); + expect(callAdminCluster).toHaveBeenCalledTimes(0); + }); + + it(`defaults to no references`, async () => { + await bulkUpdateSuccess([obj1, obj2]); + const expected = { doc: expect.not.objectContaining({ references: expect.anything() }) }; + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }); + }); + + it(`accepts custom references array`, async () => { + const test = async references => { + const objects = [obj1, obj2].map(obj => ({ ...obj, references })); + await bulkUpdateSuccess(objects); + const expected = { doc: expect.objectContaining({ references }) }; + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }); + callAdminCluster.mockReset(); + }; + await test(references); + await test(['string']); + await test([]); + }); + + it(`doesn't accept custom references if not an array`, async () => { + const test = async references => { + const objects = [obj1, obj2].map(obj => ({ ...obj, references })); + await bulkUpdateSuccess(objects); + const expected = { doc: expect.not.objectContaining({ references: expect.anything() }) }; + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }); + callAdminCluster.mockReset(); + }; + await test('string'); + await test(123); + await test(true); + await test(null); + }); + + it(`defaults to a refresh setting of wait_for`, async () => { + await bulkUpdateSuccess([obj1, obj2]); + expectClusterCallArgs({ refresh: 'wait_for' }); + }); + + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await bulkUpdateSuccess([obj1, obj2], { refresh }); + expectClusterCallArgs({ refresh }); + }); + + it(`defaults to the version of the existing document for multi-namespace types`, async () => { + // only multi-namespace documents are obtained using a pre-flight mget request + const objects = [ + { ...obj1, type: MULTI_NAMESPACE_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_TYPE }, + ]; + await bulkUpdateSuccess(objects); + const overrides = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClusterCallArgsAction(objects, { method: 'update', overrides }, 2); + }); + + it(`defaults to no version for types that are not multi-namespace`, async () => { + const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkUpdateSuccess(objects); + expectClusterCallArgsAction(objects, { method: 'update' }); + }); + + it(`accepts version`, async () => { + const version = encodeHitVersion({ _seq_no: 100, _primary_term: 200 }); + // test with both non-multi-namespace and multi-namespace types + const objects = [ + { ...obj1, version }, + { ...obj2, type: MULTI_NAMESPACE_TYPE, version }, + ]; + await bulkUpdateSuccess(objects); + const overrides = { if_seq_no: 100, if_primary_term: 200 }; + expectClusterCallArgsAction(objects, { method: 'update', overrides }, 2); + }); + + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + const getId = (type, id) => `${namespace}:${type}:${id}`; + await bulkUpdateSuccess([obj1, obj2], { namespace }); + expectClusterCallArgsAction([obj1, obj2], { method: 'update', getId }); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + await bulkUpdateSuccess([obj1, obj2]); + expectClusterCallArgsAction([obj1, obj2], { method: 'update', getId }); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + const objects1 = [{ ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkUpdateSuccess(objects1, { namespace }); + expectClusterCallArgsAction(objects1, { method: 'update', getId }); + callAdminCluster.mockReset(); + const overrides = { + // bulkUpdate uses a preflight `get` request for multi-namespace saved objects, and specifies that version on `update` + // we aren't testing for this here, but we need to include Jest assertions so this test doesn't fail + if_primary_term: expect.any(Number), + if_seq_no: expect.any(Number), + }; + const objects2 = [{ ...obj2, type: MULTI_NAMESPACE_TYPE }]; + await bulkUpdateSuccess(objects2, { namespace }); + expectClusterCallArgsAction(objects2, { method: 'update', getId, overrides }, 2); + }); + }); + + describe('errors', () => { + const obj = { + type: 'dashboard', + id: 'three', }; - await savedObjectsRepository.find(findOpts); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); - const { kueryNode } = getSearchDslNS.getSearchDsl.mock.calls[0][2]; - expect(kueryNode).toMatchInlineSnapshot(` - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "dashboard.otherField", - }, - Object { - "type": "wildcard", - "value": "@kuery-wildcard@", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", + const bulkUpdateError = async (obj, esError, expectedError) => { + const objects = [obj1, obj, obj2]; + const mockResponse = getMockBulkUpdateResponse(objects); + if (esError) { + mockResponse.items[1].update = { error: esError }; } - `); - }); + callAdminCluster.mockResolvedValue(mockResponse); // this._writeToCluster('bulk', ...) + + const result = await savedObjectsRepository.bulkUpdate(objects); + expectClusterCalls('bulk'); + const objCall = esError ? expectObjArgs(obj) : []; + const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; + expectClusterCallArgs({ body }); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], + }); + }; - it('KQL filter syntax errors rejects with bad request', async () => { - callAdminCluster.mockReturnValue(namespacedSearchResults); - const findOpts = { - namespace: 'foo-namespace', - search: 'foo*', - searchFields: ['foo'], - type: ['dashboard'], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - indexPattern: undefined, - filter: 'dashboard.attributes.otherField:<', + const bulkUpdateMultiError = async ([obj1, _obj, obj2], options, mgetResponse) => { + callAdminCluster.mockResolvedValueOnce(mgetResponse); // this._callCluster('mget', ...) + const bulkResponse = getMockBulkUpdateResponse([obj1, obj2], namespace); + callAdminCluster.mockResolvedValue(bulkResponse); // this._writeToCluster('bulk', ...) + + const result = await savedObjectsRepository.bulkUpdate([obj1, _obj, obj2], options); + expectClusterCalls('mget', 'bulk'); + const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expectClusterCallArgs({ body }, 2); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectErrorNotFound(_obj), expectSuccess(obj2)], + }); }; - await expect(savedObjectsRepository.find(findOpts)).rejects.toMatchInlineSnapshot(` - [Error: KQLSyntaxError: Expected "(", "{", value, whitespace but "<" found. - dashboard.attributes.otherField:< - --------------------------------^: Bad Request] - `); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(0); - }); + it(`returns error when type is invalid`, async () => { + const _obj = { ...obj, type: 'unknownType' }; + await bulkUpdateError(_obj, undefined, expectErrorNotFound(_obj)); + }); - it('merges output of getSearchDsl into es request body', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - getSearchDslNS.getSearchDsl.mockReturnValue({ query: 1, aggregations: 2 }); - await savedObjectsRepository.find({ type: 'foo' }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'search', - expect.objectContaining({ - body: expect.objectContaining({ - query: 1, - aggregations: 2, - }), - }) - ); - }); + it(`returns error when type is hidden`, async () => { + const _obj = { ...obj, type: HIDDEN_TYPE }; + await bulkUpdateError(_obj, undefined, expectErrorNotFound(_obj)); + }); - it('formats Elasticsearch response when there is no namespace', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - const count = noNamespaceSearchResults.hits.hits.length; + it(`returns error when ES is unable to find the document (mget)`, async () => { + const _obj = { ...obj, type: MULTI_NAMESPACE_TYPE, found: false }; + const mgetResponse = getMockMgetResponse([_obj]); + await bulkUpdateMultiError([obj1, _obj, obj2], undefined, mgetResponse); + }); - const response = await savedObjectsRepository.find({ type: 'foo' }); + it(`returns error when ES is unable to find the index (mget)`, async () => { + const _obj = { ...obj, type: MULTI_NAMESPACE_TYPE }; + const mgetResponse = { status: 404 }; + await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, mgetResponse); + }); - expect(response.total).toBe(count); - expect(response.saved_objects).toHaveLength(count); + it(`returns error when there is a conflict with an existing multi-namespace saved object (mget)`, async () => { + const _obj = { ...obj, type: MULTI_NAMESPACE_TYPE }; + const mgetResponse = getMockMgetResponse([_obj], 'bar-namespace'); + await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, mgetResponse); + }); - noNamespaceSearchResults.hits.hits.forEach((doc, i) => { - expect(response.saved_objects[i]).toEqual({ - id: doc._id.replace(/(index-pattern|config|globaltype)\:/, ''), - type: doc._source.type, - ...mockTimestampFields, - version: mockVersion, - attributes: doc._source[doc._source.type], - references: [], - }); + it(`returns error when there is a version conflict (bulk)`, async () => { + const esError = { type: 'version_conflict_engine_exception' }; + await bulkUpdateError(obj, esError, expectErrorConflict(obj)); }); - }); - it('formats Elasticsearch response when there is a namespace', async () => { - callAdminCluster.mockReturnValue(namespacedSearchResults); - const count = namespacedSearchResults.hits.hits.length; + it(`returns error when document is missing (bulk)`, async () => { + const esError = { type: 'document_missing_exception' }; + await bulkUpdateError(obj, esError, expectErrorNotFound(obj)); + }); - const response = await savedObjectsRepository.find({ - type: 'foo', - namespace: 'foo-namespace', + it(`returns error reason for other errors (bulk)`, async () => { + const esError = { reason: 'some_other_error' }; + await bulkUpdateError(obj, esError, expectErrorResult(obj, { message: esError.reason })); }); - expect(response.total).toBe(count); - expect(response.saved_objects).toHaveLength(count); + it(`returns error string for other errors if no reason is defined (bulk)`, async () => { + const esError = { foo: 'some_other_error' }; + const expectedError = expectErrorResult(obj, { message: JSON.stringify(esError) }); + await bulkUpdateError(obj, esError, expectedError); + }); + }); - namespacedSearchResults.hits.hits.forEach((doc, i) => { - expect(response.saved_objects[i]).toEqual({ - id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|globaltype)\:/, ''), - type: doc._source.type, - ...mockTimestampFields, - version: mockVersion, - attributes: doc._source[doc._source.type], - references: [], - }); + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(bulkUpdateSuccess([obj1, obj2])).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveReturnedTimes(1); }); }); - it('accepts per_page/page', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - await savedObjectsRepository.find({ type: 'foo', perPage: 10, page: 6 }); + describe('returns', () => { + const expectSuccessResult = ({ type, id, attributes, references }) => ({ + type, + id, + attributes, + references, + version: mockVersion, + ...mockTimestampFields, + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - size: 10, - from: 50, - }) - ); - }); + it(`formats the ES response`, async () => { + const response = await bulkUpdateSuccess([obj1, obj2]); + expect(response).toEqual({ + saved_objects: [obj1, obj2].map(expectSuccessResult), + }); + }); - it('can filter by fields', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - await savedObjectsRepository.find({ type: 'foo', fields: ['title'] }); + it(`includes references`, async () => { + const objects = [obj1, obj2].map(obj => ({ ...obj, references })); + const response = await bulkUpdateSuccess(objects); + expect(response).toEqual({ + saved_objects: objects.map(expectSuccessResult), + }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - _source: [ - 'foo.title', - 'namespace', - 'type', - 'references', - 'migrationVersion', - 'updated_at', - 'title', + it(`handles a mix of successful updates and errors`, async () => { + const obj = { + type: 'unknownType', + id: 'three', + }; + const objects = [obj1, obj, obj2]; + const mockResponse = getMockBulkUpdateResponse(objects); + callAdminCluster.mockResolvedValue(mockResponse); // this._writeToCluster('bulk', ...) + const result = await savedObjectsRepository.bulkUpdate(objects); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [expectSuccessResult(obj1), expectError(obj), expectSuccessResult(obj2)], + }); + }); + + it(`includes namespaces property for multi-namespace documents`, async () => { + const obj = { type: MULTI_NAMESPACE_TYPE, id: 'three' }; + const result = await bulkUpdateSuccess([obj1, obj]); + expect(result).toEqual({ + saved_objects: [ + expect.not.objectContaining({ namespaces: expect.anything() }), + expect.objectContaining({ namespaces: expect.any(Array) }), ], - }) - ); + }); + }); }); + }); - it('should set rest_total_hits_as_int to true on a request', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - await savedObjectsRepository.find({ type: 'foo' }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toHaveProperty('rest_total_hits_as_int', true); + describe('#create', () => { + beforeEach(() => { + callAdminCluster.mockImplementation((method, params) => ({ + _id: params.id, + ...mockVersionProps, + })); }); - }); - describe('#get', () => { - const noNamespaceResult = { - _id: 'index-pattern:logstash-*', - ...mockVersionProps, - _source: { - type: 'index-pattern', - specialProperty: 'specialValue', - ...mockTimestampFields, - 'index-pattern': { - title: 'Testing', - }, - }, - }; - const namespacedResult = { - _id: 'foo-namespace:index-pattern:logstash-*', - ...mockVersionProps, - _source: { - namespace: 'foo-namespace', - type: 'index-pattern', - specialProperty: 'specialValue', - ...mockTimestampFields, - 'index-pattern': { - title: 'Testing', - }, + const type = 'index-pattern'; + const attributes = { title: 'Logstash' }; + const id = 'logstash-*'; + const namespace = 'foo-namespace'; + const references = [ + { + name: 'ref_0', + type: 'test', + id: '123', }, - }; + ]; - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); + const createSuccess = async (type, attributes, options) => { + const result = await savedObjectsRepository.create(type, attributes, options); + expect(callAdminCluster).toHaveBeenCalledTimes( + registry.isMultiNamespace(type) && options.overwrite ? 2 : 1 + ); + return result; + }; - callAdminCluster.mockResolvedValue(noNamespaceResult); - await expect( - savedObjectsRepository.get('index-pattern', 'logstash-*') - ).resolves.toBeDefined(); + describe('cluster calls', () => { + it(`should use the ES create action if ID is undefined and overwrite=true`, async () => { + await createSuccess(type, attributes, { overwrite: true }); + expectClusterCalls('create'); + }); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); + it(`should use the ES create action if ID is undefined and overwrite=false`, async () => { + await createSuccess(type, attributes); + expectClusterCalls('create'); + }); - it('formats Elasticsearch response when there is no namespace', async () => { - callAdminCluster.mockResolvedValue(noNamespaceResult); - const response = await savedObjectsRepository.get('index-pattern', 'logstash-*'); - expect(response).toEqual({ - id: 'logstash-*', - type: 'index-pattern', - updated_at: mockTimestamp, - version: mockVersion, - attributes: { - title: 'Testing', - }, - references: [], + it(`should use the ES index action if ID is defined and overwrite=true`, async () => { + await createSuccess(type, attributes, { id, overwrite: true }); + expectClusterCalls('index'); }); - }); - it('formats Elasticsearch response when there are namespaces', async () => { - callAdminCluster.mockResolvedValue(namespacedResult); - const response = await savedObjectsRepository.get('index-pattern', 'logstash-*'); - expect(response).toEqual({ - id: 'logstash-*', - type: 'index-pattern', - updated_at: mockTimestamp, - version: mockVersion, - attributes: { - title: 'Testing', - }, - references: [], + it(`should use the ES create action if ID is defined and overwrite=false`, async () => { + await createSuccess(type, attributes, { id }); + expectClusterCalls('create'); }); - }); - it('prepends namespace and type to the id when providing namespace for namespaced type', async () => { - callAdminCluster.mockResolvedValue(namespacedResult); - await savedObjectsRepository.get('index-pattern', 'logstash-*', { - namespace: 'foo-namespace', + it(`should use the ES get action then index action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { + await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id, overwrite: true }); + expectClusterCalls('get', 'index'); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: 'foo-namespace:index-pattern:logstash-*', - }) - ); - }); + it(`defaults to empty references array`, async () => { + await createSuccess(type, attributes, { id }); + expectClusterCallArgs({ + body: expect.objectContaining({ references: [] }), + }); + }); - it(`only prepends type to the id when providing no namespace for namespaced type`, async () => { - callAdminCluster.mockResolvedValue(noNamespaceResult); - await savedObjectsRepository.get('index-pattern', 'logstash-*'); + it(`accepts custom references array`, async () => { + const test = async references => { + await createSuccess(type, attributes, { id, references }); + expectClusterCallArgs({ + body: expect.objectContaining({ references }), + }); + callAdminCluster.mockReset(); + }; + await test(references); + await test(['string']); + await test([]); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: 'index-pattern:logstash-*', - }) - ); - }); + it(`doesn't accept custom references if not an array`, async () => { + const test = async references => { + await createSuccess(type, attributes, { id, references }); + expectClusterCallArgs({ + body: expect.not.objectContaining({ references: expect.anything() }), + }); + callAdminCluster.mockReset(); + }; + await test('string'); + await test(123); + await test(true); + await test(null); + }); - it(`doesn't prepend namespace to the id when providing namespace for namespace agnostic type`, async () => { - callAdminCluster.mockResolvedValue(namespacedResult); - await savedObjectsRepository.get('globaltype', 'logstash-*', { - namespace: 'foo-namespace', + it(`defaults to a refresh setting of wait_for`, async () => { + await createSuccess(type, attributes); + expectClusterCallArgs({ refresh: 'wait_for' }); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: 'globaltype:logstash-*', - }) - ); - }); - }); + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await createSuccess(type, attributes, { refresh }); + expectClusterCallArgs({ refresh }); + }); - describe('#bulkGet', () => { - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); - - callAdminCluster.mockReturnValue({ docs: [] }); - await expect( - savedObjectsRepository.bulkGet([ - { id: 'one', type: 'config' }, - { id: 'two', type: 'index-pattern' }, - { id: 'three', type: 'globaltype' }, - ]) - ).resolves.toBeDefined(); - - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); + it(`should use default index`, async () => { + await createSuccess(type, attributes, { id }); + expectClusterCallArgs({ index: '.kibana-test' }); + }); - it('prepends type to id when getting objects when there is no namespace', async () => { - callAdminCluster.mockReturnValue({ docs: [] }); + it(`should use custom index`, async () => { + await createSuccess(CUSTOM_INDEX_TYPE, attributes, { id }); + expectClusterCallArgs({ index: 'custom' }); + }); - await savedObjectsRepository.bulkGet([ - { id: 'one', type: 'config' }, - { id: 'two', type: 'index-pattern' }, - { id: 'three', type: 'globaltype' }, - ]); + it(`self-generates an id if none is provided`, async () => { + await createSuccess(type, attributes); + expectClusterCallArgs({ + id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/), + }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: { - docs: [ - { _id: 'config:one', _index: '.kibana-test' }, - { _id: 'index-pattern:two', _index: '.kibana-test' }, - { _id: 'globaltype:three', _index: '.kibana-test' }, - ], - }, - }) - ); - }); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await createSuccess(type, attributes, { id, namespace }); + expectClusterCallArgs({ id: `${namespace}:${type}:${id}` }); + }); - it('prepends namespace and type appropriately to id when getting objects when there is a namespace', async () => { - callAdminCluster.mockReturnValue({ docs: [] }); + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await createSuccess(type, attributes, { id }); + expectClusterCallArgs({ id: `${type}:${id}` }); + }); - await savedObjectsRepository.bulkGet( - [ - { id: 'one', type: 'config' }, - { id: 'two', type: 'index-pattern' }, - { id: 'three', type: 'globaltype' }, - ], - { - namespace: 'foo-namespace', - } - ); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); + expectClusterCallArgs({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }); + callAdminCluster.mockReset(); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: { - docs: [ - { _id: 'foo-namespace:config:one', _index: '.kibana-test' }, - { _id: 'foo-namespace:index-pattern:two', _index: '.kibana-test' }, - { _id: 'globaltype:three', _index: '.kibana-test' }, - ], - }, - }) - ); + await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id, namespace }); + expectClusterCallArgs({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }); + }); }); - it('mockReturnValue early for empty objects argument', async () => { - callAdminCluster.mockReturnValue({ docs: [] }); + describe('errors', () => { + it(`throws when type is invalid`, async () => { + await expect(savedObjectsRepository.create('unknownType', attributes)).rejects.toThrowError( + createUnsupportedTypeError('unknownType') + ); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - const response = await savedObjectsRepository.bulkGet([]); + it(`throws when type is hidden`, async () => { + await expect(savedObjectsRepository.create(HIDDEN_TYPE, attributes)).rejects.toThrowError( + createUnsupportedTypeError(HIDDEN_TYPE) + ); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - expect(response.saved_objects).toHaveLength(0); - expect(callAdminCluster).not.toHaveBeenCalled(); + it(`throws when there is a conflict with an existing multi-namespace saved object (get)`, async () => { + const response = getMockGetResponse({ + type: MULTI_NAMESPACE_TYPE, + id, + namespace: 'bar-namespace', + }); + callAdminCluster.mockResolvedValue(response); // this._callCluster('get', ...) + await expect( + savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { + id, + overwrite: true, + namespace, + }) + ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_TYPE, id)); + expectClusterCalls('get'); + }); + + it(`throws when automatic index creation fails`, async () => { + // TODO + }); + + it(`throws when an unexpected failure occurs`, async () => { + // TODO + }); }); - it('handles missing ids gracefully', async () => { - callAdminCluster.mockResolvedValue({ - docs: [ - { - _id: 'config:good', - found: true, - ...mockVersionProps, - _source: { ...mockTimestampFields, config: { title: 'Test' } }, - }, - { - _id: 'config:bad', - found: false, - }, - ], + describe('migration', () => { + beforeEach(() => { + migrator.migrateDocument.mockImplementation(mockMigrateDocument); }); - const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet([ - { id: 'good', type: 'config' }, - { type: 'config' }, - ]); + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(createSuccess(type, attributes, { id, namespace })).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + }); - expect(savedObjects[1]).toEqual({ - type: 'config', - error: { statusCode: 404, message: 'Not found' }, + it(`migrates a document and serializes the migrated doc`, async () => { + const migrationVersion = mockMigrationVersion; + await createSuccess(type, attributes, { id, references, migrationVersion }); + const doc = { type, id, attributes, references, migrationVersion, ...mockTimestampFields }; + expectMigrationArgs(doc); + + const migratedDoc = migrator.migrateDocument(doc); + expect(serializer.savedObjectToRaw).toHaveBeenLastCalledWith(migratedDoc); }); - }); - it('reports error on missed objects', async () => { - callAdminCluster.mockResolvedValue({ - docs: [ - { - _id: 'config:good', - found: true, - ...mockVersionProps, - _source: { ...mockTimestampFields, config: { title: 'Test' } }, - }, - { - _id: 'config:bad', - found: false, - }, - ], + it(`adds namespace to body when providing namespace for single-namespace type`, async () => { + await createSuccess(type, attributes, { id, namespace }); + expectMigrationArgs({ namespace }); }); - const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet([ - { id: 'good', type: 'config' }, - { id: 'bad', type: 'config' }, - ]); + it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => { + await createSuccess(type, attributes, { id }); + expectMigrationArgs({ namespace: expect.anything() }, false); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`doesn't add namespace to body when not using single-namespace type`, async () => { + await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); + expectMigrationArgs({ namespace: expect.anything() }, false, 1); - expect(savedObjects).toHaveLength(2); - expect(savedObjects[0]).toEqual({ - id: 'good', - type: 'config', - ...mockTimestampFields, - version: mockVersion, - attributes: { title: 'Test' }, - references: [], + callAdminCluster.mockReset(); + await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id }); + expectMigrationArgs({ namespace: expect.anything() }, false, 2); }); - expect(savedObjects[1]).toEqual({ - id: 'bad', - type: 'config', - error: { statusCode: 404, message: 'Not found' }, + + it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => { + await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id, namespace }); + expectMigrationArgs({ namespaces: [namespace] }); }); - }); - it('returns errors when requesting unsupported types', async () => { - callAdminCluster.mockResolvedValue({ - docs: [ - { - _id: 'one', - found: true, - ...mockVersionProps, - _source: { ...mockTimestampFields, config: { title: 'Test1' } }, - }, - { - _id: 'three', - found: true, - ...mockVersionProps, - _source: { ...mockTimestampFields, config: { title: 'Test3' } }, - }, - { - _id: 'five', - found: true, - ...mockVersionProps, - _source: { ...mockTimestampFields, config: { title: 'Test5' } }, - }, - ], + it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => { + await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id }); + expectMigrationArgs({ namespaces: ['default'] }); }); - const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet([ - { id: 'one', type: 'config' }, - { id: 'two', type: 'invalidtype' }, - { id: 'three', type: 'config' }, - { id: 'four', type: 'invalidtype' }, - { id: 'five', type: 'config' }, - ]); + it(`doesn't add namespaces to body when not using multi-namespace type`, async () => { + await createSuccess(type, attributes, { id }); + expectMigrationArgs({ namespaces: expect.anything() }, false, 1); - expect(savedObjects).toEqual([ - { - attributes: { title: 'Test1' }, - id: 'one', - ...mockTimestampFields, - references: [], - type: 'config', - version: mockVersion, - migrationVersion: undefined, - }, - { - attributes: { title: 'Test3' }, - id: 'three', - ...mockTimestampFields, - references: [], - type: 'config', - version: mockVersion, - migrationVersion: undefined, - }, - { - attributes: { title: 'Test5' }, - id: 'five', + callAdminCluster.mockReset(); + await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id }); + expectMigrationArgs({ namespaces: expect.anything() }, false, 2); + }); + }); + + describe('returns', () => { + it(`formats the ES response`, async () => { + const result = await createSuccess(type, attributes, { id, namespace, references }); + expect(result).toEqual({ + type, + id, ...mockTimestampFields, - references: [], - type: 'config', version: mockVersion, - migrationVersion: undefined, - }, - { - error: { - error: 'Bad Request', - message: "Unsupported saved object type: 'invalidtype': Bad Request", - statusCode: 400, - }, - id: 'two', - type: 'invalidtype', - }, - { - error: { - error: 'Bad Request', - message: "Unsupported saved object type: 'invalidtype': Bad Request", - statusCode: 400, - }, - id: 'four', - type: 'invalidtype', - }, - ]); + attributes, + references, + }); + }); }); }); - describe('#update', () => { - const id = 'logstash-*'; + describe('#delete', () => { const type = 'index-pattern'; - const attributes = { title: 'Testing' }; + const id = 'logstash-*'; + const namespace = 'foo-namespace'; - beforeEach(() => { - callAdminCluster.mockResolvedValue({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', + const deleteSuccess = async (type, id, options) => { + if (registry.isMultiNamespace(type)) { + const mockGetResponse = getMockGetResponse({ type, id, namespace: options?.namespace }); + callAdminCluster.mockResolvedValueOnce(mockGetResponse); // this._callCluster('get', ...) + } + callAdminCluster.mockResolvedValue({ result: 'deleted' }); // this._writeToCluster('delete', ...) + const result = await savedObjectsRepository.delete(type, id, options); + expect(callAdminCluster).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 2 : 1); + return result; + }; + + describe('cluster calls', () => { + it(`should use the ES delete action when not using a multi-namespace type`, async () => { + await deleteSuccess(type, id); + expectClusterCalls('delete'); }); - }); - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); + it(`should use ES get action then delete action when using a multi-namespace type with no namespaces remaining`, async () => { + await deleteSuccess(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get', 'delete'); + }); - await expect( - savedObjectsRepository.update('index-pattern', 'logstash-*', attributes, { - namespace: 'foo-namespace', - }) - ).resolves.toBeDefined(); + it(`should use ES get action then update action when using a multi-namespace type with one or more namespaces remaining`, async () => { + const mockResponse = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }); + mockResponse._source.namespaces = ['default', 'some-other-nameespace']; + callAdminCluster + .mockResolvedValueOnce(mockResponse) // this._callCluster('get', ...) + .mockResolvedValue({ result: 'updated' }); // this._writeToCluster('update', ...) + await savedObjectsRepository.delete(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get', 'update'); + }); - expect(migrator.runMigrations).toHaveReturnedTimes(1); - }); + it(`includes the version of the existing document when type is multi-namespace`, async () => { + await deleteSuccess(MULTI_NAMESPACE_TYPE, id); + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClusterCallArgs(versionProperties, 2); + }); - it('mockReturnValue current ES document _seq_no and _primary_term encoded as version', async () => { - const response = await savedObjectsRepository.update( - 'index-pattern', - 'logstash-*', - attributes, - { - namespace: 'foo-namespace', - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - } - ); - expect(response).toEqual({ - id, - type, - ...mockTimestampFields, - version: mockVersion, - attributes, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], + it(`defaults to a refresh setting of wait_for`, async () => { + await deleteSuccess(type, id); + expectClusterCallArgs({ refresh: 'wait_for' }); }); - }); - it('accepts version', async () => { - await savedObjectsRepository.update( - type, - id, - { title: 'Testing' }, - { - version: encodeHitVersion({ - _seq_no: 100, - _primary_term: 200, - }), - } - ); + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await deleteSuccess(type, id, { refresh }); + expectClusterCallArgs({ refresh }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - if_seq_no: 100, - if_primary_term: 200, - }) - ); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await deleteSuccess(type, id, { namespace }); + expectClusterCallArgs({ id: `${namespace}:${type}:${id}` }); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await deleteSuccess(type, id); + expectClusterCallArgs({ id: `${type}:${id}` }); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await deleteSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); + expectClusterCallArgs({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }); + + callAdminCluster.mockReset(); + await deleteSuccess(MULTI_NAMESPACE_TYPE, id, { namespace }); + expectClusterCallArgs({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }); + }); }); - it('does not pass references if omitted', async () => { - await savedObjectsRepository.update(type, id, { title: 'Testing' }); + describe('errors', () => { + const expectNotFoundError = async (type, id, options) => { + await expect(savedObjectsRepository.delete(type, id, options)).rejects.toThrowError( + createGenericNotFoundError(type, id) + ); + }; + + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); + + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); + + it(`throws when ES is unable to find the document during get`, async () => { + callAdminCluster.mockResolvedValue({ found: false }); // this._callCluster('get', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get'); + }); + + it(`throws when ES is unable to find the index during get`, async () => { + callAdminCluster.mockResolvedValue({ status: 404 }); // this._callCluster('get', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get'); + }); + + it(`throws when the type is multi-namespace and the document exists, but not in this namespace`, async () => { + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); + callAdminCluster.mockResolvedValue(response); // this._callCluster('get', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id, { namespace: 'bar-namespace' }); + expectClusterCalls('get'); + }); + + it(`throws when ES is unable to find the document during update`, async () => { + const mockResponse = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }); + mockResponse._source.namespaces = ['default', 'some-other-nameespace']; + callAdminCluster + .mockResolvedValueOnce(mockResponse) // this._callCluster('get', ...) + .mockResolvedValue({ status: 404 }); // this._writeToCluster('update', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get', 'update'); + }); + + it(`throws when ES is unable to find the document during delete`, async () => { + callAdminCluster.mockResolvedValue({ result: 'not_found' }); // this._writeToCluster('delete', ...) + await expectNotFoundError(type, id); + expectClusterCalls('delete'); + }); + + it(`throws when ES is unable to find the index during delete`, async () => { + callAdminCluster.mockResolvedValue({ error: { type: 'index_not_found_exception' } }); // this._writeToCluster('delete', ...) + await expectNotFoundError(type, id); + expectClusterCalls('delete'); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).not.toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: { - doc: expect.objectContaining({ - references: [], - }), - }, - }) - ); + it(`throws when ES returns an unexpected response`, async () => { + callAdminCluster.mockResolvedValue({ result: 'something unexpected' }); // this._writeToCluster('delete', ...) + await expect(savedObjectsRepository.delete(type, id)).rejects.toThrowError( + 'Unexpected Elasticsearch DELETE response' + ); + expectClusterCalls('delete'); + }); }); - it('passes references if they are provided', async () => { - await savedObjectsRepository.update(type, id, { title: 'Testing' }, { references: ['foo'] }); + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + let callAdminClusterCount = 0; + migrator.runMigrations = jest.fn(async () => + // runMigrations should resolve before callAdminCluster is initiated + expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) + ); + await expect(deleteSuccess(type, id)).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: { - doc: expect.objectContaining({ - references: ['foo'], - }), - }, - }) - ); + describe('returns', () => { + it(`returns an empty object on success`, async () => { + const result = await deleteSuccess(type, id); + expect(result).toEqual({}); + }); }); + }); - it('passes empty references array if empty references array is provided', async () => { - await savedObjectsRepository.update(type, id, { title: 'Testing' }, { references: [] }); + describe('#deleteByNamespace', () => { + const namespace = 'foo-namespace'; + const mockUpdateResults = { + took: 15, + timed_out: false, + total: 3, + updated: 2, + deleted: 1, + batches: 1, + version_conflicts: 0, + noops: 0, + retries: { bulk: 0, search: 0 }, + throttled_millis: 0, + requests_per_second: -1.0, + throttled_until_millis: 0, + failures: [], + }; + const deleteByNamespaceSuccess = async (namespace, options) => { + callAdminCluster.mockResolvedValue(mockUpdateResults); + const result = await savedObjectsRepository.deleteByNamespace(namespace, options); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: { - doc: expect.objectContaining({ - references: [], - }), - }, - }) - ); - }); + return result; + }; - it(`prepends namespace to the id but doesn't add namespace to body when providing namespace for namespaced type`, async () => { - await savedObjectsRepository.update( - 'index-pattern', - 'logstash-*', - { - title: 'Testing', - }, - { - namespace: 'foo-namespace', - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - } - ); + describe('cluster calls', () => { + it(`should use the ES updateByQuery action`, async () => { + await deleteByNamespaceSuccess(namespace); + expectClusterCalls('updateByQuery'); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('update', { - id: 'foo-namespace:index-pattern:logstash-*', - body: { - doc: { - updated_at: mockTimestamp, - 'index-pattern': { title: 'Testing' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - }, - }, - ignore: [404], - refresh: 'wait_for', - index: '.kibana-test', + it(`defaults to a refresh setting of wait_for`, async () => { + await deleteByNamespaceSuccess(namespace); + expectClusterCallArgs({ refresh: 'wait_for' }); }); - }); - it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { - await savedObjectsRepository.update( - 'index-pattern', - 'logstash-*', - { - title: 'Testing', - }, - { - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - } - ); + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await deleteByNamespaceSuccess(namespace, { refresh }); + expectClusterCallArgs({ refresh }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('update', { - id: 'index-pattern:logstash-*', - body: { - doc: { - updated_at: mockTimestamp, - 'index-pattern': { title: 'Testing' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - }, - }, - ignore: [404], - refresh: 'wait_for', - index: '.kibana-test', + it(`should use all indices for types that are not namespace-agnostic`, async () => { + await deleteByNamespaceSuccess(namespace); + expectClusterCallArgs({ index: ['.kibana-test', 'custom'] }, 1); }); }); - it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { - await savedObjectsRepository.update( - 'globaltype', - 'foo', - { - name: 'bar', - }, - { - namespace: 'foo-namespace', - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - } - ); - - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('update', { - id: 'globaltype:foo', - body: { - doc: { - updated_at: mockTimestamp, - globaltype: { name: 'bar' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - }, - }, - ignore: [404], - refresh: 'wait_for', - index: '.kibana-test', + describe('errors', () => { + it(`throws when namespace is not a string`, async () => { + const test = async namespace => { + await expect(savedObjectsRepository.deleteByNamespace(namespace)).rejects.toThrowError( + `namespace is required, and must be a string` + ); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; + await test(undefined); + await test(['namespace']); + await test(123); + await test(true); }); }); - it('defaults to a refresh setting of `wait_for`', async () => { - await savedObjectsRepository.update('globaltype', 'foo', { - name: 'bar', + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(deleteByNamespaceSuccess(namespace)).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: 'wait_for', + describe('returns', () => { + it(`returns the query results on success`, async () => { + const result = await deleteByNamespaceSuccess(namespace); + expect(result).toEqual(mockUpdateResults); }); }); - it('accepts a custom refresh setting', async () => { - await savedObjectsRepository.update( - 'globaltype', - 'foo', - { - name: 'bar', - }, - { - refresh: true, - namespace: 'foo-namespace', - } - ); - - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: true, + describe('search dsl', () => { + it(`constructs a query using all multi-namespace types, and another using all single-namespace types`, async () => { + await deleteByNamespaceSuccess(namespace); + const allTypes = registry.getAllTypes().map(type => type.name); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, { + namespace, + type: allTypes.filter(type => !registry.isNamespaceAgnostic(type)), + }); }); }); }); - describe('#bulkUpdate', () => { - const { generateSavedObject, reset } = (() => { - let count = 0; + describe('#find', () => { + const generateSearchResults = namespace => { return { - generateSavedObject(overrides) { - count++; - return _.merge( + hits: { + total: 4, + hits: [ { - type: 'index-pattern', - id: `logstash-${count}`, - attributes: { title: `Testing ${count}` }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', + _index: '.kibana', + _id: `${namespace ? `${namespace}:` : ''}index-pattern:logstash-*`, + _score: 1, + ...mockVersionProps, + _source: { + namespace, + type: 'index-pattern', + ...mockTimestampFields, + 'index-pattern': { + title: 'logstash-*', + timeFieldName: '@timestamp', + notExpandable: true, }, - ], + }, }, - overrides - ); - }, - reset() { - count = 0; + { + _index: '.kibana', + _id: `${namespace ? `${namespace}:` : ''}config:6.0.0-alpha1`, + _score: 1, + ...mockVersionProps, + _source: { + namespace, + type: 'config', + ...mockTimestampFields, + config: { + buildNum: 8467, + defaultIndex: 'logstash-*', + }, + }, + }, + { + _index: '.kibana', + _id: `${namespace ? `${namespace}:` : ''}index-pattern:stocks-*`, + _score: 1, + ...mockVersionProps, + _source: { + namespace, + type: 'index-pattern', + ...mockTimestampFields, + 'index-pattern': { + title: 'stocks-*', + timeFieldName: '@timestamp', + notExpandable: true, + }, + }, + }, + { + _index: '.kibana', + _id: `${NAMESPACE_AGNOSTIC_TYPE}:something`, + _score: 1, + ...mockVersionProps, + _source: { + type: NAMESPACE_AGNOSTIC_TYPE, + ...mockTimestampFields, + [NAMESPACE_AGNOSTIC_TYPE]: { + name: 'bar', + }, + }, + }, + ], }, }; - })(); + }; - beforeEach(() => { - reset(); - }); + const type = 'index-pattern'; + const namespace = 'foo-namespace'; - const mockValidResponse = objects => - callAdminCluster.mockReturnValue({ - items: objects.map(items => ({ - update: { - _id: `${items.type}:${items.id}`, - ...mockVersionProps, - result: 'updated', - }, - })), + const findSuccess = async (options, namespace) => { + callAdminCluster.mockResolvedValue(generateSearchResults(namespace)); + const result = await savedObjectsRepository.find(options); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + return result; + }; + + describe('cluster calls', () => { + it(`should use the ES search action`, async () => { + await findSuccess({ type }); + expectClusterCalls('search'); + }); + + it(`merges output of getSearchDsl into es request body`, async () => { + const query = { query: 1, aggregations: 2 }; + getSearchDslNS.getSearchDsl.mockReturnValue(query); + await findSuccess({ type }); + expectClusterCallArgs({ body: expect.objectContaining({ ...query }) }); }); - it('waits until migrations are complete before proceeding', async () => { - const objects = [generateSavedObject(), generateSavedObject()]; + it(`accepts per_page/page`, async () => { + await findSuccess({ type, perPage: 10, page: 6 }); + expectClusterCallArgs({ + size: 10, + from: 50, + }); + }); - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); + it(`can filter by fields`, async () => { + await findSuccess({ type, fields: ['title'] }); + expectClusterCallArgs({ + _source: [ + `${type}.title`, + 'namespace', + 'namespaces', + 'type', + 'references', + 'migrationVersion', + 'updated_at', + 'title', + ], + }); + }); - mockValidResponse(objects); + it(`should set rest_total_hits_as_int to true on a request`, async () => { + await findSuccess({ type }); + expectClusterCallArgs({ rest_total_hits_as_int: true }); + }); - await expect( - savedObjectsRepository.bulkUpdate([generateSavedObject()]) - ).resolves.toBeDefined(); + it(`should not make a cluster call when attempting to find only invalid or hidden types`, async () => { + const test = async types => { + await savedObjectsRepository.find({ type: types }); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; - expect(migrator.runMigrations).toHaveReturnedTimes(1); + await test('unknownType'); + await test(HIDDEN_TYPE); + await test(['unknownType', HIDDEN_TYPE]); + }); }); - it('returns current ES document, _seq_no and _primary_term encoded as version', async () => { - const objects = [generateSavedObject(), generateSavedObject()]; - - mockValidResponse(objects); + describe('errors', () => { + it(`throws when type is not defined`, async () => { + await expect(savedObjectsRepository.find({})).rejects.toThrowError( + 'options.type must be a string or an array of strings' + ); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - const response = await savedObjectsRepository.bulkUpdate(objects); + it(`throws when searchFields is defined but not an array`, async () => { + await expect( + savedObjectsRepository.find({ type, searchFields: 'string' }) + ).rejects.toThrowError('options.searchFields must be an array'); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - expect(response.saved_objects[0]).toMatchObject({ - ..._.pick(objects[0], 'id', 'type', 'attributes'), - version: mockVersion, - references: objects[0].references, + it(`throws when fields is defined but not an array`, async () => { + await expect(savedObjectsRepository.find({ type, fields: 'string' })).rejects.toThrowError( + 'options.fields must be an array' + ); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - expect(response.saved_objects[1]).toMatchObject({ - ..._.pick(objects[1], 'id', 'type', 'attributes'), - version: mockVersion, - references: objects[1].references, + + it(`throws when KQL filter syntax is invalid`, async () => { + const findOpts = { + namespace, + search: 'foo*', + searchFields: ['foo'], + type: ['dashboard'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + indexPattern: undefined, + filter: 'dashboard.attributes.otherField:<', + }; + + await expect(savedObjectsRepository.find(findOpts)).rejects.toMatchInlineSnapshot(` + [Error: KQLSyntaxError: Expected "(", "{", value, whitespace but "<" found. + dashboard.attributes.otherField:< + --------------------------------^: Bad Request] + `); + expect(getSearchDslNS.getSearchDsl).not.toHaveBeenCalled(); + expect(callAdminCluster).not.toHaveBeenCalled(); }); }); - it('handles a mix of succesfull updates and errors', async () => { - const objects = [ - generateSavedObject(), - { - type: 'invalid-type', - id: 'invalid', - attributes: { title: 'invalid' }, - }, - generateSavedObject(), - generateSavedObject({ - id: 'version_clash', - }), - ]; - - callAdminCluster.mockReturnValue({ - items: objects - // remove invalid from mocks - .filter(item => item.id !== 'invalid') - .map(items => { - switch (items.id) { - case 'version_clash': - return { - update: { - _id: `${items.type}:${items.id}`, - error: { - type: 'version_conflict_engine_exception', - }, - }, - }; - default: - return { - update: { - _id: `${items.type}:${items.id}`, - ...mockVersionProps, - result: 'updated', - }, - }; - } - }), - }); - - const { - saved_objects: [firstUpdatedObject, invalidType, secondUpdatedObject, versionClashObject], - } = await savedObjectsRepository.bulkUpdate(objects); - - expect(firstUpdatedObject).toMatchObject({ - ..._.pick(objects[0], 'id', 'type', 'attributes', 'references'), - version: mockVersion, + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(findSuccess({ type })).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); + }); - expect(invalidType).toMatchObject({ - ..._.pick(objects[1], 'id', 'type'), - error: SavedObjectsErrorHelpers.createGenericNotFoundError('invalid-type', 'invalid').output - .payload, - }); + describe('returns', () => { + it(`formats the ES response when there is no namespace`, async () => { + const noNamespaceSearchResults = generateSearchResults(); + callAdminCluster.mockReturnValue(noNamespaceSearchResults); + const count = noNamespaceSearchResults.hits.hits.length; - expect(secondUpdatedObject).toMatchObject({ - ..._.pick(objects[2], 'id', 'type', 'attributes', 'references'), - version: mockVersion, - }); + const response = await savedObjectsRepository.find({ type }); + + expect(response.total).toBe(count); + expect(response.saved_objects).toHaveLength(count); - expect(versionClashObject).toMatchObject({ - ..._.pick(objects[3], 'id', 'type'), - error: { statusCode: 409, message: 'version conflict, document already exists' }, + noNamespaceSearchResults.hits.hits.forEach((doc, i) => { + expect(response.saved_objects[i]).toEqual({ + id: doc._id.replace(/(index-pattern|config|globalType)\:/, ''), + type: doc._source.type, + ...mockTimestampFields, + version: mockVersion, + attributes: doc._source[doc._source.type], + references: [], + }); + }); }); - }); - it('doesnt call Elasticsearch if there are no valid objects to update', async () => { - const objects = [ - { - type: 'invalid-type', - id: 'invalid', - attributes: { title: 'invalid' }, - }, - { - type: 'invalid-type', - id: 'invalid 2', - attributes: { title: 'invalid' }, - }, - ]; + it(`formats the ES response when there is a namespace`, async () => { + const namespacedSearchResults = generateSearchResults(namespace); + callAdminCluster.mockReturnValue(namespacedSearchResults); + const count = namespacedSearchResults.hits.hits.length; - const { - saved_objects: [invalidType, invalidType2], - } = await savedObjectsRepository.bulkUpdate(objects); + const response = await savedObjectsRepository.find({ type, namespace }); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(response.total).toBe(count); + expect(response.saved_objects).toHaveLength(count); - expect(invalidType).toMatchObject({ - ..._.pick(objects[0], 'id', 'type'), - error: SavedObjectsErrorHelpers.createGenericNotFoundError('invalid-type', 'invalid').output - .payload, + namespacedSearchResults.hits.hits.forEach((doc, i) => { + expect(response.saved_objects[i]).toEqual({ + id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|globalType)\:/, ''), + type: doc._source.type, + ...mockTimestampFields, + version: mockVersion, + attributes: doc._source[doc._source.type], + references: [], + }); + }); }); - expect(invalidType2).toMatchObject({ - ..._.pick(objects[1], 'id', 'type'), - error: SavedObjectsErrorHelpers.createGenericNotFoundError('invalid-type', 'invalid 2') - .output.payload, + it(`should return empty results when attempting to find only invalid or hidden types`, async () => { + const test = async types => { + const result = await savedObjectsRepository.find({ type: types }); + expect(result).toEqual(expect.objectContaining({ saved_objects: [] })); + }; + + await test('unknownType'); + await test(HIDDEN_TYPE); + await test(['unknownType', HIDDEN_TYPE]); }); }); - it('accepts version', async () => { - const objects = [ - generateSavedObject({ - version: encodeHitVersion({ - _seq_no: 100, - _primary_term: 200, - }), - }), - generateSavedObject({ - version: encodeHitVersion({ - _seq_no: 300, - _primary_term: 400, - }), - }), - ]; + describe('search dsl', () => { + it(`passes mappings, registry, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl`, async () => { + const relevantOpts = { + namespace, + search: 'foo*', + searchFields: ['foo'], + type: [type], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + kueryNode: undefined, + }; - mockValidResponse(objects); + await findSuccess(relevantOpts, namespace); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, relevantOpts); + }); + + it(`accepts KQL filter and passes kueryNode to getSearchDsl`, async () => { + const findOpts = { + namespace, + search: 'foo*', + searchFields: ['foo'], + type: ['dashboard'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + indexPattern: undefined, + filter: 'dashboard.attributes.otherField: *', + }; + + await findSuccess(findOpts, namespace); + const { kueryNode } = getSearchDslNS.getSearchDsl.mock.calls[0][2]; + expect(kueryNode).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "dashboard.otherField", + }, + Object { + "type": "wildcard", + "value": "@kuery-wildcard@", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); - await savedObjectsRepository.bulkUpdate(objects); + it(`supports multiple types`, async () => { + const types = ['config', 'index-pattern']; + await findSuccess({ type: types }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( + mappings, + registry, + expect.objectContaining({ + type: types, + }) + ); + }); - const [ - , - { - body: [{ update: firstUpdate }, , { update: secondUpdate }], - }, - ] = callAdminCluster.mock.calls[0]; + it(`filters out invalid types`, async () => { + const types = ['config', 'unknownType', 'index-pattern']; + await findSuccess({ type: types }); - expect(firstUpdate).toMatchObject({ - if_seq_no: 100, - if_primary_term: 200, + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( + mappings, + registry, + expect.objectContaining({ + type: ['config', 'index-pattern'], + }) + ); }); - expect(secondUpdate).toMatchObject({ - if_seq_no: 300, - if_primary_term: 400, + it(`filters out hidden types`, async () => { + const types = ['config', HIDDEN_TYPE, 'index-pattern']; + await findSuccess({ type: types }); + + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( + mappings, + registry, + expect.objectContaining({ + type: ['config', 'index-pattern'], + }) + ); }); }); + }); - it('does not pass references if omitted', async () => { - const objects = [ - { - type: 'index-pattern', - id: `logstash-no-ref`, - attributes: { title: `Testing no-ref` }, - }, - ]; + describe('#get', () => { + const type = 'index-pattern'; + const id = 'logstash-*'; + const namespace = 'foo-namespace'; + + const getSuccess = async (type, id, options) => { + const response = getMockGetResponse({ type, id, namespace: options?.namespace }); + callAdminCluster.mockResolvedValue(response); + const result = await savedObjectsRepository.get(type, id, options); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + return result; + }; - mockValidResponse(objects); + describe('cluster calls', () => { + it(`should use the ES get action`, async () => { + await getSuccess(type, id); + expectClusterCalls('get'); + }); - await savedObjectsRepository.bulkUpdate(objects); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await getSuccess(type, id, { namespace }); + expectClusterCallArgs({ id: `${namespace}:${type}:${id}` }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await getSuccess(type, id); + expectClusterCallArgs({ id: `${type}:${id}` }); + }); - const [ - , - { - body: [, { doc: firstDoc }], - }, - ] = callAdminCluster.mock.calls[0]; + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await getSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); + expectClusterCallArgs({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }); - expect(firstDoc).not.toMatchObject({ - references: [], + callAdminCluster.mockReset(); + await getSuccess(MULTI_NAMESPACE_TYPE, id, { namespace }); + expectClusterCallArgs({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }); }); }); - it('passes references if they are provided', async () => { - const objects = [ - generateSavedObject({ - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - }), - ]; + describe('errors', () => { + const expectNotFoundError = async (type, id, options) => { + await expect(savedObjectsRepository.get(type, id, options)).rejects.toThrowError( + createGenericNotFoundError(type, id) + ); + }; - mockValidResponse(objects); + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - await savedObjectsRepository.bulkUpdate(objects); + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`throws when ES is unable to find the document during get`, async () => { + callAdminCluster.mockResolvedValue({ found: false }); + await expectNotFoundError(type, id); + expectClusterCalls('get'); + }); - const [ - , - { - body: [, { doc }], - }, - ] = callAdminCluster.mock.calls[0]; + it(`throws when ES is unable to find the index during get`, async () => { + callAdminCluster.mockResolvedValue({ status: 404 }); + await expectNotFoundError(type, id); + expectClusterCalls('get'); + }); - expect(doc).toMatchObject({ - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], + it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); + callAdminCluster.mockResolvedValue(response); + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id, { namespace: 'bar-namespace' }); + expectClusterCalls('get'); }); }); - it('passes empty references array if empty references array is provided', async () => { - const objects = [ - { - type: 'index-pattern', - id: `logstash-no-ref`, - attributes: { title: `Testing no-ref` }, - references: [], - }, - ]; - - mockValidResponse(objects); - - await savedObjectsRepository.bulkUpdate(objects); + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(getSuccess(type, id)).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + describe('returns', () => { + it(`formats the ES response`, async () => { + const result = await getSuccess(type, id); + expect(result).toEqual({ + id, + type, + updated_at: mockTimestamp, + version: mockVersion, + attributes: { + title: 'Testing', + }, + references: [], + }); + }); - const [ - , - { - body: [, { doc }], - }, - ] = callAdminCluster.mock.calls[0]; + it(`includes namespaces if type is multi-namespace`, async () => { + const result = await getSuccess(MULTI_NAMESPACE_TYPE, id); + expect(result).toMatchObject({ + namespaces: expect.any(Array), + }); + }); - expect(doc).toMatchObject({ - references: [], + it(`doesn't include namespaces if type is not multi-namespace`, async () => { + const result = await getSuccess(type, id); + expect(result).not.toMatchObject({ + namespaces: expect.anything(), + }); }); }); + }); - it('defaults to a refresh setting of `wait_for`', async () => { - const objects = [ - { - type: 'index-pattern', - id: `logstash-no-ref`, - attributes: { title: `Testing no-ref` }, - references: [], + describe('#incrementCounter', () => { + const type = 'config'; + const id = 'one'; + const field = 'buildNum'; + const namespace = 'foo-namespace'; + + const incrementCounterSuccess = async (type, id, field, options) => { + const isMultiNamespace = registry.isMultiNamespace(type); + if (isMultiNamespace) { + const response = getMockGetResponse({ type, id, namespace: options?.namespace }); + callAdminCluster.mockResolvedValueOnce(response); // this._callCluster('get', ...) + } + callAdminCluster.mockImplementation((method, params) => ({ + _id: params.id, + ...mockVersionProps, + _index: '.kibana', + get: { + found: true, + _source: { + type, + ...mockTimestampFields, + [type]: { + [field]: 8468, + defaultIndex: 'logstash-*', + }, + }, }, - ]; - - mockValidResponse(objects); + })); + const result = await savedObjectsRepository.incrementCounter(type, id, field, options); + expect(callAdminCluster).toHaveBeenCalledTimes(isMultiNamespace ? 2 : 1); + return result; + }; - await savedObjectsRepository.bulkUpdate(objects); + describe('cluster calls', () => { + it(`should use the ES update action if type is not multi-namespace`, async () => { + await incrementCounterSuccess(type, id, field, { namespace }); + expectClusterCalls('update'); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`should use the ES get action then update action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { + await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, field, { namespace }); + expectClusterCalls('get', 'update'); + }); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ refresh: 'wait_for' }); - }); + it(`defaults to a refresh setting of wait_for`, async () => { + await incrementCounterSuccess(type, id, field, { namespace }); + expectClusterCallArgs({ refresh: 'wait_for' }); + }); - it('accepts a custom refresh setting', async () => { - const objects = [ - { - type: 'index-pattern', - id: `logstash-no-ref`, - attributes: { title: `Testing no-ref` }, - references: [], - }, - ]; + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await incrementCounterSuccess(type, id, field, { namespace, refresh }); + expectClusterCallArgs({ refresh }); + }); - mockValidResponse(objects); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await incrementCounterSuccess(type, id, field, { namespace }); + expectClusterCallArgs({ id: `${namespace}:${type}:${id}` }); + }); - await savedObjectsRepository.bulkUpdate(objects, { refresh: true }); + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await incrementCounterSuccess(type, id, field); + expectClusterCallArgs({ id: `${type}:${id}` }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, field, { namespace }); + expectClusterCallArgs({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ refresh: true }); + callAdminCluster.mockReset(); + await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, field, { namespace }); + expectClusterCallArgs({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }); + }); }); - it(`prepends namespace to the id but doesn't add namespace to body when providing namespace for namespaced type`, async () => { - const objects = [generateSavedObject(), generateSavedObject()]; + describe('errors', () => { + const expectUnsupportedTypeError = async (type, id, field) => { + await expect(savedObjectsRepository.incrementCounter(type, id, field)).rejects.toThrowError( + createUnsupportedTypeError(type) + ); + }; - mockValidResponse(objects); + it(`throws when type is not a string`, async () => { + const test = async type => { + await expect( + savedObjectsRepository.incrementCounter(type, id, field) + ).rejects.toThrowError(`"type" argument must be a string`); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; - await savedObjectsRepository.bulkUpdate(objects, { - namespace: 'foo-namespace', + await test(null); + await test(42); + await test(false); + await test({}); }); - const [ - , - { - body: [ - { update: firstUpdate }, - { doc: firstUpdateDoc }, - { update: secondUpdate }, - { doc: secondUpdateDoc }, - ], - }, - ] = callAdminCluster.mock.calls[0]; + it(`throws when counterFieldName is not a string`, async () => { + const test = async field => { + await expect( + savedObjectsRepository.incrementCounter(type, id, field) + ).rejects.toThrowError(`"counterFieldName" argument must be a string`); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; - expect(firstUpdate).toMatchObject({ - _id: 'foo-namespace:index-pattern:logstash-1', - _index: '.kibana-test', + await test(null); + await test(42); + await test(false); + await test({}); }); - expect(firstUpdateDoc).toMatchObject({ - updated_at: mockTimestamp, - 'index-pattern': { title: 'Testing 1' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], + it(`throws when type is invalid`, async () => { + await expectUnsupportedTypeError('unknownType', id, field); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - expect(secondUpdate).toMatchObject({ - _id: 'foo-namespace:index-pattern:logstash-2', - _index: '.kibana-test', + it(`throws when type is hidden`, async () => { + await expectUnsupportedTypeError(HIDDEN_TYPE, id, field); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - expect(secondUpdateDoc).toMatchObject({ - updated_at: mockTimestamp, - 'index-pattern': { title: 'Testing 2' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], + it(`throws when there is a conflict with an existing multi-namespace saved object (get)`, async () => { + const response = getMockGetResponse({ + type: MULTI_NAMESPACE_TYPE, + id, + namespace: 'bar-namespace', + }); + callAdminCluster.mockResolvedValue(response); // this._callCluster('get', ...) + await expect( + savedObjectsRepository.incrementCounter(MULTI_NAMESPACE_TYPE, id, field, { namespace }) + ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_TYPE, id)); + expectClusterCalls('get'); }); }); - it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { - const objects = [generateSavedObject(), generateSavedObject()]; - - mockValidResponse(objects); + describe('migration', () => { + beforeEach(() => { + migrator.migrateDocument.mockImplementation(mockMigrateDocument); + }); - await savedObjectsRepository.bulkUpdate(objects); + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect( + incrementCounterSuccess(type, id, field, { namespace }) + ).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + }); - const [ - , - { - body: [ - { update: firstUpdate }, - { doc: firstUpdateDoc }, - { update: secondUpdate }, - { doc: secondUpdateDoc }, - ], - }, - ] = callAdminCluster.mock.calls[0]; + it(`migrates a document and serializes the migrated doc`, async () => { + const migrationVersion = mockMigrationVersion; + await incrementCounterSuccess(type, id, field, { migrationVersion }); + const attributes = { buildNum: 1 }; // this is added by the incrementCounter function + const doc = { type, id, attributes, migrationVersion, ...mockTimestampFields }; + expectMigrationArgs(doc); - expect(firstUpdate).toMatchObject({ - _id: 'index-pattern:logstash-1', - _index: '.kibana-test', + const migratedDoc = migrator.migrateDocument(doc); + expect(serializer.savedObjectToRaw).toHaveBeenLastCalledWith(migratedDoc); }); + }); - expect(firstUpdateDoc).toMatchObject({ - updated_at: mockTimestamp, - 'index-pattern': { title: 'Testing 1' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', + describe('returns', () => { + it(`formats the ES response`, async () => { + callAdminCluster.mockImplementation((method, params) => ({ + _id: params.id, + ...mockVersionProps, + _index: '.kibana', + get: { + found: true, + _source: { + type: 'config', + ...mockTimestampFields, + config: { + buildNum: 8468, + defaultIndex: 'logstash-*', + }, + }, }, - ], - }); - - expect(secondUpdate).toMatchObject({ - _id: 'index-pattern:logstash-2', - _index: '.kibana-test', - }); + })); - expect(secondUpdateDoc).toMatchObject({ - updated_at: mockTimestamp, - 'index-pattern': { title: 'Testing 2' }, - references: [ + const response = await savedObjectsRepository.incrementCounter( + 'config', + '6.0.0-alpha1', + 'buildNum', { - name: 'ref_0', - type: 'test', - id: '1', + namespace: 'foo-namespace', + } + ); + + expect(response).toEqual({ + type: 'config', + id: '6.0.0-alpha1', + ...mockTimestampFields, + version: mockVersion, + attributes: { + buildNum: 8468, + defaultIndex: 'logstash-*', }, - ], + }); }); }); + }); - it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { - const objects = [ - generateSavedObject({ - type: 'globaltype', - id: 'foo', - namespace: 'foo-namespace', - }), - ]; + describe('#deleteFromNamespaces', () => { + const id = 'some-id'; + const type = MULTI_NAMESPACE_TYPE; + const namespace1 = 'default'; + const namespace2 = 'foo-namespace'; + const namespace3 = 'bar-namespace'; + + const mockGetResponse = (type, id, namespaces) => { + // mock a document that exists in two namespaces + const mockResponse = getMockGetResponse({ type, id }); + mockResponse._source.namespaces = namespaces; + callAdminCluster.mockResolvedValueOnce(mockResponse); // this._callCluster('get', ...) + }; + + const deleteFromNamespacesSuccess = async ( + type, + id, + namespaces, + currentNamespaces, + options + ) => { + mockGetResponse(type, id, currentNamespaces); // this._callCluster('get', ...) + const isDelete = currentNamespaces.every(namespace => namespaces.includes(namespace)); + callAdminCluster.mockResolvedValue({ + _id: `${type}:${id}`, + ...mockVersionProps, + result: isDelete ? 'deleted' : 'updated', + }); // this._writeToCluster('delete', ...) *or* this._writeToCluster('update', ...) + const result = await savedObjectsRepository.deleteFromNamespaces( + type, + id, + namespaces, + options + ); + expect(callAdminCluster).toHaveBeenCalledTimes(2); + return result; + }; - mockValidResponse(objects); + describe('cluster calls', () => { + describe('delete action', () => { + const deleteFromNamespacesSuccessDelete = async (expectFn, options, _type = type) => { + const test = async namespaces => { + await deleteFromNamespacesSuccess(_type, id, namespaces, namespaces, options); + expectFn(); + callAdminCluster.mockReset(); + }; + await test([namespace1]); + await test([namespace1, namespace2]); + }; + + it(`should use ES get action then delete action if the object has no namespaces remaining`, async () => { + const expectFn = () => expectClusterCalls('get', 'delete'); + await deleteFromNamespacesSuccessDelete(expectFn); + }); - await savedObjectsRepository.bulkUpdate(objects); + it(`formats the ES requests`, async () => { + const expectFn = () => { + expectClusterCallArgs({ id: `${type}:${id}` }, 1); + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClusterCallArgs({ id: `${type}:${id}`, ...versionProperties }, 2); + }; + await deleteFromNamespacesSuccessDelete(expectFn); + }); - const [ - , - { - body: [{ update }, { doc }], - }, - ] = callAdminCluster.mock.calls[0]; + it(`defaults to a refresh setting of wait_for`, async () => { + await deleteFromNamespacesSuccessDelete(() => + expectClusterCallArgs({ refresh: 'wait_for' }, 2) + ); + }); - expect(update).toMatchObject({ - _id: 'globaltype:foo', - _index: '.kibana-test', - }); + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + const expectFn = () => expectClusterCallArgs({ refresh }, 2); + await deleteFromNamespacesSuccessDelete(expectFn, { refresh }); + }); - expect(doc).toMatchObject({ - updated_at: mockTimestamp, - globaltype: { title: 'Testing 1' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], + it(`should use default index`, async () => { + const expectFn = () => expectClusterCallArgs({ index: '.kibana-test' }, 2); + await deleteFromNamespacesSuccessDelete(expectFn); + }); + + it(`should use custom index`, async () => { + const expectFn = () => expectClusterCallArgs({ index: 'custom' }, 2); + await deleteFromNamespacesSuccessDelete(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE); + }); }); - }); - }); - describe('#incrementCounter', () => { - beforeEach(() => { - callAdminCluster.mockImplementation((method, params) => ({ - _id: params.id, - ...mockVersionProps, - _index: '.kibana', - get: { - found: true, - _source: { - type: 'config', - ...mockTimestampFields, - config: { - buildNum: 8468, - defaultIndex: 'logstash-*', - }, - }, - }, - })); - }); + describe('update action', () => { + const deleteFromNamespacesSuccessUpdate = async (expectFn, options, _type = type) => { + const test = async remaining => { + const currentNamespaces = [namespace1].concat(remaining); + await deleteFromNamespacesSuccess(_type, id, [namespace1], currentNamespaces, options); + expectFn(); + callAdminCluster.mockReset(); + }; + await test([namespace2]); + await test([namespace2, namespace3]); + }; + + it(`should use ES get action then update action if the object has one or more namespaces remaining`, async () => { + await deleteFromNamespacesSuccessUpdate(() => expectClusterCalls('get', 'update')); + }); - it('formats Elasticsearch response', async () => { - callAdminCluster.mockImplementation((method, params) => ({ - _id: params.id, - ...mockVersionProps, - _index: '.kibana', - get: { - found: true, - _source: { - type: 'config', - ...mockTimestampFields, - config: { - buildNum: 8468, - defaultIndex: 'logstash-*', - }, - }, - }, - })); + it(`formats the ES requests`, async () => { + let ctr = 0; + const expectFn = () => { + expectClusterCallArgs({ id: `${type}:${id}` }, 1); + const namespaces = ctr++ === 0 ? [namespace2] : [namespace2, namespace3]; + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClusterCallArgs( + { + id: `${type}:${id}`, + ...versionProperties, + body: { doc: { ...mockTimestampFields, namespaces } }, + }, + 2 + ); + }; + await deleteFromNamespacesSuccessUpdate(expectFn); + }); - const response = await savedObjectsRepository.incrementCounter( - 'config', - '6.0.0-alpha1', - 'buildNum', - { - namespace: 'foo-namespace', - } - ); + it(`defaults to a refresh setting of wait_for`, async () => { + const expectFn = () => expectClusterCallArgs({ refresh: 'wait_for' }, 2); + await deleteFromNamespacesSuccessUpdate(expectFn); + }); - expect(response).toEqual({ - type: 'config', - id: '6.0.0-alpha1', - ...mockTimestampFields, - version: mockVersion, - attributes: { - buildNum: 8468, - defaultIndex: 'logstash-*', - }, + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + const expectFn = () => expectClusterCallArgs({ refresh }, 2); + await deleteFromNamespacesSuccessUpdate(expectFn, { refresh }); + }); + + it(`should use default index`, async () => { + const expectFn = () => expectClusterCallArgs({ index: '.kibana-test' }, 2); + await deleteFromNamespacesSuccessUpdate(expectFn); + }); + + it(`should use custom index`, async () => { + const expectFn = () => expectClusterCallArgs({ index: 'custom' }, 2); + await deleteFromNamespacesSuccessUpdate(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE); + }); }); }); - it('migrates the doc if an upsert is required', async () => { - migrator.migrateDocument = doc => { - doc.attributes.buildNum = 42; - doc.migrationVersion = { foo: '2.3.4' }; - doc.references = [{ name: 'search_0', type: 'search', id: '123' }]; - return doc; + describe('errors', () => { + const expectNotFoundError = async (type, id, namespaces, options) => { + await expect( + savedObjectsRepository.deleteFromNamespaces(type, id, namespaces, options) + ).rejects.toThrowError(createGenericNotFoundError(type, id)); + }; + const expectBadRequestError = async (type, id, namespaces, message) => { + await expect( + savedObjectsRepository.deleteFromNamespaces(type, id, namespaces) + ).rejects.toThrowError(createBadRequestError(message)); }; - await savedObjectsRepository.incrementCounter('config', 'doesnotexist', 'buildNum', { - namespace: 'foo-namespace', + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id, [namespace1, namespace2]); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - body: { - upsert: { - config: { buildNum: 42 }, - migrationVersion: { foo: '2.3.4' }, - type: 'config', - ...mockTimestampFields, - references: [{ name: 'search_0', type: 'search', id: '123' }], - }, - }, + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id, [namespace1, namespace2]); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - }); - it('defaults to a refresh setting of `wait_for`', async () => { - await savedObjectsRepository.incrementCounter('config', 'doesnotexist', 'buildNum', { - namespace: 'foo-namespace', + it(`throws when type is not namespace-agnostic`, async () => { + const test = async type => { + const message = `${type} doesn't support multiple namespaces`; + await expectBadRequestError(type, id, [namespace1, namespace2], message); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; + await test('index-pattern'); + await test(NAMESPACE_AGNOSTIC_TYPE); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: 'wait_for', + it(`throws when namespaces is an empty array`, async () => { + const test = async namespaces => { + const message = 'namespaces must be a non-empty array of strings'; + await expectBadRequestError(type, id, namespaces, message); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; + await test([]); }); - }); - it('accepts a custom refresh setting', async () => { - await savedObjectsRepository.incrementCounter('config', 'doesnotexist', 'buildNum', { - namespace: 'foo-namespace', - refresh: true, + it(`throws when ES is unable to find the document during get`, async () => { + callAdminCluster.mockResolvedValue({ found: false }); // this._callCluster('get', ...) + await expectNotFoundError(type, id, [namespace1, namespace2]); + expectClusterCalls('get'); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: true, + it(`throws when ES is unable to find the index during get`, async () => { + callAdminCluster.mockResolvedValue({ status: 404 }); // this._callCluster('get', ...) + await expectNotFoundError(type, id, [namespace1, namespace2]); + expectClusterCalls('get'); }); - }); - it(`prepends namespace to the id but doesn't add namespace to body when providing namespace for namespaced type`, async () => { - await savedObjectsRepository.incrementCounter('config', '6.0.0-alpha1', 'buildNum', { - namespace: 'foo-namespace', + it(`throws when the document exists, but not in this namespace`, async () => { + mockGetResponse(type, id, [namespace1]); // this._callCluster('get', ...) + await expectNotFoundError(type, id, [namespace1], { namespace: 'some-other-namespace' }); + expectClusterCalls('get'); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`throws when ES is unable to find the document during delete`, async () => { + mockGetResponse(type, id, [namespace1]); // this._callCluster('get', ...) + callAdminCluster.mockResolvedValue({ result: 'not_found' }); // this._writeToCluster('delete', ...) + await expectNotFoundError(type, id, [namespace1]); + expectClusterCalls('get', 'delete'); + }); + + it(`throws when ES is unable to find the index during delete`, async () => { + mockGetResponse(type, id, [namespace1]); // this._callCluster('get', ...) + callAdminCluster.mockResolvedValue({ error: { type: 'index_not_found_exception' } }); // this._writeToCluster('delete', ...) + await expectNotFoundError(type, id, [namespace1]); + expectClusterCalls('get', 'delete'); + }); + + it(`throws when ES returns an unexpected response`, async () => { + mockGetResponse(type, id, [namespace1]); // this._callCluster('get', ...) + callAdminCluster.mockResolvedValue({ result: 'something unexpected' }); // this._writeToCluster('delete', ...) + await expect( + savedObjectsRepository.deleteFromNamespaces(type, id, [namespace1]) + ).rejects.toThrowError('Unexpected Elasticsearch DELETE response'); + expectClusterCalls('get', 'delete'); + }); + + it(`throws when ES is unable to find the document during update`, async () => { + mockGetResponse(type, id, [namespace1, namespace2]); // this._callCluster('get', ...) + callAdminCluster.mockResolvedValue({ status: 404 }); // this._writeToCluster('update', ...) + await expectNotFoundError(type, id, [namespace1]); + expectClusterCalls('get', 'update'); + }); + }); - const requestDoc = callAdminCluster.mock.calls[0][1]; - expect(requestDoc.id).toBe('foo-namespace:config:6.0.0-alpha1'); - expect(requestDoc.body.script.params.type).toBe('config'); - expect(requestDoc.body.upsert.type).toBe('config'); - expect(requestDoc).toHaveProperty('body.upsert.config'); + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + let callAdminClusterCount = 0; + migrator.runMigrations = jest.fn(async () => + // runMigrations should resolve before callAdminCluster is initiated + expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) + ); + await expect( + deleteFromNamespacesSuccess(type, id, [namespace1], [namespace1]) + ).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveReturnedTimes(2); + }); }); - it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { - await savedObjectsRepository.incrementCounter('config', '6.0.0-alpha1', 'buildNum'); + describe('returns', () => { + it(`returns an empty object on success (delete)`, async () => { + const test = async namespaces => { + const result = await deleteFromNamespacesSuccess(type, id, namespaces, namespaces); + expect(result).toEqual({}); + callAdminCluster.mockReset(); + }; + await test([namespace1]); + await test([namespace1, namespace2]); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`returns an empty object on success (update)`, async () => { + const test = async remaining => { + const currentNamespaces = [namespace1].concat(remaining); + const result = await deleteFromNamespacesSuccess( + type, + id, + [namespace1], + currentNamespaces + ); + expect(result).toEqual({}); + callAdminCluster.mockReset(); + }; + await test([namespace2]); + await test([namespace2, namespace3]); + }); - const requestDoc = callAdminCluster.mock.calls[0][1]; - expect(requestDoc.id).toBe('config:6.0.0-alpha1'); - expect(requestDoc.body.script.params.type).toBe('config'); - expect(requestDoc.body.upsert.type).toBe('config'); - expect(requestDoc).toHaveProperty('body.upsert.config'); + it(`succeeds when the document doesn't exist in all of the targeted namespaces`, async () => { + const namespaces = [namespace2]; + const currentNamespaces = [namespace1]; + const result = await deleteFromNamespacesSuccess(type, id, namespaces, currentNamespaces); + expect(result).toEqual({}); + }); }); + }); - it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { - callAdminCluster.mockImplementation((method, params) => ({ - _id: params.id, + describe('#update', () => { + const id = 'logstash-*'; + const type = 'index-pattern'; + const attributes = { title: 'Testing' }; + const namespace = 'foo-namespace'; + const references = [ + { + name: 'ref_0', + type: 'test', + id: '1', + }, + ]; + + const updateSuccess = async (type, id, attributes, options) => { + if (registry.isMultiNamespace(type)) { + const mockGetResponse = getMockGetResponse({ type, id, namespace: options?.namespace }); + callAdminCluster.mockResolvedValueOnce(mockGetResponse); // this._callCluster('get', ...) + } + callAdminCluster.mockResolvedValue({ + _id: `${type}:${id}`, ...mockVersionProps, - _index: '.kibana', - get: { - found: true, - _source: { - type: 'globaltype', - ...mockTimestampFields, - globaltype: { - counter: 1, - }, - }, - }, - })); + result: 'updated', + ...(registry.isMultiNamespace(type) && { + // don't need the rest of the source for test purposes, just the namespaces attribute + get: { _source: { namespaces: [options?.namespace ?? 'default'] } }, + }), + }); // this._writeToCluster('update', ...) + const result = await savedObjectsRepository.update(type, id, attributes, options); + expect(callAdminCluster).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 2 : 1); + return result; + }; - await savedObjectsRepository.incrementCounter('globaltype', 'foo', 'counter', { - namespace: 'foo-namespace', + describe('cluster calls', () => { + it(`should use the ES get action then update action when type is multi-namespace`, async () => { + await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes); + expectClusterCalls('get', 'update'); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`should use the ES update action when type is not multi-namespace`, async () => { + await updateSuccess(type, id, attributes); + expectClusterCalls('update'); + }); - const requestDoc = callAdminCluster.mock.calls[0][1]; - expect(requestDoc.id).toBe('globaltype:foo'); - expect(requestDoc.body.script.params.type).toBe('globaltype'); - expect(requestDoc.body.upsert.type).toBe('globaltype'); - expect(requestDoc).toHaveProperty('body.upsert.globaltype'); - }); + it(`defaults to no references array`, async () => { + await updateSuccess(type, id, attributes); + expectClusterCallArgs({ + body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, + }); + }); - it('should assert that the "type" and "counterFieldName" arguments are strings', () => { - expect.assertions(6); - - expect( - savedObjectsRepository.incrementCounter(null, '6.0.0-alpha1', 'buildNum', { - namespace: 'foo-namespace', - }) - ).rejects.toEqual(new Error('"type" argument must be a string')); - - expect( - savedObjectsRepository.incrementCounter(42, '6.0.0-alpha1', 'buildNum', { - namespace: 'foo-namespace', - }) - ).rejects.toEqual(new Error('"type" argument must be a string')); - - expect( - savedObjectsRepository.incrementCounter({}, '6.0.0-alpha1', 'buildNum', { - namespace: 'foo-namespace', - }) - ).rejects.toEqual(new Error('"type" argument must be a string')); - - expect( - savedObjectsRepository.incrementCounter('config', '6.0.0-alpha1', null, { - namespace: 'foo-namespace', - }) - ).rejects.toEqual(new Error('"counterFieldName" argument must be a string')); - - expect( - savedObjectsRepository.incrementCounter('config', '6.0.0-alpha1', 42, { - namespace: 'foo-namespace', - }) - ).rejects.toEqual(new Error('"counterFieldName" argument must be a string')); - - expect( - savedObjectsRepository.incrementCounter( - 'config', - '6.0.0-alpha1', - {}, - { - namespace: 'foo-namespace', - } - ) - ).rejects.toEqual(new Error('"counterFieldName" argument must be a string')); - }); - }); + it(`accepts custom references array`, async () => { + const test = async references => { + await updateSuccess(type, id, attributes, { references }); + expectClusterCallArgs({ + body: { doc: expect.objectContaining({ references }) }, + }); + callAdminCluster.mockReset(); + }; + await test(references); + await test(['string']); + await test([]); + }); + + it(`doesn't accept custom references if not an array`, async () => { + const test = async references => { + await updateSuccess(type, id, attributes, { references }); + expectClusterCallArgs({ + body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, + }); + callAdminCluster.mockReset(); + }; + await test('string'); + await test(123); + await test(true); + await test(null); + }); + + it(`defaults to a refresh setting of wait_for`, async () => { + await updateSuccess(type, id, { foo: 'bar' }); + expectClusterCallArgs({ refresh: 'wait_for' }); + }); + + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await updateSuccess(type, id, { foo: 'bar' }, { refresh }); + expectClusterCallArgs({ refresh }); + }); + + it(`defaults to the version of the existing document when type is multi-namespace`, async () => { + await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes, { references }); + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClusterCallArgs(versionProperties, 2); + }); + + it(`accepts version`, async () => { + await updateSuccess(type, id, attributes, { + version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), + }); + expectClusterCallArgs({ if_seq_no: 100, if_primary_term: 200 }); + }); - describe('types on custom index', () => { - it("should error when attempting to 'update' an unsupported type", async () => { - await expect( - savedObjectsRepository.update('hiddenType', 'bogus', { title: 'some title' }) - ).rejects.toEqual(new Error('Saved object [hiddenType/bogus] not found')); - }); - }); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await updateSuccess(type, id, attributes, { namespace }); + expectClusterCallArgs({ id: expect.stringMatching(`${namespace}:${type}:${id}`) }); + }); - describe('unsupported types', () => { - it("should error when attempting to 'update' an unsupported type", async () => { - await expect( - savedObjectsRepository.update('hiddenType', 'bogus', { title: 'some title' }) - ).rejects.toEqual(new Error('Saved object [hiddenType/bogus] not found')); - }); + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await updateSuccess(type, id, attributes, { references }); + expectClusterCallArgs({ id: expect.stringMatching(`${type}:${id}`) }); + }); - it("should error when attempting to 'get' an unsupported type", async () => { - await expect(savedObjectsRepository.get('hiddenType')).rejects.toEqual( - new Error('Not Found') - ); - }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await updateSuccess(NAMESPACE_AGNOSTIC_TYPE, id, attributes, { namespace }); + expectClusterCallArgs({ id: expect.stringMatching(`${NAMESPACE_AGNOSTIC_TYPE}:${id}`) }); - it("should return an error object when attempting to 'create' an unsupported type", async () => { - await expect( - savedObjectsRepository.create('hiddenType', { title: 'some title' }) - ).rejects.toEqual(new Error("Unsupported saved object type: 'hiddenType': Bad Request")); - }); + callAdminCluster.mockReset(); + await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes, { namespace }); + expectClusterCallArgs({ id: expect.stringMatching(`${MULTI_NAMESPACE_TYPE}:${id}`) }, 2); + }); - it("should not return hidden saved ojects when attempting to 'find' support and unsupported types", async () => { - callAdminCluster.mockReturnValue({ - hits: { - total: 1, - hits: [ - { - _id: 'one', - _source: { - updated_at: mockTimestamp, - type: 'config', - }, - references: [], - }, - ], - }, + it(`includes _sourceIncludes when type is multi-namespace`, async () => { + await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes); + expectClusterCallArgs({ _sourceIncludes: ['namespaces'] }, 2); }); - const results = await savedObjectsRepository.find({ type: ['hiddenType', 'config'] }); - expect(results).toEqual({ - total: 1, - saved_objects: [ - { - id: 'one', - references: [], - type: 'config', - updated_at: mockTimestamp, - }, - ], - page: 1, - per_page: 20, + + it(`doesn't include _sourceIncludes when type is not multi-namespace`, async () => { + await updateSuccess(type, id, attributes); + expect(callAdminCluster).toHaveBeenLastCalledWith( + expect.any(String), + expect.not.objectContaining({ + _sourceIncludes: expect.anything(), + }) + ); }); }); - it("should return empty results when attempting to 'find' an unsupported type", async () => { - callAdminCluster.mockReturnValue({ - hits: { - total: 0, - hits: [], - }, + describe('errors', () => { + const expectNotFoundError = async (type, id) => { + await expect(savedObjectsRepository.update(type, id)).rejects.toThrowError( + createGenericNotFoundError(type, id) + ); + }; + + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - const results = await savedObjectsRepository.find({ type: 'hiddenType' }); - expect(results).toEqual({ - total: 0, - saved_objects: [], - page: 1, - per_page: 20, + + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - }); - it("should return empty results when attempting to 'find' more than one unsupported types", async () => { - const findParams = { type: ['hiddenType', 'hiddenType2'] }; - callAdminCluster.mockReturnValue({ - status: 200, - hits: { - total: 0, - hits: [], - }, + it(`throws when ES is unable to find the document during get`, async () => { + callAdminCluster.mockResolvedValue({ found: false }); // this._callCluster('get', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get'); }); - const results = await savedObjectsRepository.find(findParams); - expect(results).toEqual({ - total: 0, - saved_objects: [], - page: 1, - per_page: 20, + + it(`throws when ES is unable to find the index during get`, async () => { + callAdminCluster.mockResolvedValue({ status: 404 }); // this._callCluster('get', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get'); }); - }); - it("should error when attempting to 'delete' hidden types", async () => { - await expect(savedObjectsRepository.delete('hiddenType')).rejects.toEqual( - new Error('Not Found') - ); + it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); + callAdminCluster.mockResolvedValue(response); // this._callCluster('get', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id, { namespace: 'bar-namespace' }); + expectClusterCalls('get'); + }); + + it(`throws when ES is unable to find the document during update`, async () => { + callAdminCluster.mockResolvedValue({ status: 404 }); // this._writeToCluster('update', ...) + await expectNotFoundError(type, id); + expectClusterCalls('update'); + }); }); - it("should error when attempting to 'bulkCreate' an unsupported type", async () => { - callAdminCluster.mockReturnValue({ - items: [ - { - index: { - _id: 'one', - _seq_no: 1, - _primary_term: 1, - _type: 'config', - attributes: { - title: 'Test One', - }, - }, - }, - ], - }); - const results = await savedObjectsRepository.bulkCreate([ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'hiddenType', id: 'two', attributes: { title: 'Test Two' } }, - ]); - expect(results).toEqual({ - saved_objects: [ - { - type: 'config', - id: 'one', - attributes: { title: 'Test One' }, - references: [], - version: 'WzEsMV0=', - updated_at: mockTimestamp, - }, - { - error: { - error: 'Bad Request', - message: "Unsupported saved object type: 'hiddenType': Bad Request", - statusCode: 400, - }, - id: 'two', - type: 'hiddenType', - }, - ], + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + let callAdminClusterCount = 0; + migrator.runMigrations = jest.fn(async () => + // runMigrations should resolve before callAdminCluster is initiated + expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) + ); + await expect(updateSuccess(type, id, attributes)).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveReturnedTimes(1); }); }); - it("should error when attempting to 'incrementCounter' for an unsupported type", async () => { - await expect( - savedObjectsRepository.incrementCounter('hiddenType', 'doesntmatter', 'fieldArg') - ).rejects.toEqual(new Error("Unsupported saved object type: 'hiddenType': Bad Request")); + describe('returns', () => { + it(`returns _seq_no and _primary_term encoded as version`, async () => { + const result = await updateSuccess(type, id, attributes, { + namespace, + references, + }); + expect(result).toEqual({ + id, + type, + ...mockTimestampFields, + version: mockVersion, + attributes, + references, + }); + }); + + it(`includes namespaces if type is multi-namespace`, async () => { + const result = await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes); + expect(result).toMatchObject({ + namespaces: expect.any(Array), + }); + }); + + it(`doesn't include namespaces if type is not multi-namespace`, async () => { + const result = await updateSuccess(type, id, attributes); + expect(result).not.toMatchObject({ + namespaces: expect.anything(), + }); + }); }); }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 72a7867854b60..5f17c11792763 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -45,6 +45,8 @@ import { SavedObjectsBulkUpdateObject, SavedObjectsBulkUpdateOptions, SavedObjectsDeleteOptions, + SavedObjectsAddToNamespacesOptions, + SavedObjectsDeleteFromNamespacesOptions, } from '../saved_objects_client'; import { SavedObject, @@ -60,20 +62,12 @@ import { validateConvertFilterToKueryNode } from './filter_utils'; // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type Left = { - tag: 'Left'; - error: T; -}; +type Left = { tag: 'Left'; error: Record }; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type Right = { - tag: 'Right'; - value: T; -}; - -type Either = Left | Right; -const isLeft = (either: Either): either is Left => { - return either.tag === 'Left'; -}; +type Right = { tag: 'Right'; value: Record }; +type Either = Left | Right; +const isLeft = (either: Either): either is Left => either.tag === 'Left'; +const isRight = (either: Either): either is Right => either.tag === 'Right'; export interface SavedObjectsRepositoryOptions { index: string; @@ -220,8 +214,8 @@ export class SavedObjectsRepository { const { id, migrationVersion, - overwrite = false, namespace, + overwrite = false, references = [], refresh = DEFAULT_REFRESH_SETTING, } = options; @@ -230,22 +224,36 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } - const method = id && !overwrite ? 'create' : 'index'; const time = this._getCurrentTime(); + let savedObjectNamespace; + let savedObjectNamespaces: string[] | undefined; + + if (this._registry.isSingleNamespace(type) && namespace) { + savedObjectNamespace = namespace; + } else if (this._registry.isMultiNamespace(type)) { + if (id && overwrite) { + // we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces + savedObjectNamespaces = await this.preflightGetNamespaces(type, id, namespace); + } else { + savedObjectNamespaces = getSavedObjectNamespaces(namespace); + } + } try { const migrated = this._migrator.migrateDocument({ id, type, - namespace, + ...(savedObjectNamespace && { namespace: savedObjectNamespace }), + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), attributes, migrationVersion, updated_at: time, - references, + ...(Array.isArray(references) && { references }), }); const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); + const method = id && overwrite ? 'index' : 'create'; const response = await this._writeToCluster(method, { id: raw._id, index: this.getIndexForType(type), @@ -282,10 +290,9 @@ export class SavedObjectsRepository { ): Promise> { const { namespace, overwrite = false, refresh = DEFAULT_REFRESH_SETTING } = options; const time = this._getCurrentTime(); - const bulkCreateParams: object[] = []; - let requestIndexCounter = 0; - const expectedResults: Array> = objects.map(object => { + let bulkGetRequestIndexCounter = 0; + const expectedResults: Either[] = objects.map(object => { if (!this._allowedTypes.includes(object.type)) { return { tag: 'Left' as 'Left', @@ -297,9 +304,73 @@ export class SavedObjectsRepository { }; } - const method = object.id && !overwrite ? 'create' : 'index'; + const method = object.id && overwrite ? 'index' : 'create'; + const requiresNamespacesCheck = + method === 'index' && this._registry.isMultiNamespace(object.type); + + return { + tag: 'Right' as 'Right', + value: { + method, + object, + ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), + }, + }; + }); + + const bulkGetDocs = expectedResults + .filter(isRight) + .filter(({ value }) => value.esRequestIndex !== undefined) + .map(({ value: { object: { type, id } } }) => ({ + _id: this._serializer.generateRawId(namespace, type, id), + _index: this.getIndexForType(type), + _source: ['type', 'namespaces'], + })); + const bulkGetResponse = bulkGetDocs.length + ? await this._callCluster('mget', { + body: { + docs: bulkGetDocs, + }, + ignore: [404], + }) + : undefined; + + let bulkRequestIndexCounter = 0; + const bulkCreateParams: object[] = []; + const expectedBulkResults: Either[] = expectedResults.map(expectedBulkGetResult => { + if (isLeft(expectedBulkGetResult)) { + return expectedBulkGetResult; + } + + let savedObjectNamespace; + let savedObjectNamespaces; + const { esRequestIndex, object, method } = expectedBulkGetResult.value; + if (esRequestIndex !== undefined) { + const indexFound = bulkGetResponse.status !== 404; + const actualResult = indexFound ? bulkGetResponse.docs[esRequestIndex] : undefined; + const docFound = indexFound && actualResult.found === true; + if (docFound && !this.rawDocExistsInNamespace(actualResult, namespace)) { + const { id, type } = object; + return { + tag: 'Left' as 'Left', + error: { + id, + type, + error: SavedObjectsErrorHelpers.createConflictError(type, id).output.payload, + }, + }; + } + savedObjectNamespaces = getSavedObjectNamespaces(namespace, docFound && actualResult); + } else { + if (this._registry.isSingleNamespace(object.type)) { + savedObjectNamespace = namespace; + } else if (this._registry.isMultiNamespace(object.type)) { + savedObjectNamespaces = getSavedObjectNamespaces(namespace); + } + } + const expectedResult = { - esRequestIndex: requestIndexCounter++, + esRequestIndex: bulkRequestIndexCounter++, requestedId: object.id, rawMigratedDoc: this._serializer.savedObjectToRaw( this._migrator.migrateDocument({ @@ -307,7 +378,8 @@ export class SavedObjectsRepository { type: object.type, attributes: object.attributes, migrationVersion: object.migrationVersion, - namespace, + ...(savedObjectNamespace && { namespace: savedObjectNamespace }), + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), updated_at: time, references: object.references || [], }) as SavedObjectSanitizedDoc @@ -327,19 +399,21 @@ export class SavedObjectsRepository { return { tag: 'Right' as 'Right', value: expectedResult }; }); - const esResponse = await this._writeToCluster('bulk', { - refresh, - body: bulkCreateParams, - }); + const bulkResponse = bulkCreateParams.length + ? await this._writeToCluster('bulk', { + refresh, + body: bulkCreateParams, + }) + : undefined; return { - saved_objects: expectedResults.map(expectedResult => { + saved_objects: expectedBulkResults.map(expectedResult => { if (isLeft(expectedResult)) { - return expectedResult.error; + return expectedResult.error as any; } const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value; - const response = esResponse.items[esRequestIndex]; + const response = bulkResponse.items[esRequestIndex]; const { error, _id: responseId, @@ -348,7 +422,7 @@ export class SavedObjectsRepository { } = Object.values(response)[0] as any; const { - _source: { type, [type]: attributes, references = [] }, + _source: { type, [type]: attributes, references = [], namespaces }, } = rawMigratedDoc; const id = requestedId || responseId; @@ -362,6 +436,7 @@ export class SavedObjectsRepository { return { id, type, + ...(namespaces && { namespaces }), updated_at: time, version: encodeVersion(seqNo, primaryTerm), attributes, @@ -382,32 +457,76 @@ export class SavedObjectsRepository { */ async delete(type: string, id: string, options: SavedObjectsDeleteOptions = {}): Promise<{}> { if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(); + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options; - const response = await this._writeToCluster('delete', { - id: this._serializer.generateRawId(namespace, type, id), + const rawId = this._serializer.generateRawId(namespace, type, id); + let preflightResult: SavedObjectsRawDoc | undefined; + + if (this._registry.isMultiNamespace(type)) { + preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); + const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); + const remainingNamespaces = existingNamespaces?.filter( + x => x !== getNamespaceString(namespace) + ); + + if (remainingNamespaces?.length) { + // if there is 1 or more namespace remaining, update the saved object + const time = this._getCurrentTime(); + + const doc = { + updated_at: time, + namespaces: remainingNamespaces, + }; + + const updateResponse = await this._writeToCluster('update', { + id: rawId, + index: this.getIndexForType(type), + ...getExpectedVersionProperties(undefined, preflightResult), + refresh, + ignore: [404], + body: { + doc, + }, + }); + + if (updateResponse.status === 404) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + return {}; + } + } + + const deleteResponse = await this._writeToCluster('delete', { + id: rawId, index: this.getIndexForType(type), + ...getExpectedVersionProperties(undefined, preflightResult), refresh, ignore: [404], }); - const deleted = response.result === 'deleted'; + const deleted = deleteResponse.result === 'deleted'; if (deleted) { return {}; } - const docNotFound = response.result === 'not_found'; - const indexNotFound = response.error && response.error.type === 'index_not_found_exception'; - if (docNotFound || indexNotFound) { + const deleteDocNotFound = deleteResponse.result === 'not_found'; + const deleteIndexNotFound = + deleteResponse.error && deleteResponse.error.type === 'index_not_found_exception'; + if (deleteDocNotFound || deleteIndexNotFound) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } throw new Error( - `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, response })}` + `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ + type, + id, + response: deleteResponse, + })}` ); } @@ -426,25 +545,37 @@ export class SavedObjectsRepository { } const { refresh = DEFAULT_REFRESH_SETTING } = options; - const allTypes = Object.keys(getRootPropertiesObjects(this._mappings)); + const typesToUpdate = allTypes.filter(type => !this._registry.isNamespaceAgnostic(type)); - const typesToDelete = allTypes.filter(type => !this._registry.isNamespaceAgnostic(type)); - - const esOptions = { - index: this.getIndicesForTypes(typesToDelete), + const updateOptions = { + index: this.getIndicesForTypes(typesToUpdate), ignore: [404], refresh, body: { + script: { + source: ` + if (!ctx._source.containsKey('namespaces')) { + ctx.op = "delete"; + } else { + ctx._source['namespaces'].removeAll(Collections.singleton(params['namespace'])); + if (ctx._source['namespaces'].empty) { + ctx.op = "delete"; + } + } + `, + lang: 'painless', + params: { namespace: getNamespaceString(namespace) }, + }, conflicts: 'proceed', ...getSearchDsl(this._mappings, this._registry, { namespace, - type: typesToDelete, + type: typesToUpdate, }), }, }; - return await this._writeToCluster('deleteByQuery', esOptions); + return await this._writeToCluster('updateByQuery', updateOptions); } /** @@ -586,55 +717,77 @@ export class SavedObjectsRepository { return { saved_objects: [] }; } - const unsupportedTypeObjects = objects - .filter(o => !this._allowedTypes.includes(o.type)) - .map(({ type, id }) => { - return ({ - id, - type, - error: SavedObjectsErrorHelpers.createUnsupportedTypeError(type).output.payload, - } as any) as SavedObject; - }); + let bulkGetRequestIndexCounter = 0; + const expectedBulkGetResults: Either[] = objects.map(object => { + const { type, id, fields } = object; - const supportedTypeObjects = objects.filter(o => this._allowedTypes.includes(o.type)); + if (!this._allowedTypes.includes(type)) { + return { + tag: 'Left' as 'Left', + error: { + id, + type, + error: SavedObjectsErrorHelpers.createUnsupportedTypeError(type).output.payload, + }, + }; + } - const response = await this._callCluster('mget', { - body: { - docs: supportedTypeObjects.map(({ type, id, fields }) => { - return { - _id: this._serializer.generateRawId(namespace, type, id), - _index: this.getIndexForType(type), - _source: includedFields(type, fields), - }; - }), - }, + return { + tag: 'Right' as 'Right', + value: { + type, + id, + fields, + esRequestIndex: bulkGetRequestIndexCounter++, + }, + }; }); + const bulkGetDocs = expectedBulkGetResults + .filter(isRight) + .map(({ value: { type, id, fields } }) => ({ + _id: this._serializer.generateRawId(namespace, type, id), + _index: this.getIndexForType(type), + _source: includedFields(type, fields), + })); + const bulkGetResponse = bulkGetDocs.length + ? await this._callCluster('mget', { + body: { + docs: bulkGetDocs, + }, + ignore: [404], + }) + : undefined; + return { - saved_objects: (response.docs as any[]) - .map((doc, i) => { - const { id, type } = supportedTypeObjects[i]; + saved_objects: expectedBulkGetResults.map(expectedResult => { + if (isLeft(expectedResult)) { + return expectedResult.error as any; + } - if (!doc.found) { - return ({ - id, - type, - error: { statusCode: 404, message: 'Not found' }, - } as any) as SavedObject; - } + const { type, id, esRequestIndex } = expectedResult.value; + const doc = bulkGetResponse.docs[esRequestIndex]; - const time = doc._source.updated_at; - return { + if (!doc.found || !this.rawDocExistsInNamespace(doc, namespace)) { + return ({ id, type, - ...(time && { updated_at: time }), - version: encodeHitVersion(doc), - attributes: doc._source[type], - references: doc._source.references || [], - migrationVersion: doc._source.migrationVersion, - }; - }) - .concat(unsupportedTypeObjects), + error: SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload, + } as any) as SavedObject; + } + + const time = doc._source.updated_at; + return { + id, + type, + ...(doc._source.namespaces && { namespaces: doc._source.namespaces }), + ...(time && { updated_at: time }), + version: encodeHitVersion(doc), + attributes: doc._source[type], + references: doc._source.references || [], + migrationVersion: doc._source.migrationVersion, + }; + }), }; } @@ -666,7 +819,7 @@ export class SavedObjectsRepository { const docNotFound = response.found === false; const indexNotFound = response.status === 404; - if (docNotFound || indexNotFound) { + if (docNotFound || indexNotFound || !this.rawDocExistsInNamespace(response, namespace)) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } @@ -676,6 +829,7 @@ export class SavedObjectsRepository { return { id, type, + ...(response._source.namespaces && { namespaces: response._source.namespaces }), ...(updatedAt && { updated_at: updatedAt }), version: encodeHitVersion(response), attributes: response._source[type], @@ -707,29 +861,32 @@ export class SavedObjectsRepository { const { version, namespace, references, refresh = DEFAULT_REFRESH_SETTING } = options; + let preflightResult: SavedObjectsRawDoc | undefined; + if (this._registry.isMultiNamespace(type)) { + preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); + } + const time = this._getCurrentTime(); const doc = { [type]: attributes, updated_at: time, - references, + ...(Array.isArray(references) && { references }), }; - if (!Array.isArray(doc.references)) { - delete doc.references; - } - const response = await this._writeToCluster('update', { + const updateResponse = await this._writeToCluster('update', { id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), - ...(version && decodeRequestVersion(version)), + ...getExpectedVersionProperties(version, preflightResult), refresh, ignore: [404], body: { doc, }, + ...(this._registry.isMultiNamespace(type) && { _sourceIncludes: ['namespaces'] }), }); - if (response.status === 404) { + if (updateResponse.status === 404) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } @@ -738,12 +895,168 @@ export class SavedObjectsRepository { id, type, updated_at: time, - version: encodeHitVersion(response), + version: encodeHitVersion(updateResponse), + ...(this._registry.isMultiNamespace(type) && { + namespaces: updateResponse.get._source.namespaces, + }), references, attributes, }; } + /** + * Adds one or more namespaces to a given multi-namespace saved object. This method and + * [`deleteFromNamespaces`]{@link SavedObjectsRepository.deleteFromNamespaces} are the only ways to change which Spaces a multi-namespace + * saved object is shared to. + */ + async addToNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsAddToNamespacesOptions = {} + ): Promise<{}> { + if (!this._allowedTypes.includes(type)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + + if (!this._registry.isMultiNamespace(type)) { + throw SavedObjectsErrorHelpers.createBadRequestError( + `${type} doesn't support multiple namespaces` + ); + } + + if (!namespaces.length) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'namespaces must be a non-empty array of strings' + ); + } + + const { version, namespace, refresh = DEFAULT_REFRESH_SETTING } = options; + + const rawId = this._serializer.generateRawId(undefined, type, id); + const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); + const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); + // there should never be a case where a multi-namespace object does not have any existing namespaces + // however, it is a possibility if someone manually modifies the document in Elasticsearch + const time = this._getCurrentTime(); + + const doc = { + updated_at: time, + namespaces: existingNamespaces ? unique(existingNamespaces.concat(namespaces)) : namespaces, + }; + + const updateResponse = await this._writeToCluster('update', { + id: rawId, + index: this.getIndexForType(type), + ...getExpectedVersionProperties(version, preflightResult), + refresh, + ignore: [404], + body: { + doc, + }, + }); + + if (updateResponse.status === 404) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + + return {}; + } + + /** + * Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted + * entirely. This method and [`addToNamespaces`]{@link SavedObjectsRepository.addToNamespaces} are the only ways to change which Spaces a + * multi-namespace saved object is shared to. + */ + async deleteFromNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsDeleteFromNamespacesOptions = {} + ): Promise<{}> { + if (!this._allowedTypes.includes(type)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + + if (!this._registry.isMultiNamespace(type)) { + throw SavedObjectsErrorHelpers.createBadRequestError( + `${type} doesn't support multiple namespaces` + ); + } + + if (!namespaces.length) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'namespaces must be a non-empty array of strings' + ); + } + + const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options; + + const rawId = this._serializer.generateRawId(undefined, type, id); + const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); + const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); + // if there are somehow no existing namespaces, allow the operation to proceed and delete this saved object + const remainingNamespaces = existingNamespaces?.filter(x => !namespaces.includes(x)); + + if (remainingNamespaces?.length) { + // if there is 1 or more namespace remaining, update the saved object + const time = this._getCurrentTime(); + + const doc = { + updated_at: time, + namespaces: remainingNamespaces, + }; + + const updateResponse = await this._writeToCluster('update', { + id: rawId, + index: this.getIndexForType(type), + ...getExpectedVersionProperties(undefined, preflightResult), + refresh, + ignore: [404], + body: { + doc, + }, + }); + + if (updateResponse.status === 404) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + return {}; + } else { + // if there are no namespaces remaining, delete the saved object + const deleteResponse = await this._writeToCluster('delete', { + id: this._serializer.generateRawId(undefined, type, id), + index: this.getIndexForType(type), + ...getExpectedVersionProperties(undefined, preflightResult), + refresh, + ignore: [404], + }); + + const deleted = deleteResponse.result === 'deleted'; + if (deleted) { + return {}; + } + + const deleteDocNotFound = deleteResponse.result === 'not_found'; + const deleteIndexNotFound = + deleteResponse.error && deleteResponse.error.type === 'index_not_found_exception'; + if (deleteDocNotFound || deleteIndexNotFound) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + + throw new Error( + `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ + type, + id, + response: deleteResponse, + })}` + ); + } + } + /** * Updates multiple objects in bulk * @@ -757,10 +1070,10 @@ export class SavedObjectsRepository { options: SavedObjectsBulkUpdateOptions = {} ): Promise> { const time = this._getCurrentTime(); - const bulkUpdateParams: object[] = []; + const { namespace } = options; - let requestIndexCounter = 0; - const expectedResults: Array> = objects.map(object => { + let bulkGetRequestIndexCounter = 0; + const expectedBulkGetResults: Either[] = objects.map(object => { const { type, id } = object; if (!this._allowedTypes.includes(type)) { @@ -775,41 +1088,100 @@ export class SavedObjectsRepository { } const { attributes, references, version } = object; - const { namespace } = options; const documentToSave = { [type]: attributes, updated_at: time, - references, + ...(Array.isArray(references) && { references }), }; - if (!Array.isArray(documentToSave.references)) { - delete documentToSave.references; - } + const requiresNamespacesCheck = this._registry.isMultiNamespace(object.type); - const expectedResult = { - type, - id, - esRequestIndex: requestIndexCounter++, - documentToSave, + return { + tag: 'Right' as 'Right', + value: { + type, + id, + version, + documentToSave, + ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), + }, }; + }); - bulkUpdateParams.push( - { - update: { - _id: this._serializer.generateRawId(namespace, type, id), - _index: this.getIndexForType(type), - ...(version && decodeRequestVersion(version)), + const bulkGetDocs = expectedBulkGetResults + .filter(isRight) + .filter(({ value }) => value.esRequestIndex !== undefined) + .map(({ value: { type, id } }) => ({ + _id: this._serializer.generateRawId(namespace, type, id), + _index: this.getIndexForType(type), + _source: ['type', 'namespaces'], + })); + const bulkGetResponse = bulkGetDocs.length + ? await this._callCluster('mget', { + body: { + docs: bulkGetDocs, }, - }, - { doc: documentToSave } - ); + ignore: [404], + }) + : undefined; - return { tag: 'Right' as 'Right', value: expectedResult }; - }); + let bulkUpdateRequestIndexCounter = 0; + const bulkUpdateParams: object[] = []; + const expectedBulkUpdateResults: Either[] = expectedBulkGetResults.map( + expectedBulkGetResult => { + if (isLeft(expectedBulkGetResult)) { + return expectedBulkGetResult; + } + + const { esRequestIndex, id, type, version, documentToSave } = expectedBulkGetResult.value; + let namespaces; + let versionProperties; + if (esRequestIndex !== undefined) { + const indexFound = bulkGetResponse.status !== 404; + const actualResult = indexFound ? bulkGetResponse.docs[esRequestIndex] : undefined; + const docFound = indexFound && actualResult.found === true; + if (!docFound || !this.rawDocExistsInNamespace(actualResult, namespace)) { + return { + tag: 'Left' as 'Left', + error: { + id, + type, + error: SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload, + }, + }; + } + namespaces = actualResult._source.namespaces; + versionProperties = getExpectedVersionProperties(version, actualResult); + } else { + versionProperties = getExpectedVersionProperties(version); + } + + const expectedResult = { + type, + id, + namespaces, + esRequestIndex: bulkUpdateRequestIndexCounter++, + documentToSave: expectedBulkGetResult.value.documentToSave, + }; + + bulkUpdateParams.push( + { + update: { + _id: this._serializer.generateRawId(namespace, type, id), + _index: this.getIndexForType(type), + ...versionProperties, + }, + }, + { doc: documentToSave } + ); + + return { tag: 'Right' as 'Right', value: expectedResult }; + } + ); const { refresh = DEFAULT_REFRESH_SETTING } = options; - const esResponse = bulkUpdateParams.length + const bulkUpdateResponse = bulkUpdateParams.length ? await this._writeToCluster('bulk', { refresh, body: bulkUpdateParams, @@ -817,13 +1189,13 @@ export class SavedObjectsRepository { : {}; return { - saved_objects: expectedResults.map(expectedResult => { + saved_objects: expectedBulkUpdateResults.map(expectedResult => { if (isLeft(expectedResult)) { - return expectedResult.error; + return expectedResult.error as any; } - const { type, id, documentToSave, esRequestIndex } = expectedResult.value; - const response = esResponse.items[esRequestIndex]; + const { type, id, namespaces, documentToSave, esRequestIndex } = expectedResult.value; + const response = bulkUpdateResponse.items[esRequestIndex]; const { error, _seq_no: seqNo, _primary_term: primaryTerm } = Object.values( response )[0] as any; @@ -839,6 +1211,7 @@ export class SavedObjectsRepository { return { id, type, + ...(namespaces && { namespaces }), updated_at, version: encodeVersion(seqNo, primaryTerm), attributes, @@ -877,10 +1250,20 @@ export class SavedObjectsRepository { const { migrationVersion, namespace, refresh = DEFAULT_REFRESH_SETTING } = options; const time = this._getCurrentTime(); + let savedObjectNamespace; + let savedObjectNamespaces: string[] | undefined; + + if (this._registry.isSingleNamespace(type) && namespace) { + savedObjectNamespace = namespace; + } else if (this._registry.isMultiNamespace(type)) { + savedObjectNamespaces = await this.preflightGetNamespaces(type, id, namespace); + } const migrated = this._migrator.migrateDocument({ id, type, + ...(savedObjectNamespace && { namespace: savedObjectNamespace }), + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), attributes: { [counterFieldName]: 1 }, migrationVersion, updated_at: time, @@ -889,7 +1272,7 @@ export class SavedObjectsRepository { const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); const response = await this._writeToCluster('update', { - id: this._serializer.generateRawId(namespace, type, id), + id: raw._id, index: this.getIndexForType(type), refresh, _source: true, @@ -952,14 +1335,13 @@ export class SavedObjectsRepository { } /** - * Returns an array of indices as specified in `this._schema` for each of the + * Returns an array of indices as specified in `this._registry` for each of the * given `types`. If any of the types don't have an associated index, the * default index `this._index` will be included. * * @param types The types whose indices should be retrieved */ private getIndicesForTypes(types: string[]) { - const unique = (array: string[]) => [...new Set(array)]; return unique(types.map(t => this.getIndexForType(t))); } @@ -975,12 +1357,97 @@ export class SavedObjectsRepository { const savedObject = this._serializer.rawToSavedObject(raw); return omit(savedObject, 'namespace'); } + + /** + * Check to ensure that a raw document exists in a namespace. If the document is not a multi-namespace type, then this returns `true` as + * we rely on the guarantees of the document ID format. If the document is a multi-namespace type, this checks to ensure that the + * document's `namespaces` value includes the string representation of the given namespace. + * + * WARNING: This should only be used for documents that were retrieved from Elasticsearch. Otherwise, the guarantees of the document ID + * format mentioned above do not apply. + */ + private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace?: string) { + const rawDocType = raw._source.type; + + // if the type is namespace isolated, or namespace agnostic, we can continue to rely on the guarantees + // of the document ID format and don't need to check this + if (!this._registry.isMultiNamespace(rawDocType)) { + return true; + } + + const namespaces = raw._source.namespaces; + return namespaces?.includes(getNamespaceString(namespace)) ?? false; + } + + /** + * Pre-flight check to get a multi-namespace saved object's included namespaces. This ensures that, if the saved object exists, it + * includes the target namespace. + * + * @param type The type of the saved object. + * @param id The ID of the saved object. + * @param namespace The target namespace. + * @returns Array of namespaces that this saved object currently includes, or (if the object does not exist yet) the namespaces that a + * newly-created object will include. Value may be undefined if an existing saved object has no namespaces attribute; this should not + * happen in normal operations, but it is possible if the Elasticsearch document is manually modified. + * @throws Will throw an error if the saved object exists and it does not include the target namespace. + */ + private async preflightGetNamespaces(type: string, id: string, namespace?: string) { + if (!this._registry.isMultiNamespace(type)) { + throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); + } + + const response = await this._callCluster('get', { + id: this._serializer.generateRawId(undefined, type, id), + index: this.getIndexForType(type), + ignore: [404], + }); + + const indexFound = response.status !== 404; + const docFound = indexFound && response.found === true; + if (docFound) { + if (!this.rawDocExistsInNamespace(response, namespace)) { + throw SavedObjectsErrorHelpers.createConflictError(type, id); + } + return getSavedObjectNamespaces(namespace, response); + } + return getSavedObjectNamespaces(namespace); + } + + /** + * Pre-flight check for a multi-namespace saved object's namespaces. This ensures that, if the saved object exists, it includes the target + * namespace. + * + * @param type The type of the saved object. + * @param id The ID of the saved object. + * @param namespace The target namespace. + * @returns Raw document from Elasticsearch. + * @throws Will throw an error if the saved object is not found, or if it doesn't include the target namespace. + */ + private async preflightCheckIncludesNamespace(type: string, id: string, namespace?: string) { + if (!this._registry.isMultiNamespace(type)) { + throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); + } + + const rawId = this._serializer.generateRawId(undefined, type, id); + const response = await this._callCluster('get', { + id: rawId, + index: this.getIndexForType(type), + ignore: [404], + }); + + const indexFound = response.status !== 404; + const docFound = indexFound && response.found === true; + if (!docFound || !this.rawDocExistsInNamespace(response, namespace)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + return response as SavedObjectsRawDoc; + } } function getBulkOperationError(error: { type: string; reason?: string }, type: string, id: string) { switch (error.type) { case 'version_conflict_engine_exception': - return { statusCode: 409, message: 'version conflict, document already exists' }; + return SavedObjectsErrorHelpers.createConflictError(type, id).output.payload; case 'document_missing_exception': return SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload; default: @@ -989,3 +1456,49 @@ function getBulkOperationError(error: { type: string; reason?: string }, type: s }; } } + +/** + * Returns an object with the expected version properties. This facilitates Elasticsearch's Optimistic Concurrency Control. + * + * @param version Optional version specified by the consumer. + * @param document Optional existing document that was obtained in a preflight operation. + */ +function getExpectedVersionProperties(version?: string, document?: SavedObjectsRawDoc) { + if (version) { + return decodeRequestVersion(version); + } else if (document) { + return { + if_seq_no: document._seq_no, + if_primary_term: document._primary_term, + }; + } + return {}; +} + +/** + * Returns the string representation of a namespace. + * The default namespace is undefined, and is represented by the string 'default'. + */ +function getNamespaceString(namespace?: string) { + return namespace ?? 'default'; +} + +/** + * Returns a string array of namespaces for a given saved object. If the saved object is undefined, the result is an array that contains the + * current namespace. Value may be undefined if an existing saved object has no namespaces attribute; this should not happen in normal + * operations, but it is possible if the Elasticsearch document is manually modified. + * + * @param namespace The current namespace. + * @param document Optional existing saved object that was obtained in a preflight operation. + */ +function getSavedObjectNamespaces( + namespace?: string, + document?: SavedObjectsRawDoc +): string[] | undefined { + if (document) { + return document._source?.namespaces; + } + return [getNamespaceString(namespace)]; +} + +const unique = (array: string[]) => [...new Set(array)]; diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts index b2129765ee426..a72c21dd5eee4 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts @@ -17,6 +17,9 @@ * under the License. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { esKuery, KueryNode } from '../../../../../../plugins/data/server'; + import { typeRegistryMock } from '../../../saved_objects_type_registry.mock'; import { getQueryParams } from './query_params'; @@ -24,1268 +27,329 @@ const registry = typeRegistryMock.create(); const MAPPINGS = { properties: { - type: { - type: 'keyword', - }, - pending: { - properties: { - title: { - type: 'text', - }, - }, - }, + pending: { properties: { title: { type: 'text' } } }, saved: { properties: { - title: { - type: 'text', - fields: { - raw: { - type: 'keyword', - }, - }, - }, - obj: { - properties: { - key1: { - type: 'text', - }, - }, - }, - }, - }, - global: { - properties: { - name: { - type: 'keyword', - }, + title: { type: 'text', fields: { raw: { type: 'keyword' } } }, + obj: { properties: { key1: { type: 'text' } } }, }, }, + // mock registry returns isMultiNamespace=true for 'shared' type + shared: { properties: { name: { type: 'keyword' } } }, + // mock registry returns isNamespaceAgnostic=true for 'global' type + global: { properties: { name: { type: 'keyword' } } }, }, }; +const ALL_TYPES = Object.keys(MAPPINGS.properties); +// get all possible subsets (combination) of all types +const ALL_TYPE_SUBSETS = ALL_TYPES.reduce( + (subsets, value) => subsets.concat(subsets.map(set => [...set, value])), + [[] as string[]] +) + .filter(x => x.length) // exclude empty set + .map(x => (x.length === 1 ? x[0] : x)); // if a subset is a single string, destructure it -// create a type clause to be used within the "should", if a namespace is specified -// the clause will ensure the namespace matches; otherwise, the clause will ensure -// that there isn't a namespace field. -const createTypeClause = (type: string, namespace?: string) => { - if (namespace) { - return { - bool: { - must: [{ term: { type } }, { term: { namespace } }], - }, - }; - } +/** + * Note: these tests cases are defined in the order they appear in the source code, for readability's sake + */ +describe('#getQueryParams', () => { + const mappings = MAPPINGS; + type Result = ReturnType; - return { - bool: { - must: [{ term: { type } }], - must_not: [{ exists: { field: 'namespace' } }], - }, - }; -}; + describe('kueryNode filter clause (query.bool.filter[...]', () => { + const expectResult = (result: Result, expected: any) => { + expect(result.query.bool.filter).toEqual(expect.arrayContaining([expected])); + }; -describe('searchDsl/queryParams', () => { - describe('no parameters', () => { - it('searches for all known types without a namespace specified', () => { - expect(getQueryParams({ mappings: MAPPINGS, registry })).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending'), - createTypeClause('saved'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, + describe('`kueryNode` parameter', () => { + it('does not include the clause when `kueryNode` is not specified', () => { + const result = getQueryParams({ mappings, registry, kueryNode: undefined }); + expect(result.query.bool.filter).toHaveLength(1); }); - }); - }); - describe('namespace', () => { - it('filters namespaced types for namespace, and ensures namespace agnostic types have no namespace', () => { - expect(getQueryParams({ mappings: MAPPINGS, registry, namespace: 'foo-namespace' })).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending', 'foo-namespace'), - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - }); - }); - }); + it('includes the specified Kuery clause', () => { + const test = (kueryNode: KueryNode) => { + const result = getQueryParams({ mappings, registry, kueryNode }); + const expected = esKuery.toElasticsearchQuery(kueryNode); + expect(result.query.bool.filter).toHaveLength(2); + expectResult(result, expected); + }; - describe('type (singular, namespaced)', () => { - it('includes a terms filter for type and namespace not being specified', () => { - expect( - getQueryParams({ mappings: MAPPINGS, registry, namespace: undefined, type: 'saved' }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved')], - minimum_should_match: 1, - }, - }, - ], - }, - }, - }); - }); - }); + const simpleClause = { + type: 'function', + function: 'is', + arguments: [ + { type: 'literal', value: 'global.name' }, + { type: 'literal', value: 'GLOBAL' }, + { type: 'literal', value: false }, + ], + } as KueryNode; + test(simpleClause); - describe('type (singular, global)', () => { - it('includes a terms filter for type and namespace not being specified', () => { - expect( - getQueryParams({ mappings: MAPPINGS, registry, namespace: undefined, type: 'global' }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('global')], - minimum_should_match: 1, + const complexClause = { + type: 'function', + function: 'and', + arguments: [ + simpleClause, + { + type: 'function', + function: 'not', + arguments: [ + { + type: 'function', + function: 'is', + arguments: [ + { type: 'literal', value: 'saved.obj.key1' }, + { type: 'literal', value: 'key' }, + { type: 'literal', value: true }, + ], }, - }, - ], - }, - }, + ], + }, + ], + } as KueryNode; + test(complexClause); }); }); }); - describe('type (plural, namespaced and global)', () => { - it('includes term filters for types and namespace not being specified', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: ['saved', 'global'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - }, - }, - }); - }); - }); + describe('reference filter clause (query.bool.filter[bool.must])', () => { + describe('`hasReference` parameter', () => { + const expectResult = (result: Result, expected: any) => { + expect(result.query.bool.filter).toEqual( + expect.arrayContaining([{ bool: expect.objectContaining({ must: expected }) }]) + ); + }; - describe('namespace, type (plural, namespaced and global)', () => { - it('includes a terms filter for type and namespace not being specified', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + it('does not include the clause when `hasReference` is not specified', () => { + const result = getQueryParams({ + mappings, registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - }, - }, + hasReference: undefined, + }); + expectResult(result, undefined); }); - }); - }); - describe('search', () => { - it('includes a sqs query and all known types without a namespace specified', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + it('creates a clause with query for specified reference', () => { + const hasReference = { id: 'foo', type: 'bar' }; + const result = getQueryParams({ + mappings, registry, - namespace: undefined, - type: undefined, - search: 'us*', - }) - ).toEqual({ - query: { - bool: { - filter: [ - { + hasReference, + }); + expectResult(result, [ + { + nested: { + path: 'references', + query: { bool: { - should: [ - createTypeClause('pending'), - createTypeClause('saved'), - createTypeClause('global'), + must: [ + { term: { 'references.id': hasReference.id } }, + { term: { 'references.type': hasReference.type } }, ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'us*', - lenient: true, - fields: ['*'], }, }, - ], + }, }, - }, + ]); }); }); }); - describe('namespace, search', () => { - it('includes a sqs query and namespaced types with the namespace and global types without a namespace', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: undefined, - search: 'us*', - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending', 'foo-namespace'), - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'us*', - lenient: true, - fields: ['*'], - }, - }, - ], - }, - }, - }); - }); - }); + describe('type filter clauses (query.bool.filter[bool.should])', () => { + describe('`type` parameter', () => { + const expectResult = (result: Result, ...types: string[]) => { + expect(result.query.bool.filter).toEqual( + expect.arrayContaining([ + { + bool: expect.objectContaining({ + should: types.map(type => ({ + bool: expect.objectContaining({ + must: expect.arrayContaining([{ term: { type } }]), + }), + })), + minimum_should_match: 1, + }), + }, + ]) + ); + }; - describe('type (plural, namespaced and global), search', () => { - it('includes a sqs query and types without a namespace', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: ['saved', 'global'], - search: 'us*', - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'us*', - lenient: true, - fields: ['*'], - }, - }, - ], - }, - }, + it('searches for all known types when `type` is not specified', () => { + const result = getQueryParams({ mappings, registry, type: undefined }); + expectResult(result, ...ALL_TYPES); }); - }); - }); - describe('namespace, type (plural, namespaced and global), search', () => { - it('includes a sqs query and namespace type with a namespace and global type without a namespace', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - search: 'us*', - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'us*', - lenient: true, - fields: ['*'], - }, - }, - ], - }, - }, + it('searches for specified type/s', () => { + const test = (typeOrTypes: string | string[]) => { + const result = getQueryParams({ + mappings, + registry, + type: typeOrTypes, + }); + const type = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; + expectResult(result, ...type); + }; + for (const typeOrTypes of ALL_TYPE_SUBSETS) { + test(typeOrTypes); + } }); }); - }); - describe('search, searchFields', () => { - it('includes all types for field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: undefined, - search: 'y*', - searchFields: ['title'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending'), - createTypeClause('saved'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['pending.title', 'saved.title', 'global.title'], - }, - }, - ], - }, - }, - }); - }); - it('supports field boosting', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: undefined, - search: 'y*', - searchFields: ['title^3'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending'), - createTypeClause('saved'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['pending.title^3', 'saved.title^3', 'global.title^3'], - }, - }, - ], - }, - }, + describe('`namespace` parameter', () => { + const createTypeClause = (type: string, namespace?: string) => { + if (registry.isMultiNamespace(type)) { + return { + bool: { + must: expect.arrayContaining([{ term: { namespaces: namespace ?? 'default' } }]), + must_not: [{ exists: { field: 'namespace' } }], + }, + }; + } else if (namespace && registry.isSingleNamespace(type)) { + return { + bool: { + must: expect.arrayContaining([{ term: { namespace } }]), + must_not: [{ exists: { field: 'namespaces' } }], + }, + }; + } + // isNamespaceAgnostic + return { + bool: expect.objectContaining({ + must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }], + }), + }; + }; + + const expectResult = (result: Result, ...typeClauses: any) => { + expect(result.query.bool.filter).toEqual( + expect.arrayContaining([ + { bool: expect.objectContaining({ should: typeClauses, minimum_should_match: 1 }) }, + ]) + ); + }; + + const test = (namespace?: string) => { + for (const typeOrTypes of ALL_TYPE_SUBSETS) { + const result = getQueryParams({ mappings, registry, type: typeOrTypes, namespace }); + const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; + expectResult(result, ...types.map(x => createTypeClause(x, namespace))); + } + // also test with no specified type/s + const result = getQueryParams({ mappings, registry, type: undefined, namespace }); + expectResult(result, ...ALL_TYPES.map(x => createTypeClause(x, namespace))); + }; + + it('filters results with "namespace" field when `namespace` is not specified', () => { + test(undefined); }); - }); - it('supports field and multi-field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: undefined, - search: 'y*', - searchFields: ['title', 'title.raw'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending'), - createTypeClause('saved'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: [ - 'pending.title', - 'saved.title', - 'global.title', - 'pending.title.raw', - 'saved.title.raw', - 'global.title.raw', - ], - }, - }, - ], - }, - }, + + it('filters results for specified namespace for appropriate type/s', () => { + test('foo-namespace'); }); }); }); - describe('namespace, search, searchFields', () => { - it('includes all types for field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + describe('search clause (query.bool.must.simple_query_string)', () => { + const search = 'foo*'; + + const expectResult = (result: Result, sqsClause: any) => { + expect(result.query.bool.must).toEqual([{ simple_query_string: sqsClause }]); + }; + + describe('`search` parameter', () => { + it('does not include clause when `search` is not specified', () => { + const result = getQueryParams({ + mappings, registry, - namespace: 'foo-namespace', - type: undefined, - search: 'y*', - searchFields: ['title'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending', 'foo-namespace'), - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['pending.title', 'saved.title', 'global.title'], - }, - }, - ], - }, - }, + search: undefined, + }); + expect(result.query.bool.must).toBeUndefined(); }); - }); - it('supports field boosting', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + + it('creates a clause with query for specified search', () => { + const result = getQueryParams({ + mappings, registry, - namespace: 'foo-namespace', - type: undefined, - search: 'y*', - searchFields: ['title^3'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending', 'foo-namespace'), - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['pending.title^3', 'saved.title^3', 'global.title^3'], - }, - }, - ], - }, - }, + search, + }); + expectResult(result, expect.objectContaining({ query: search })); }); }); - it('supports field and multi-field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + + describe('`searchFields` parameter', () => { + const getExpectedFields = (searchFields: string[], typeOrTypes: string | string[]) => { + const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; + return searchFields.map(x => types.map(y => `${y}.${x}`)).flat(); + }; + + const test = (searchFields: string[]) => { + for (const typeOrTypes of ALL_TYPE_SUBSETS) { + const result = getQueryParams({ + mappings, + registry, + type: typeOrTypes, + search, + searchFields, + }); + const fields = getExpectedFields(searchFields, typeOrTypes); + expectResult(result, expect.objectContaining({ fields })); + } + // also test with no specified type/s + const result = getQueryParams({ + mappings, registry, - namespace: 'foo-namespace', type: undefined, - search: 'y*', - searchFields: ['title', 'title.raw'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending', 'foo-namespace'), - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: [ - 'pending.title', - 'saved.title', - 'global.title', - 'pending.title.raw', - 'saved.title.raw', - 'global.title.raw', - ], - }, - }, - ], - }, - }, - }); - }); - }); + search, + searchFields, + }); + const fields = getExpectedFields(searchFields, ALL_TYPES); + expectResult(result, expect.objectContaining({ fields })); + }; - describe('type (plural, namespaced and global), search, searchFields', () => { - it('includes all types for field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: ['saved', 'global'], - search: 'y*', - searchFields: ['title'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['saved.title', 'global.title'], - }, - }, - ], - }, - }, - }); - }); - it('supports field boosting', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: ['saved', 'global'], - search: 'y*', - searchFields: ['title^3'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['saved.title^3', 'global.title^3'], - }, - }, - ], - }, - }, - }); - }); - it('supports field and multi-field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + it('includes lenient flag and all fields when `searchFields` is not specified', () => { + const result = getQueryParams({ + mappings, registry, - namespace: undefined, - type: ['saved', 'global'], - search: 'y*', - searchFields: ['title', 'title.raw'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['saved.title', 'global.title', 'saved.title.raw', 'global.title.raw'], - }, - }, - ], - }, - }, + search, + searchFields: undefined, + }); + expectResult(result, expect.objectContaining({ lenient: true, fields: ['*'] })); }); - }); - }); - describe('namespace, type (plural, namespaced and global), search, searchFields', () => { - it('includes all types for field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - search: 'y*', - searchFields: ['title'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['saved.title', 'global.title'], - }, - }, - ], - }, - }, - }); - }); - it('supports field boosting', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - search: 'y*', - searchFields: ['title^3'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['saved.title^3', 'global.title^3'], - }, - }, - ], - }, - }, - }); - }); - it('supports field and multi-field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - search: 'y*', - searchFields: ['title', 'title.raw'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['saved.title', 'global.title', 'saved.title.raw', 'global.title.raw'], - }, - }, - ], - }, - }, + it('includes specified search fields for appropriate type/s', () => { + test(['title']); }); - }); - }); - describe('type (plural, namespaced and global), search, defaultSearchOperator', () => { - it('supports defaultSearchOperator', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - search: 'foo', - searchFields: undefined, - defaultSearchOperator: 'AND', - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - minimum_should_match: 1, - should: [ - { - bool: { - must: [ - { - term: { - type: 'saved', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'global', - }, - }, - ], - must_not: [ - { - exists: { - field: 'namespace', - }, - }, - ], - }, - }, - ], - }, - }, - ], - must: [ - { - simple_query_string: { - lenient: true, - fields: ['*'], - default_operator: 'AND', - query: 'foo', - }, - }, - ], - }, - }, + it('supports boosting', () => { + test(['title^3']); }); - }); - }); - describe('type (plural, namespaced and global), hasReference', () => { - it('supports hasReference', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - search: undefined, - searchFields: undefined, - defaultSearchOperator: 'OR', - hasReference: { - type: 'bar', - id: '1', - }, - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - must: [ - { - nested: { - path: 'references', - query: { - bool: { - must: [ - { - term: { - 'references.id': '1', - }, - }, - { - term: { - 'references.type': 'bar', - }, - }, - ], - }, - }, - }, - }, - ], - should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - }, - }, + it('supports multiple fields', () => { + test(['title, title.raw']); }); }); - }); - describe('type filter', () => { - it(' with namespace', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - kueryNode: { - type: 'function', - function: 'is', - arguments: [ - { type: 'literal', value: 'global.name' }, - { type: 'literal', value: 'GLOBAL' }, - { type: 'literal', value: false }, - ], - }, - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - match: { - 'global.name': 'GLOBAL', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - bool: { - must: [ - { - term: { - type: 'pending', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'saved', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'global', - }, - }, - ], - must_not: [ - { - exists: { - field: 'namespace', - }, - }, - ], - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - }); - }); - it(' with namespace and more complex filter', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + describe('`defaultSearchOperator` parameter', () => { + it('does not include default_operator when `defaultSearchOperator` is not specified', () => { + const result = getQueryParams({ + mappings, registry, - namespace: 'foo-namespace', - kueryNode: { - type: 'function', - function: 'and', - arguments: [ - { - type: 'function', - function: 'is', - arguments: [ - { type: 'literal', value: 'global.name' }, - { type: 'literal', value: 'GLOBAL' }, - { type: 'literal', value: false }, - ], - }, - { - type: 'function', - function: 'not', - arguments: [ - { - type: 'function', - function: 'is', - arguments: [ - { type: 'literal', value: 'saved.obj.key1' }, - { type: 'literal', value: 'key' }, - { type: 'literal', value: true }, - ], - }, - ], - }, - ], - }, - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - filter: [ - { - bool: { - should: [ - { - match: { - 'global.name': 'GLOBAL', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - must_not: { - bool: { - should: [ - { - match_phrase: { - 'saved.obj.key1': 'key', - }, - }, - ], - minimum_should_match: 1, - }, - }, - }, - }, - ], - }, - }, - { - bool: { - should: [ - { - bool: { - must: [ - { - term: { - type: 'pending', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'saved', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'global', - }, - }, - ], - must_not: [ - { - exists: { - field: 'namespace', - }, - }, - ], - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, + search, + defaultSearchOperator: undefined, + }); + expectResult(result, expect.not.objectContaining({ default_operator: expect.anything() })); }); - }); - it(' with search and searchFields', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + + it('includes specified default operator', () => { + const defaultSearchOperator = 'AND'; + const result = getQueryParams({ + mappings, registry, - namespace: 'foo-namespace', - search: 'y*', - searchFields: ['title'], - kueryNode: { - type: 'function', - function: 'is', - arguments: [ - { type: 'literal', value: 'global.name' }, - { type: 'literal', value: 'GLOBAL' }, - { type: 'literal', value: false }, - ], - }, - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - match: { - 'global.name': 'GLOBAL', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - bool: { - must: [ - { - term: { - type: 'pending', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'saved', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'global', - }, - }, - ], - must_not: [ - { - exists: { - field: 'namespace', - }, - }, - ], - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['pending.title', 'saved.title', 'global.title'], - }, - }, - ], - }, - }, + search, + defaultSearchOperator, + }); + expectResult(result, expect.objectContaining({ default_operator: defaultSearchOperator })); }); }); }); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index e6c06208ca1a5..967ce8bceaf84 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -66,18 +66,26 @@ function getClauseForType( namespace: string | undefined, type: string ) { - if (namespace && !registry.isNamespaceAgnostic(type)) { + if (registry.isMultiNamespace(type)) { + return { + bool: { + must: [{ term: { type } }, { term: { namespaces: namespace ?? 'default' } }], + must_not: [{ exists: { field: 'namespace' } }], + }, + }; + } else if (namespace && registry.isSingleNamespace(type)) { return { bool: { must: [{ term: { type } }, { term: { namespace } }], + must_not: [{ exists: { field: 'namespaces' } }], }, }; } - + // isSingleNamespace in the default namespace, or isNamespaceAgnostic return { bool: { must: [{ term: { type } }], - must_not: [{ exists: { field: 'namespace' } }], + must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }], }, }; } diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts index c6de9fa94291c..b209c9ca54f63 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -31,6 +31,8 @@ const create = () => find: jest.fn(), get: jest.fn(), update: jest.fn(), + addToNamespaces: jest.fn(), + deleteFromNamespaces: jest.fn(), } as unknown) as jest.Mocked); export const savedObjectsClientMock = { create }; diff --git a/src/core/server/saved_objects/service/saved_objects_client.test.js b/src/core/server/saved_objects/service/saved_objects_client.test.js index 1794d9ae86c17..53bb31369adbf 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.test.js +++ b/src/core/server/saved_objects/service/saved_objects_client.test.js @@ -147,3 +147,37 @@ test(`#bulkUpdate`, async () => { }); expect(result).toBe(returnValue); }); + +test(`#addToNamespaces`, async () => { + const returnValue = Symbol(); + const mockRepository = { + addToNamespaces: jest.fn().mockResolvedValue(returnValue), + }; + const client = new SavedObjectsClient(mockRepository); + + const type = Symbol(); + const id = Symbol(); + const namespaces = Symbol(); + const options = Symbol(); + const result = await client.addToNamespaces(type, id, namespaces, options); + + expect(mockRepository.addToNamespaces).toHaveBeenCalledWith(type, id, namespaces, options); + expect(result).toBe(returnValue); +}); + +test(`#deleteFromNamespaces`, async () => { + const returnValue = Symbol(); + const mockRepository = { + deleteFromNamespaces: jest.fn().mockResolvedValue(returnValue), + }; + const client = new SavedObjectsClient(mockRepository); + + const type = Symbol(); + const id = Symbol(); + const namespaces = Symbol(); + const options = Symbol(); + const result = await client.deleteFromNamespaces(type, id, namespaces, options); + + expect(mockRepository.deleteFromNamespaces).toHaveBeenCalledWith(type, id, namespaces, options); + expect(result).toBe(returnValue); +}); diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 70d69374ba8fe..8780f07cc3091 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -107,6 +107,26 @@ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { refresh?: MutatingOperationRefreshSetting; } +/** + * + * @public + */ +export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions { + /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ + version?: string; + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; +} + +/** + * + * @public + */ +export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions { + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; +} + /** * * @public @@ -270,6 +290,40 @@ export class SavedObjectsClient { return await this._repository.update(type, id, attributes, options); } + /** + * Adds namespaces to a SavedObject + * + * @param type + * @param id + * @param namespaces + * @param options + */ + async addToNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsAddToNamespacesOptions = {} + ): Promise<{}> { + return await this._repository.addToNamespaces(type, id, namespaces, options); + } + + /** + * Removes namespaces from a SavedObject + * + * @param type + * @param id + * @param namespaces + * @param options + */ + async deleteFromNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsDeleteFromNamespacesOptions = {} + ): Promise<{}> { + return await this._repository.deleteFromNamespaces(type, id, namespaces, options); + } + /** * Bulk Updates multiple SavedObject at once * diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index f14e9d9efb5e3..9efc82603b179 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -172,6 +172,19 @@ export type MutatingOperationRefreshSetting = boolean | 'wait_for'; */ export type SavedObjectsClientContract = Pick; +/** + * The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: + * * single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. + * * multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. + * * agnostic: this type of saved object is global. + * + * Note: do not write logic that uses this value directly; instead, use the appropriate accessors in the + * {@link SavedObjectTypeRegistry | type registry}. + * + * @public + */ +export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'agnostic'; + /** * @remarks This is only internal for now, and will only be public when we expose the registerType API * @@ -190,9 +203,14 @@ export interface SavedObjectsType { */ hidden: boolean; /** - * Is the type global (true), or namespaced (false). + * Is the type global (true), or not (false). + * @deprecated Use `namespaceType` instead. + */ + namespaceAgnostic?: boolean; + /** + * The {@link SavedObjectsNamespaceType | namespace type} for the type. */ - namespaceAgnostic: boolean; + namespaceType?: SavedObjectsNamespaceType; /** * If defined, the type instances will be stored in the given index instead of the default one. */ @@ -329,6 +347,8 @@ export type SavedObjectLegacyMigrationFn = ( */ interface SavedObjectsLegacyTypeSchema { isNamespaceAgnostic?: boolean; + /** Cannot be used in conjunction with `isNamespaceAgnostic` */ + multiNamespace?: boolean; hidden?: boolean; indexPattern?: ((config: LegacyConfig) => string) | string; convertToAliasScript?: string; diff --git a/src/core/server/saved_objects/utils.test.ts b/src/core/server/saved_objects/utils.test.ts index 0719fe7138e8a..64bdf1771decc 100644 --- a/src/core/server/saved_objects/utils.test.ts +++ b/src/core/server/saved_objects/utils.test.ts @@ -84,6 +84,16 @@ describe('convertLegacyTypes', () => { }, { pluginId: 'pluginB', + properties: { + typeB: { + properties: { + fieldB: { type: 'text' }, + }, + }, + }, + }, + { + pluginId: 'pluginC', properties: { typeC: { properties: { @@ -92,6 +102,16 @@ describe('convertLegacyTypes', () => { }, }, }, + { + pluginId: 'pluginD', + properties: { + typeD: { + properties: { + fieldD: { type: 'text' }, + }, + }, + }, + }, ], savedObjectMigrations: {}, savedObjectSchemas: { @@ -100,6 +120,18 @@ describe('convertLegacyTypes', () => { hidden: true, isNamespaceAgnostic: true, }, + typeB: { + indexPattern: 'barBaz', + hidden: false, + multiNamespace: true, + }, + typeD: { + indexPattern: 'bazQux', + hidden: false, + // if both isNamespaceAgnostic and multiNamespace are true, the resulting namespaceType is 'agnostic' + isNamespaceAgnostic: true, + multiNamespace: true, + }, }, savedObjectValidations: {}, savedObjectsManagement: {}, @@ -372,29 +404,56 @@ describe('convertTypesToLegacySchema', () => { { name: 'typeA', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: {} }, convertToAliasScript: 'some script', }, { name: 'typeB', hidden: true, - namespaceAgnostic: false, + namespaceType: 'single', indexPattern: 'myIndex', mappings: { properties: {} }, }, + { + name: 'typeC', + hidden: false, + namespaceType: 'multiple', + mappings: { properties: {} }, + }, + // deprecated test case + { + name: 'typeD', + hidden: false, + namespaceAgnostic: true, + namespaceType: 'multiple', // if namespaceAgnostic and namespaceType are both set, namespaceAgnostic takes precedence + mappings: { properties: {} }, + }, ]; expect(convertTypesToLegacySchema(types)).toEqual({ typeA: { hidden: false, isNamespaceAgnostic: true, + multiNamespace: false, convertToAliasScript: 'some script', }, typeB: { hidden: true, isNamespaceAgnostic: false, + multiNamespace: false, indexPattern: 'myIndex', }, + typeC: { + hidden: false, + isNamespaceAgnostic: false, + multiNamespace: true, + }, + // deprecated test case + typeD: { + hidden: false, + isNamespaceAgnostic: true, + multiNamespace: false, + }, }); }); }); diff --git a/src/core/server/saved_objects/utils.ts b/src/core/server/saved_objects/utils.ts index ea90efd8b9fbd..5348963812629 100644 --- a/src/core/server/saved_objects/utils.ts +++ b/src/core/server/saved_objects/utils.ts @@ -20,6 +20,7 @@ import { LegacyConfig } from '../legacy'; import { SavedObjectMigrationMap } from './migrations'; import { + SavedObjectsNamespaceType, SavedObjectsType, SavedObjectsLegacyUiExports, SavedObjectLegacyMigrationMap, @@ -48,10 +49,15 @@ export const convertLegacyTypes = ( const schema = savedObjectSchemas[type]; const migrations = savedObjectMigrations[type]; const management = savedObjectsManagement[type]; + const namespaceType = (schema?.isNamespaceAgnostic + ? 'agnostic' + : schema?.multiNamespace + ? 'multiple' + : 'single') as SavedObjectsNamespaceType; return { name: type, hidden: schema?.hidden ?? false, - namespaceAgnostic: schema?.isNamespaceAgnostic ?? false, + namespaceType, mappings, indexPattern: typeof schema?.indexPattern === 'function' @@ -76,7 +82,8 @@ export const convertTypesToLegacySchema = ( return { ...schema, [type.name]: { - isNamespaceAgnostic: type.namespaceAgnostic, + isNamespaceAgnostic: type.namespaceAgnostic || type.namespaceType === 'agnostic', + multiNamespace: !type.namespaceAgnostic && type.namespaceType === 'multiple', hidden: type.hidden, indexPattern: type.indexPattern, convertToAliasScript: type.convertToAliasScript, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index f3e3b7736d8d3..a35bca7375286 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1003,7 +1003,7 @@ export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolea export type ISavedObjectsRepository = Pick; // @public -export type ISavedObjectTypeRegistry = Pick; +export type ISavedObjectTypeRegistry = Omit; // @public export type IScopedClusterClient = Pick; @@ -1643,6 +1643,7 @@ export interface SavedObject { }; id: string; migrationVersion?: SavedObjectsMigrationVersion; + namespaces?: string[]; references: SavedObjectReference[]; type: string; updated_at?: string; @@ -1687,6 +1688,12 @@ export interface SavedObjectReference { type: string; } +// @public (undocumented) +export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions { + refresh?: MutatingOperationRefreshSetting; + version?: string; +} + // Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts // @@ -1754,11 +1761,13 @@ export interface SavedObjectsBulkUpdateResponse { export class SavedObjectsClient { // @internal constructor(repository: ISavedObjectsRepository); + addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>; bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; + deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; // (undocumented) static errors: typeof SavedObjectsErrorHelpers; // (undocumented) @@ -1839,6 +1848,11 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp refresh?: MutatingOperationRefreshSetting; } +// @public (undocumented) +export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions { + refresh?: MutatingOperationRefreshSetting; +} + // @public (undocumented) export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions { refresh?: MutatingOperationRefreshSetting; @@ -1849,6 +1863,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static createBadRequestError(reason?: string): DecoratedError; // (undocumented) + static createConflictError(type: string, id: string): DecoratedError; + // (undocumented) static createEsAutoCreateIndexError(): DecoratedError; // (undocumented) static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; @@ -1861,6 +1877,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static decorateConflictError(error: Error, reason?: string): DecoratedError; // (undocumented) + static decorateEsCannotExecuteScriptError(error: Error, reason?: string): DecoratedError; + // (undocumented) static decorateEsUnavailableError(error: Error, reason?: string): DecoratedError; // (undocumented) static decorateForbiddenError(error: Error, reason?: string): DecoratedError; @@ -1877,6 +1895,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static isEsAutoCreateIndexError(error: Error | DecoratedError): boolean; // (undocumented) + static isEsCannotExecuteScriptError(error: Error | DecoratedError): boolean; + // (undocumented) static isEsUnavailableError(error: Error | DecoratedError): boolean; // (undocumented) static isForbiddenError(error: Error | DecoratedError): boolean; @@ -2106,6 +2126,9 @@ export interface SavedObjectsMigrationVersion { [pluginName: string]: string; } +// @public +export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'agnostic'; + // @public export interface SavedObjectsRawDoc { // (undocumented) @@ -2124,6 +2147,7 @@ export interface SavedObjectsRawDoc { // @public (undocumented) export class SavedObjectsRepository { + addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>; bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; @@ -2134,6 +2158,7 @@ export class SavedObjectsRepository { static createRepository(migrator: KibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, callCluster: APICaller, extraTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; + deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; // (undocumented) find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; @@ -2175,7 +2200,11 @@ export class SavedObjectsSchema { // (undocumented) isHiddenType(type: string): boolean; // (undocumented) + isMultiNamespace(type: string): boolean; + // (undocumented) isNamespaceAgnostic(type: string): boolean; + // (undocumented) + isSingleNamespace(type: string): boolean; } // @public @@ -2224,7 +2253,9 @@ export interface SavedObjectsType { mappings: SavedObjectsTypeMappingDefinition; migrations?: SavedObjectMigrationMap; name: string; - namespaceAgnostic: boolean; + // @deprecated + namespaceAgnostic?: boolean; + namespaceType?: SavedObjectsNamespaceType; } // @public @@ -2269,7 +2300,9 @@ export class SavedObjectTypeRegistry { getType(type: string): SavedObjectsType | undefined; isHidden(type: string): boolean; isImportableAndExportable(type: string): boolean; + isMultiNamespace(type: string): boolean; isNamespaceAgnostic(type: string): boolean; + isSingleNamespace(type: string): boolean; registerType(type: SavedObjectsType): void; } diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index d3faab6c557cd..04aaacc3cf31a 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -96,4 +96,6 @@ export interface SavedObject { references: SavedObjectReference[]; /** {@inheritdoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; + /** Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. */ + namespaces?: string[]; } diff --git a/test/api_integration/apis/saved_objects/bulk_create.js b/test/api_integration/apis/saved_objects/bulk_create.js index afa5153336ff7..2d77fdf266793 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.js +++ b/test/api_integration/apis/saved_objects/bulk_create.js @@ -58,7 +58,9 @@ export default function({ getService }) { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', error: { - message: 'version conflict, document already exists', + error: 'Conflict', + message: + 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] conflict', statusCode: 409, }, }, diff --git a/test/api_integration/apis/saved_objects/bulk_get.js b/test/api_integration/apis/saved_objects/bulk_get.js index 984ac781d3e46..67e511f2bf548 100644 --- a/test/api_integration/apis/saved_objects/bulk_get.js +++ b/test/api_integration/apis/saved_objects/bulk_get.js @@ -80,8 +80,9 @@ export default function({ getService }) { id: 'does not exist', type: 'dashboard', error: { + error: 'Not Found', + message: 'Saved object [dashboard/does not exist] not found', statusCode: 404, - message: 'Not found', }, }, { @@ -123,24 +124,28 @@ export default function({ getService }) { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', type: 'visualization', error: { + error: 'Not Found', + message: + 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found', statusCode: 404, - message: 'Not found', }, }, { id: 'does not exist', type: 'dashboard', error: { + error: 'Not Found', + message: 'Saved object [dashboard/does not exist] not found', statusCode: 404, - message: 'Not found', }, }, { id: '7.0.0-alpha1', type: 'config', error: { + error: 'Not Found', + message: 'Saved object [config/7.0.0-alpha1] not found', statusCode: 404, - message: 'Not found', }, }, ], diff --git a/test/api_integration/apis/saved_objects/export.js b/test/api_integration/apis/saved_objects/export.js index fc9ab8140869c..9558e82880deb 100644 --- a/test/api_integration/apis/saved_objects/export.js +++ b/test/api_integration/apis/saved_objects/export.js @@ -170,8 +170,9 @@ export default function({ getService }) { id: '1', type: 'dashboard', error: { + error: 'Not Found', + message: 'Saved object [dashboard/1] not found', statusCode: 404, - message: 'Not found', }, }, ], diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index f691e62b9352a..2caf3a7055df9 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -10,14 +10,16 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { EncryptedSavedObjectsService } from '../crypto'; import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, savedObjectsTypeRegistryMock } from 'src/core/server/mocks'; import { encryptedSavedObjectsServiceMock } from '../crypto/index.mock'; let wrapper: EncryptedSavedObjectsClientWrapper; let mockBaseClient: jest.Mocked; +let mockBaseTypeRegistry: ReturnType; let encryptedSavedObjectsServiceMockInstance: jest.Mocked; beforeEach(() => { mockBaseClient = savedObjectsClientMock.create(); + mockBaseTypeRegistry = savedObjectsTypeRegistryMock.create(); encryptedSavedObjectsServiceMockInstance = encryptedSavedObjectsServiceMock.create([ { type: 'known-type', @@ -28,6 +30,7 @@ beforeEach(() => { wrapper = new EncryptedSavedObjectsClientWrapper({ service: encryptedSavedObjectsServiceMockInstance, baseClient: mockBaseClient, + baseTypeRegistry: mockBaseTypeRegistry, } as any); }); @@ -91,35 +94,50 @@ describe('#create', () => { ); }); - it('uses `namespace` to encrypt attributes if it is specified', async () => { - const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; - const options = { overwrite: true, namespace: 'some-namespace' }; - const mockedResponse = { - id: 'uuid-v4-id', - type: 'known-type', - attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, - references: [], - }; + describe('namespace', () => { + const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => { + const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; + const options = { overwrite: true, namespace }; + const mockedResponse = { + id: 'uuid-v4-id', + type: 'known-type', + attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, + references: [], + }; - mockBaseClient.create.mockResolvedValue(mockedResponse); + mockBaseClient.create.mockResolvedValue(mockedResponse); - expect(await wrapper.create('known-type', attributes, options)).toEqual({ - ...mockedResponse, - attributes: { attrOne: 'one', attrThree: 'three' }, - }); + expect(await wrapper.create('known-type', attributes, options)).toEqual({ + ...mockedResponse, + attributes: { attrOne: 'one', attrThree: 'three' }, + }); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( - { type: 'known-type', id: 'uuid-v4-id', namespace: 'some-namespace' }, - { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } - ); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( + { + type: 'known-type', + id: 'uuid-v4-id', + namespace: expectNamespaceInDescriptor ? namespace : undefined, + }, + { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } + ); - expect(mockBaseClient.create).toHaveBeenCalledTimes(1); - expect(mockBaseClient.create).toHaveBeenCalledWith( - 'known-type', - { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, - { id: 'uuid-v4-id', overwrite: true, namespace: 'some-namespace' } - ); + expect(mockBaseClient.create).toHaveBeenCalledTimes(1); + expect(mockBaseClient.create).toHaveBeenCalledWith( + 'known-type', + { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, + { id: 'uuid-v4-id', overwrite: true, namespace } + ); + }; + + it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => { + await doTest('some-namespace', true); + }); + + it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => { + mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); + await doTest('some-namespace', false); + }); }); it('fails if base client fails', async () => { @@ -190,14 +208,13 @@ describe('#bulkCreate', () => { it('fails if ID is specified for registered type', async () => { const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; - const options = { namespace: 'some-namespace' }; const bulkCreateParams = [ { id: 'some-id', type: 'known-type', attributes }, { type: 'unknown-type', attributes }, ]; - await expect(wrapper.bulkCreate(bulkCreateParams, options)).rejects.toThrowError( + await expect(wrapper.bulkCreate(bulkCreateParams)).rejects.toThrowError( 'Predefined IDs are not allowed for saved objects with encrypted attributes.' ); @@ -257,39 +274,57 @@ describe('#bulkCreate', () => { ); }); - it('uses `namespace` to encrypt attributes if it is specified', async () => { - const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; - const options = { namespace: 'some-namespace' }; - const mockedResponse = { - saved_objects: [{ id: 'uuid-v4-id', type: 'known-type', attributes, references: [] }], - }; + describe('namespace', () => { + const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => { + const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; + const options = { namespace }; + const mockedResponse = { + saved_objects: [{ id: 'uuid-v4-id', type: 'known-type', attributes, references: [] }], + }; + + mockBaseClient.bulkCreate.mockResolvedValue(mockedResponse); + + const bulkCreateParams = [{ type: 'known-type', attributes }]; + await expect(wrapper.bulkCreate(bulkCreateParams, options)).resolves.toEqual({ + saved_objects: [ + { + ...mockedResponse.saved_objects[0], + attributes: { attrOne: 'one', attrThree: 'three' }, + }, + ], + }); - mockBaseClient.bulkCreate.mockResolvedValue(mockedResponse); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( + { + type: 'known-type', + id: 'uuid-v4-id', + namespace: expectNamespaceInDescriptor ? namespace : undefined, + }, + { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } + ); + + expect(mockBaseClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(mockBaseClient.bulkCreate).toHaveBeenCalledWith( + [ + { + ...bulkCreateParams[0], + id: 'uuid-v4-id', + attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, + }, + ], + options + ); + }; - const bulkCreateParams = [{ type: 'known-type', attributes }]; - await expect(wrapper.bulkCreate(bulkCreateParams, options)).resolves.toEqual({ - saved_objects: [ - { ...mockedResponse.saved_objects[0], attributes: { attrOne: 'one', attrThree: 'three' } }, - ], + it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => { + await doTest('some-namespace', true); }); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( - { type: 'known-type', id: 'uuid-v4-id', namespace: 'some-namespace' }, - { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } - ); - - expect(mockBaseClient.bulkCreate).toHaveBeenCalledTimes(1); - expect(mockBaseClient.bulkCreate).toHaveBeenCalledWith( - [ - { - ...bulkCreateParams[0], - id: 'uuid-v4-id', - attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, - }, - ], - options - ); + it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => { + mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); + await doTest('some-namespace', false); + }); }); it('fails if base client fails', async () => { @@ -432,63 +467,79 @@ describe('#bulkUpdate', () => { ); }); - it('uses `namespace` to encrypt attributes if it is specified', async () => { - const docs = [ - { - id: 'some-id', - type: 'known-type', - attributes: { - attrOne: 'one', - attrSecret: 'secret', - attrThree: 'three', - }, - version: 'some-version', - }, - ]; - - mockBaseClient.bulkUpdate.mockResolvedValue({ - saved_objects: docs.map(doc => ({ ...doc, references: undefined })), - }); - - await expect(wrapper.bulkUpdate(docs, { namespace: 'some-namespace' })).resolves.toEqual({ - saved_objects: [ + describe('namespace', () => { + const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => { + const docs = [ { id: 'some-id', type: 'known-type', attributes: { attrOne: 'one', + attrSecret: 'secret', attrThree: 'three', }, version: 'some-version', - references: undefined, }, - ], - }); - - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( - { type: 'known-type', id: 'some-id', namespace: 'some-namespace' }, - { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } - ); + ]; + const options = { namespace }; + + mockBaseClient.bulkUpdate.mockResolvedValue({ + saved_objects: docs.map(doc => ({ ...doc, references: undefined })), + }); + + await expect(wrapper.bulkUpdate(docs, options)).resolves.toEqual({ + saved_objects: [ + { + id: 'some-id', + type: 'known-type', + attributes: { + attrOne: 'one', + attrThree: 'three', + }, + version: 'some-version', + references: undefined, + }, + ], + }); - expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1); - expect(mockBaseClient.bulkUpdate).toHaveBeenCalledWith( - [ + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( { - id: 'some-id', type: 'known-type', - attributes: { - attrOne: 'one', - attrSecret: '*secret*', - attrThree: 'three', + id: 'some-id', + namespace: expectNamespaceInDescriptor ? namespace : undefined, + }, + { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } + ); + + expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1); + expect(mockBaseClient.bulkUpdate).toHaveBeenCalledWith( + [ + { + id: 'some-id', + type: 'known-type', + attributes: { + attrOne: 'one', + attrSecret: '*secret*', + attrThree: 'three', + }, + version: 'some-version', + + references: undefined, }, - version: 'some-version', + ], + options + ); + }; - references: undefined, - }, - ], - { namespace: 'some-namespace' } - ); + it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => { + await doTest('some-namespace', true); + }); + + it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => { + mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); + await doTest('some-namespace', false); + }); }); it('fails if base client fails', async () => { @@ -871,31 +922,46 @@ describe('#update', () => { ); }); - it('uses `namespace` to encrypt attributes if it is specified', async () => { - const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; - const options = { version: 'some-version', namespace: 'some-namespace' }; - const mockedResponse = { id: 'some-id', type: 'known-type', attributes, references: [] }; + describe('namespace', () => { + const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => { + const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; + const options = { version: 'some-version', namespace }; + const mockedResponse = { id: 'some-id', type: 'known-type', attributes, references: [] }; - mockBaseClient.update.mockResolvedValue(mockedResponse); + mockBaseClient.update.mockResolvedValue(mockedResponse); - await expect(wrapper.update('known-type', 'some-id', attributes, options)).resolves.toEqual({ - ...mockedResponse, - attributes: { attrOne: 'one', attrThree: 'three' }, - }); + await expect(wrapper.update('known-type', 'some-id', attributes, options)).resolves.toEqual({ + ...mockedResponse, + attributes: { attrOne: 'one', attrThree: 'three' }, + }); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( - { type: 'known-type', id: 'some-id', namespace: 'some-namespace' }, - { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } - ); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( + { + type: 'known-type', + id: 'some-id', + namespace: expectNamespaceInDescriptor ? namespace : undefined, + }, + { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } + ); + + expect(mockBaseClient.update).toHaveBeenCalledTimes(1); + expect(mockBaseClient.update).toHaveBeenCalledWith( + 'known-type', + 'some-id', + { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, + options + ); + }; - expect(mockBaseClient.update).toHaveBeenCalledTimes(1); - expect(mockBaseClient.update).toHaveBeenCalledWith( - 'known-type', - 'some-id', - { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, - options - ); + it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => { + await doTest('some-namespace', true); + }); + + it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => { + mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); + await doTest('some-namespace', false); + }); }); it('fails if base client fails', async () => { diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index b4f1de52c9ce3..e8197536d29d9 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -19,11 +19,15 @@ import { SavedObjectsFindResponse, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, + SavedObjectsAddToNamespacesOptions, + SavedObjectsDeleteFromNamespacesOptions, + ISavedObjectTypeRegistry, } from 'src/core/server'; import { EncryptedSavedObjectsService } from '../crypto'; interface EncryptedSavedObjectsClientOptions { baseClient: SavedObjectsClientContract; + baseTypeRegistry: ISavedObjectTypeRegistry; service: Readonly; } @@ -41,6 +45,10 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon public readonly errors = options.baseClient.errors ) {} + // only include namespace in AAD descriptor if the specified type is single-namespace + private getDescriptorNamespace = (type: string, namespace?: string) => + this.options.baseTypeRegistry.isSingleNamespace(type) ? namespace : undefined; + public async create( type: string, attributes: T = {} as T, @@ -60,11 +68,12 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon } const id = generateID(); + const namespace = this.getDescriptorNamespace(type, options.namespace); return this.stripEncryptedAttributesFromResponse( await this.options.baseClient.create( type, await this.options.service.encryptAttributes( - { type, id, namespace: options.namespace }, + { type, id, namespace }, attributes as Record ), { ...options, id } @@ -95,11 +104,12 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon } const id = generateID(); + const namespace = this.getDescriptorNamespace(object.type, options?.namespace); return { ...object, id, attributes: await this.options.service.encryptAttributes( - { type: object.type, id, namespace: options && options.namespace }, + { type: object.type, id, namespace }, object.attributes as Record ), } as SavedObjectsBulkCreateObject; @@ -124,10 +134,11 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon if (!this.options.service.isRegistered(type)) { return object; } + const namespace = this.getDescriptorNamespace(type, options?.namespace); return { ...object, attributes: await this.options.service.encryptAttributes( - { type, id, namespace: options && options.namespace }, + { type, id, namespace }, attributes ), }; @@ -173,20 +184,35 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon if (!this.options.service.isRegistered(type)) { return await this.options.baseClient.update(type, id, attributes, options); } - + const namespace = this.getDescriptorNamespace(type, options?.namespace); return this.stripEncryptedAttributesFromResponse( await this.options.baseClient.update( type, id, - await this.options.service.encryptAttributes( - { type, id, namespace: options && options.namespace }, - attributes - ), + await this.options.service.encryptAttributes({ type, id, namespace }, attributes), options ) ); } + public async addToNamespaces( + type: string, + id: string, + namespaces: string[], + options?: SavedObjectsAddToNamespacesOptions + ) { + return await this.options.baseClient.addToNamespaces(type, id, namespaces, options); + } + + public async deleteFromNamespaces( + type: string, + id: string, + namespaces: string[], + options?: SavedObjectsDeleteFromNamespacesOptions + ) { + return await this.options.baseClient.deleteFromNamespaces(type, id, namespaces, options); + } + /** * Strips encrypted attributes from any non-bulk Saved Objects API response. If type isn't * registered, response is returned as is. diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts index c76477cd8da43..10599ae3a1798 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts @@ -40,7 +40,8 @@ export function setupSavedObjects({ savedObjects.addClientWrapper( Number.MAX_SAFE_INTEGER, 'encryptedSavedObjects', - ({ client: baseClient }) => new EncryptedSavedObjectsClientWrapper({ baseClient, service }) + ({ client: baseClient, typeRegistry: baseTypeRegistry }) => + new EncryptedSavedObjectsClientWrapper({ baseClient, baseTypeRegistry, service }) ); const internalRepositoryPromise = getStartServices().then(([core]) => diff --git a/x-pack/plugins/security/server/audit/audit_logger.test.ts b/x-pack/plugins/security/server/audit/audit_logger.test.ts index 01cde02b7dfdd..f7ee210a21a74 100644 --- a/x-pack/plugins/security/server/audit/audit_logger.test.ts +++ b/x-pack/plugins/security/server/audit/audit_logger.test.ts @@ -18,25 +18,46 @@ describe(`#savedObjectsAuthorizationFailure`, () => { const username = 'foo-user'; const action = 'foo-action'; const types = ['foo-type-1', 'foo-type-2']; - const missing = [`saved_object:${types[0]}/foo-action`, `saved_object:${types[1]}/foo-action`]; + const spaceIds = ['foo-space', 'bar-space']; + const missing = [ + { + spaceId: 'foo-space', + privilege: `saved_object:${types[0]}/${action}`, + }, + { + spaceId: 'foo-space', + privilege: `saved_object:${types[1]}/${action}`, + }, + ]; const args = { foo: 'bar', baz: 'quz', }; - securityAuditLogger.savedObjectsAuthorizationFailure(username, action, types, missing, args); + securityAuditLogger.savedObjectsAuthorizationFailure( + username, + action, + types, + spaceIds, + missing, + args + ); expect(auditLogger.log).toHaveBeenCalledWith( 'saved_objects_authorization_failure', - expect.stringContaining(`${username} unauthorized to ${action}`), + expect.any(String), { username, action, types, + spaceIds, missing, args, } ); + expect(auditLogger.log.mock.calls[0][1]).toMatchInlineSnapshot( + `"foo-user unauthorized to [foo-action] [foo-type-1,foo-type-2] in [foo-space,bar-space]: missing [(foo-space)saved_object:foo-type-1/foo-action,(foo-space)saved_object:foo-type-2/foo-action]"` + ); }); }); @@ -47,22 +68,27 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { const username = 'foo-user'; const action = 'foo-action'; const types = ['foo-type-1', 'foo-type-2']; + const spaceIds = ['foo-space', 'bar-space']; const args = { foo: 'bar', baz: 'quz', }; - securityAuditLogger.savedObjectsAuthorizationSuccess(username, action, types, args); + securityAuditLogger.savedObjectsAuthorizationSuccess(username, action, types, spaceIds, args); expect(auditLogger.log).toHaveBeenCalledWith( 'saved_objects_authorization_success', - expect.stringContaining(`${username} authorized to ${action}`), + expect.any(String), { username, action, types, + spaceIds, args, } ); + expect(auditLogger.log.mock.calls[0][1]).toMatchInlineSnapshot( + `"foo-user authorized to [foo-action] [foo-type-1,foo-type-2] in [foo-space,bar-space]"` + ); }); }); diff --git a/x-pack/plugins/security/server/audit/audit_logger.ts b/x-pack/plugins/security/server/audit/audit_logger.ts index df8df35f97b49..40b525b5d2188 100644 --- a/x-pack/plugins/security/server/audit/audit_logger.ts +++ b/x-pack/plugins/security/server/audit/audit_logger.ts @@ -13,16 +13,23 @@ export class SecurityAuditLogger { username: string, action: string, types: string[], - missing: string[], + spaceIds: string[], + missing: Array<{ spaceId?: string; privilege: string }>, args?: Record ) { + const typesString = types.join(','); + const spacesString = spaceIds.length ? ` in [${spaceIds.join(',')}]` : ''; + const missingString = missing + .map(({ spaceId, privilege }) => `${spaceId ? `(${spaceId})` : ''}${privilege}`) + .join(','); this.getAuditLogger().log( 'saved_objects_authorization_failure', - `${username} unauthorized to ${action} ${types.join(',')}, missing ${missing.join(',')}`, + `${username} unauthorized to [${action}] [${typesString}]${spacesString}: missing [${missingString}]`, { username, action, types, + spaceIds, missing, args, } @@ -33,15 +40,19 @@ export class SecurityAuditLogger { username: string, action: string, types: string[], + spaceIds: string[], args?: Record ) { + const typesString = types.join(','); + const spacesString = spaceIds.length ? ` in [${spaceIds.join(',')}]` : ''; this.getAuditLogger().log( 'saved_objects_authorization_success', - `${username} authorized to ${action} ${types.join(',')}`, + `${username} authorized to [${action}] [${typesString}]${spacesString}`, { username, action, types, + spaceIds, args, } ); diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/check_privileges.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/check_privileges.test.ts.snap deleted file mode 100644 index 1212c2cd6a5cb..0000000000000 --- a/x-pack/plugins/security/server/authorization/__snapshots__/check_privileges.test.ts.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#atSpace throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; - -exports[`#atSpace with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because ["saved_object:bar-type/get" is not allowed]]]]`; - -exports[`#atSpace with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]`; - -exports[`#atSpaces throws error when Elasticsearch returns malformed response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`; - -exports[`#atSpaces throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; - -exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when an a space is missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; - -exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; - -exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when an extra space is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because ["space:space_3" is not allowed]]]`; - -exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; - -exports[`#globally throws error when Elasticsearch returns malformed response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`; - -exports[`#globally throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; - -exports[`#globally with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because ["saved_object:bar-type/get" is not allowed]]]]`; - -exports[`#globally with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]`; diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index 8c1241937892e..a64c5d509ca11 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -31,123 +31,121 @@ const createMockClusterClient = (response: any) => { }; describe('#atSpace', () => { - const checkPrivilegesAtSpaceTest = ( - description: string, - options: { - spaceId: string; - privilegeOrPrivileges: string | string[]; - esHasPrivilegesResponse: HasPrivilegesResponse; - expectedResult?: any; - expectErrorThrown?: any; + const checkPrivilegesAtSpaceTest = async (options: { + spaceId: string; + privilegeOrPrivileges: string | string[]; + esHasPrivilegesResponse: HasPrivilegesResponse; + }) => { + const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( + options.esHasPrivilegesResponse + ); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + mockActions, + mockClusterClient, + application + ); + const request = httpServerMock.createKibanaRequest(); + const checkPrivileges = checkPrivilegesWithRequest(request); + + let actualResult; + let errorThrown = null; + try { + actualResult = await checkPrivileges.atSpace(options.spaceId, options.privilegeOrPrivileges); + } catch (err) { + errorThrown = err; } - ) => { - test(description, async () => { - const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( - options.esHasPrivilegesResponse - ); - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( - mockActions, - mockClusterClient, - application - ); - const request = httpServerMock.createKibanaRequest(); - const checkPrivileges = checkPrivilegesWithRequest(request); - - let actualResult; - let errorThrown = null; - try { - actualResult = await checkPrivileges.atSpace( - options.spaceId, - options.privilegeOrPrivileges - ); - } catch (err) { - errorThrown = err; - } - expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.hasPrivileges', - { - body: { - applications: [ - { - application, - resources: [`space:${options.spaceId}`], - privileges: uniq([ - mockActions.version, - mockActions.login, - ...(Array.isArray(options.privilegeOrPrivileges) - ? options.privilegeOrPrivileges - : [options.privilegeOrPrivileges]), - ]), - }, - ], + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { + body: { + applications: [ + { + application, + resources: [`space:${options.spaceId}`], + privileges: uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.privilegeOrPrivileges) + ? options.privilegeOrPrivileges + : [options.privilegeOrPrivileges]), + ]), }, - } - ); - - if (options.expectedResult) { - expect(errorThrown).toBeNull(); - expect(actualResult).toEqual(options.expectedResult); - } - - if (options.expectErrorThrown) { - expect(errorThrown).toMatchSnapshot(); - } + ], + }, }); + + if (errorThrown) { + return errorThrown; + } + return actualResult; }; - checkPrivilegesAtSpaceTest('successful when checking for login and user has login', { - spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: true, + test('successful when checking for login and user has login', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - privileges: { - [mockActions.login]: true, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_1", + }, + ], + "username": "foo-username", + } + `); }); - checkPrivilegesAtSpaceTest(`failure when checking for login and user doesn't have login`, { - spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: false, - [mockActions.version]: true, + test(`failure when checking for login and user doesn't have login`, async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: false, + [mockActions.version]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - privileges: { - [mockActions.login]: false, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": false, + "privilege": "mock-action:login", + "resource": "space_1", + }, + ], + "username": "foo-username", + } + `); }); - checkPrivilegesAtSpaceTest( - `throws error when checking for login and user has login but doesn't have version`, - { + test(`throws error when checking for login and user has login but doesn't have version`, async () => { + const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', privilegeOrPrivileges: mockActions.login, esHasPrivilegesResponse: { @@ -162,74 +160,99 @@ describe('#atSpace', () => { }, }, }, - expectErrorThrown: true, - } - ); - - checkPrivilegesAtSpaceTest(`successful when checking for two actions and the user has both`, { - spaceId: 'space_1', - privilegeOrPrivileges: [ - `saved_object:${savedObjectTypes[0]}/get`, - `saved_object:${savedObjectTypes[1]}/get`, - ], - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + }); + expect(result).toMatchInlineSnapshot( + `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]` + ); + }); + + test(`successful when checking for two actions and the user has both`, async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - privileges: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + "username": "foo-username", + } + `); }); - checkPrivilegesAtSpaceTest(`failure when checking for two actions and the user has only one`, { - spaceId: 'space_1', - privilegeOrPrivileges: [ - `saved_object:${savedObjectTypes[0]}/get`, - `saved_object:${savedObjectTypes[1]}/get`, - ], - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + test(`failure when checking for two actions and the user has only one`, async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - privileges: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + "username": "foo-username", + } + `); }); describe('with a malformed Elasticsearch response', () => { - checkPrivilegesAtSpaceTest( - `throws a validation error when an extra privilege is present in the response`, - { + test(`throws a validation error when an extra privilege is present in the response`, async () => { + const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { @@ -246,13 +269,14 @@ describe('#atSpace', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because ["saved_object:bar-type/get" is not allowed]]]]` + ); + }); - checkPrivilegesAtSpaceTest( - `throws a validation error when privileges are missing in the response`, - { + test(`throws a validation error when privileges are missing in the response`, async () => { + const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { @@ -267,82 +291,69 @@ describe('#atSpace', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]` + ); + }); }); }); describe('#atSpaces', () => { - const checkPrivilegesAtSpacesTest = ( - description: string, - options: { - spaceIds: string[]; - privilegeOrPrivileges: string | string[]; - esHasPrivilegesResponse: HasPrivilegesResponse; - expectedResult?: any; - expectErrorThrown?: any; - } - ) => { - test(description, async () => { - const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( - options.esHasPrivilegesResponse - ); - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( - mockActions, - mockClusterClient, - application - ); - const request = httpServerMock.createKibanaRequest(); - const checkPrivileges = checkPrivilegesWithRequest(request); - - let actualResult; - let errorThrown = null; - try { - actualResult = await checkPrivileges.atSpaces( - options.spaceIds, - options.privilegeOrPrivileges - ); - } catch (err) { - errorThrown = err; - } + const checkPrivilegesAtSpacesTest = async (options: { + spaceIds: string[]; + privilegeOrPrivileges: string | string[]; + esHasPrivilegesResponse: HasPrivilegesResponse; + }) => { + const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( + options.esHasPrivilegesResponse + ); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + mockActions, + mockClusterClient, + application + ); + const request = httpServerMock.createKibanaRequest(); + const checkPrivileges = checkPrivilegesWithRequest(request); - expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.hasPrivileges', - { - body: { - applications: [ - { - application, - resources: options.spaceIds.map(spaceId => `space:${spaceId}`), - privileges: uniq([ - mockActions.version, - mockActions.login, - ...(Array.isArray(options.privilegeOrPrivileges) - ? options.privilegeOrPrivileges - : [options.privilegeOrPrivileges]), - ]), - }, - ], - }, - } + let actualResult; + let errorThrown = null; + try { + actualResult = await checkPrivileges.atSpaces( + options.spaceIds, + options.privilegeOrPrivileges ); + } catch (err) { + errorThrown = err; + } - if (options.expectedResult) { - expect(errorThrown).toBeNull(); - expect(actualResult).toEqual(options.expectedResult); - } - - if (options.expectErrorThrown) { - expect(errorThrown).toMatchSnapshot(); - } + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { + body: { + applications: [ + { + application, + resources: options.spaceIds.map(spaceId => `space:${spaceId}`), + privileges: uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.privilegeOrPrivileges) + ? options.privilegeOrPrivileges + : [options.privilegeOrPrivileges]), + ]), + }, + ], + }, }); + + if (errorThrown) { + return errorThrown; + } + return actualResult; }; - checkPrivilegesAtSpacesTest( - 'successful when checking for login and user has login at both spaces', - { + test('successful when checking for login and user has login at both spaces', async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: mockActions.login, esHasPrivilegesResponse: { @@ -361,24 +372,29 @@ describe('#atSpaces', () => { }, }, }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - spacePrivileges: { - space_1: { - [mockActions.login]: true, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_1", }, - space_2: { - [mockActions.login]: true, + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_2", }, - }, - }, - } - ); + ], + "username": "foo-username", + } + `); + }); - checkPrivilegesAtSpacesTest( - 'failure when checking for login and user has login at only one space', - { + test('failure when checking for login and user has login at only one space', async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: mockActions.login, esHasPrivilegesResponse: { @@ -397,24 +413,29 @@ describe('#atSpaces', () => { }, }, }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - spacePrivileges: { - space_1: { - [mockActions.login]: true, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_1", }, - space_2: { - [mockActions.login]: false, + Object { + "authorized": false, + "privilege": "mock-action:login", + "resource": "space_2", }, - }, - }, - } - ); + ], + "username": "foo-username", + } + `); + }); - checkPrivilegesAtSpacesTest( - `throws error when checking for login and user has login but doesn't have version`, - { + test(`throws error when checking for login and user has login but doesn't have version`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: mockActions.login, esHasPrivilegesResponse: { @@ -433,38 +454,43 @@ describe('#atSpaces', () => { }, }, }, - expectErrorThrown: true, - } - ); - - checkPrivilegesAtSpacesTest(`throws error when Elasticsearch returns malformed response`, { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [ - `saved_object:${savedObjectTypes[0]}/get`, - `saved_object:${savedObjectTypes[1]}/get`, - ], - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - 'space:space_2': { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + }); + expect(result).toMatchInlineSnapshot( + `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]` + ); + }); + + test(`throws error when Elasticsearch returns malformed response`, async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + 'space:space_2': { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, }, }, }, - }, - expectErrorThrown: true, + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]` + ); }); - checkPrivilegesAtSpacesTest( - `successful when checking for two actions at two spaces and user has it all`, - { + test(`successful when checking for two actions at two spaces and user has it all`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, @@ -490,26 +516,39 @@ describe('#atSpaces', () => { }, }, }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - spacePrivileges: { - space_1: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", }, - space_2: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", }, - }, - }, - } - ); + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + "username": "foo-username", + } + `); + }); - checkPrivilegesAtSpacesTest( - `failure when checking for two actions at two spaces and user has one action at one space`, - { + test(`failure when checking for two actions at two spaces and user has one action at one space`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, @@ -535,26 +574,39 @@ describe('#atSpaces', () => { }, }, }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - spacePrivileges: { - space_1: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: false, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", }, - space_2: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: false, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", }, - }, - }, - } - ); + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + "username": "foo-username", + } + `); + }); - checkPrivilegesAtSpacesTest( - `failure when checking for two actions at two spaces and user has two actions at one space`, - { + test(`failure when checking for two actions at two spaces and user has two actions at one space`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, @@ -580,26 +632,39 @@ describe('#atSpaces', () => { }, }, }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - spacePrivileges: { - space_1: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", }, - space_2: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: false, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", }, - }, - }, - } - ); + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + "username": "foo-username", + } + `); + }); - checkPrivilegesAtSpacesTest( - `failure when checking for two actions at two spaces and user has two actions at one space & one action at the other`, - { + test(`failure when checking for two actions at two spaces and user has two actions at one space & one action at the other`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, @@ -625,27 +690,40 @@ describe('#atSpaces', () => { }, }, }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - spacePrivileges: { - space_1: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", }, - space_2: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: false, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", }, - }, - }, - } - ); + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + "username": "foo-username", + } + `); + }); describe('with a malformed Elasticsearch response', () => { - checkPrivilegesAtSpacesTest( - `throws a validation error when an extra privilege is present in the response`, - { + test(`throws a validation error when an extra privilege is present in the response`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { @@ -668,13 +746,14 @@ describe('#atSpaces', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]` + ); + }); - checkPrivilegesAtSpacesTest( - `throws a validation error when privileges are missing in the response`, - { + test(`throws a validation error when privileges are missing in the response`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { @@ -695,13 +774,14 @@ describe('#atSpaces', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]` + ); + }); - checkPrivilegesAtSpacesTest( - `throws a validation error when an extra space is present in the response`, - { + test(`throws a validation error when an extra space is present in the response`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { @@ -727,13 +807,14 @@ describe('#atSpaces', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because ["space:space_3" is not allowed]]]` + ); + }); - checkPrivilegesAtSpacesTest( - `throws a validation error when an a space is missing in the response`, - { + test(`throws a validation error when an a space is missing in the response`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { @@ -749,124 +830,127 @@ describe('#atSpaces', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]` + ); + }); }); }); describe('#globally', () => { - const checkPrivilegesGloballyTest = ( - description: string, - options: { - privilegeOrPrivileges: string | string[]; - esHasPrivilegesResponse: HasPrivilegesResponse; - expectedResult?: any; - expectErrorThrown?: any; + const checkPrivilegesGloballyTest = async (options: { + privilegeOrPrivileges: string | string[]; + esHasPrivilegesResponse: HasPrivilegesResponse; + }) => { + const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( + options.esHasPrivilegesResponse + ); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + mockActions, + mockClusterClient, + application + ); + const request = httpServerMock.createKibanaRequest(); + const checkPrivileges = checkPrivilegesWithRequest(request); + + let actualResult; + let errorThrown = null; + try { + actualResult = await checkPrivileges.globally(options.privilegeOrPrivileges); + } catch (err) { + errorThrown = err; } - ) => { - test(description, async () => { - const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( - options.esHasPrivilegesResponse - ); - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( - mockActions, - mockClusterClient, - application - ); - const request = httpServerMock.createKibanaRequest(); - const checkPrivileges = checkPrivilegesWithRequest(request); - - let actualResult; - let errorThrown = null; - try { - actualResult = await checkPrivileges.globally(options.privilegeOrPrivileges); - } catch (err) { - errorThrown = err; - } - expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.hasPrivileges', - { - body: { - applications: [ - { - application, - resources: [GLOBAL_RESOURCE], - privileges: uniq([ - mockActions.version, - mockActions.login, - ...(Array.isArray(options.privilegeOrPrivileges) - ? options.privilegeOrPrivileges - : [options.privilegeOrPrivileges]), - ]), - }, - ], + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { + body: { + applications: [ + { + application, + resources: [GLOBAL_RESOURCE], + privileges: uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.privilegeOrPrivileges) + ? options.privilegeOrPrivileges + : [options.privilegeOrPrivileges]), + ]), }, - } - ); - - if (options.expectedResult) { - expect(errorThrown).toBeNull(); - expect(actualResult).toEqual(options.expectedResult); - } - - if (options.expectErrorThrown) { - expect(errorThrown).toMatchSnapshot(); - } + ], + }, }); + + if (errorThrown) { + return errorThrown; + } + return actualResult; }; - checkPrivilegesGloballyTest('successful when checking for login and user has login', { - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [mockActions.login]: true, - [mockActions.version]: true, + test('successful when checking for login and user has login', async () => { + const result = await checkPrivilegesGloballyTest({ + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - privileges: { - [mockActions.login]: true, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": undefined, + }, + ], + "username": "foo-username", + } + `); }); - checkPrivilegesGloballyTest(`failure when checking for login and user doesn't have login`, { - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [mockActions.login]: false, - [mockActions.version]: true, + test(`failure when checking for login and user doesn't have login`, async () => { + const result = await checkPrivilegesGloballyTest({ + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: false, + [mockActions.version]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - privileges: { - [mockActions.login]: false, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": false, + "privilege": "mock-action:login", + "resource": undefined, + }, + ], + "username": "foo-username", + } + `); }); - checkPrivilegesGloballyTest( - `throws error when checking for login and user has login but doesn't have version`, - { + test(`throws error when checking for login and user has login but doesn't have version`, async () => { + const result = await checkPrivilegesGloballyTest({ privilegeOrPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, @@ -880,92 +964,121 @@ describe('#globally', () => { }, }, }, - expectErrorThrown: true, - } - ); - - checkPrivilegesGloballyTest(`throws error when Elasticsearch returns malformed response`, { - privilegeOrPrivileges: [ - `saved_object:${savedObjectTypes[0]}/get`, - `saved_object:${savedObjectTypes[1]}/get`, - ], - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + }); + expect(result).toMatchInlineSnapshot( + `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]` + ); + }); + + test(`throws error when Elasticsearch returns malformed response`, async () => { + const result = await checkPrivilegesGloballyTest({ + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, }, }, }, - }, - expectErrorThrown: true, + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]` + ); }); - checkPrivilegesGloballyTest(`successful when checking for two actions and the user has both`, { - privilegeOrPrivileges: [ - `saved_object:${savedObjectTypes[0]}/get`, - `saved_object:${savedObjectTypes[1]}/get`, - ], - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + test(`successful when checking for two actions and the user has both`, async () => { + const result = await checkPrivilegesGloballyTest({ + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - privileges: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + "username": "foo-username", + } + `); }); - checkPrivilegesGloballyTest(`failure when checking for two actions and the user has only one`, { - privilegeOrPrivileges: [ - `saved_object:${savedObjectTypes[0]}/get`, - `saved_object:${savedObjectTypes[1]}/get`, - ], - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + test(`failure when checking for two actions and the user has only one`, async () => { + const result = await checkPrivilegesGloballyTest({ + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - privileges: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + "username": "foo-username", + } + `); }); describe('with a malformed Elasticsearch response', () => { - checkPrivilegesGloballyTest( - `throws a validation error when an extra privilege is present in the response`, - { + test(`throws a validation error when an extra privilege is present in the response`, async () => { + const result = await checkPrivilegesGloballyTest({ privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, @@ -981,13 +1094,14 @@ describe('#globally', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because ["saved_object:bar-type/get" is not allowed]]]]` + ); + }); - checkPrivilegesGloballyTest( - `throws a validation error when privileges are missing in the response`, - { + test(`throws a validation error when privileges are missing in the response`, async () => { + const result = await checkPrivilegesGloballyTest({ privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, @@ -1001,8 +1115,10 @@ describe('#globally', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]` + ); + }); }); }); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.ts b/x-pack/plugins/security/server/authorization/check_privileges.ts index 3ef7a8f29a0bf..177a49d6defe9 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.ts @@ -16,32 +16,17 @@ interface CheckPrivilegesActions { version: string; } -interface CheckPrivilegesAtResourcesResponse { +export interface CheckPrivilegesResponse { hasAllRequested: boolean; username: string; - resourcePrivileges: { - [resource: string]: { - [privilege: string]: boolean; - }; - }; -} - -export interface CheckPrivilegesAtResourceResponse { - hasAllRequested: boolean; - username: string; - privileges: { - [privilege: string]: boolean; - }; -} - -export interface CheckPrivilegesAtSpacesResponse { - hasAllRequested: boolean; - username: string; - spacePrivileges: { - [spaceId: string]: { - [privilege: string]: boolean; - }; - }; + privileges: Array<{ + /** + * If this attribute is undefined, this element is a privilege for the global resource. + */ + resource?: string; + privilege: string; + authorized: boolean; + }>; } export type CheckPrivilegesWithRequest = (request: KibanaRequest) => CheckPrivileges; @@ -50,12 +35,12 @@ export interface CheckPrivileges { atSpace( spaceId: string, privilegeOrPrivileges: string | string[] - ): Promise; + ): Promise; atSpaces( spaceIds: string[], privilegeOrPrivileges: string | string[] - ): Promise; - globally(privilegeOrPrivileges: string | string[]): Promise; + ): Promise; + globally(privilegeOrPrivileges: string | string[]): Promise; } export function checkPrivilegesWithRequestFactory( @@ -75,7 +60,7 @@ export function checkPrivilegesWithRequestFactory( const checkPrivilegesAtResources = async ( resources: string[], privilegeOrPrivileges: string | string[] - ): Promise => { + ): Promise => { const privileges = Array.isArray(privilegeOrPrivileges) ? privilegeOrPrivileges : [privilegeOrPrivileges]; @@ -106,55 +91,43 @@ export function checkPrivilegesWithRequestFactory( ); } + // we need to filter out the non requested privileges from the response + const resourcePrivileges = transform(applicationPrivilegesResponse, (result, value, key) => { + result[key!] = pick(value, privileges); + }) as HasPrivilegesResponseApplication; + const privilegeArray = Object.entries(resourcePrivileges) + .map(([key, val]) => { + // we need to turn the resource responses back into the space ids + const resource = + key !== GLOBAL_RESOURCE ? ResourceSerializer.deserializeSpaceResource(key!) : undefined; + return Object.entries(val).map(([privilege, authorized]) => ({ + resource, + privilege, + authorized, + })); + }) + .flat(); + return { hasAllRequested: hasPrivilegesResponse.has_all_requested, username: hasPrivilegesResponse.username, - // we need to filter out the non requested privileges from the response - resourcePrivileges: transform(applicationPrivilegesResponse, (result, value, key) => { - result[key!] = pick(value, privileges); - }), - }; - }; - - const checkPrivilegesAtResource = async ( - resource: string, - privilegeOrPrivileges: string | string[] - ) => { - const { hasAllRequested, username, resourcePrivileges } = await checkPrivilegesAtResources( - [resource], - privilegeOrPrivileges - ); - return { - hasAllRequested, - username, - privileges: resourcePrivileges[resource], + privileges: privilegeArray, }; }; return { async atSpace(spaceId: string, privilegeOrPrivileges: string | string[]) { const spaceResource = ResourceSerializer.serializeSpaceResource(spaceId); - return await checkPrivilegesAtResource(spaceResource, privilegeOrPrivileges); + return await checkPrivilegesAtResources([spaceResource], privilegeOrPrivileges); }, async atSpaces(spaceIds: string[], privilegeOrPrivileges: string | string[]) { const spaceResources = spaceIds.map(spaceId => ResourceSerializer.serializeSpaceResource(spaceId) ); - const { hasAllRequested, username, resourcePrivileges } = await checkPrivilegesAtResources( - spaceResources, - privilegeOrPrivileges - ); - return { - hasAllRequested, - username, - // we need to turn the resource responses back into the space ids - spacePrivileges: transform(resourcePrivileges, (result, value, key) => { - result[ResourceSerializer.deserializeSpaceResource(key!)] = value; - }), - }; + return await checkPrivilegesAtResources(spaceResources, privilegeOrPrivileges); }, async globally(privilegeOrPrivileges: string | string[]) { - return await checkPrivilegesAtResource(GLOBAL_RESOURCE, privilegeOrPrivileges); + return await checkPrivilegesAtResources([GLOBAL_RESOURCE], privilegeOrPrivileges); }, }; }; diff --git a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts index 0377dd06eb669..6014bad739e77 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts @@ -6,11 +6,11 @@ import { KibanaRequest } from '../../../../../src/core/server'; import { SpacesService } from '../plugin'; -import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from './check_privileges'; +import { CheckPrivilegesResponse, CheckPrivilegesWithRequest } from './check_privileges'; export type CheckPrivilegesDynamically = ( privilegeOrPrivileges: string | string[] -) => Promise; +) => Promise; export type CheckPrivilegesDynamicallyWithRequest = ( request: KibanaRequest diff --git a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts index 4618e8e6641fc..43b3824500579 100644 --- a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts @@ -7,57 +7,113 @@ import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges'; import { httpServerMock } from '../../../../../src/core/server/mocks'; +import { CheckPrivileges, CheckPrivilegesWithRequest } from './check_privileges'; +import { SpacesService } from '../plugin'; -test(`checkPrivileges.atSpace when spaces is enabled`, async () => { - const expectedResult = Symbol(); - const spaceId = 'foo-space'; - const mockCheckPrivileges = { - atSpace: jest.fn().mockReturnValue(expectedResult), - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const request = httpServerMock.createKibanaRequest(); - const privilegeOrPrivileges = ['foo', 'bar']; - const mockSpacesService = { - getSpaceId: jest.fn(), - namespaceToSpaceId: jest.fn().mockReturnValue(spaceId), - }; +let mockCheckPrivileges: jest.Mocked; +let mockCheckPrivilegesWithRequest: jest.Mocked; +let mockSpacesService: jest.Mocked | undefined; +const request = httpServerMock.createKibanaRequest(); - const checkSavedObjectsPrivileges = checkSavedObjectsPrivilegesWithRequestFactory( +const createFactory = () => + checkSavedObjectsPrivilegesWithRequestFactory( mockCheckPrivilegesWithRequest, () => mockSpacesService )(request); - const namespace = 'foo'; - - const result = await checkSavedObjectsPrivileges(privilegeOrPrivileges, namespace); +beforeEach(() => { + mockCheckPrivileges = { + atSpace: jest.fn(), + atSpaces: jest.fn(), + globally: jest.fn(), + }; + mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - expect(result).toBe(expectedResult); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, privilegeOrPrivileges); - expect(mockSpacesService.namespaceToSpaceId).toBeCalledWith(namespace); + mockSpacesService = { + getSpaceId: jest.fn(), + namespaceToSpaceId: jest.fn().mockImplementation((namespace: string) => `${namespace}-id`), + }; }); -test(`checkPrivileges.globally when spaces is disabled`, async () => { - const expectedResult = Symbol(); - const mockCheckPrivileges = { - globally: jest.fn().mockReturnValue(expectedResult), - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); +describe('#checkSavedObjectsPrivileges', () => { + const actions = ['foo', 'bar']; + const namespace1 = 'baz'; + const namespace2 = 'qux'; - const request = httpServerMock.createKibanaRequest(); + describe('when checking multiple namespaces', () => { + const namespaces = [namespace1, namespace2]; - const privilegeOrPrivileges = ['foo', 'bar']; + test(`throws an error when Spaces is disabled`, async () => { + mockSpacesService = undefined; + const checkSavedObjectsPrivileges = createFactory(); - const checkSavedObjectsPrivileges = checkSavedObjectsPrivilegesWithRequestFactory( - mockCheckPrivilegesWithRequest, - () => undefined - )(request); + await expect( + checkSavedObjectsPrivileges(actions, namespaces) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't check saved object privileges for multiple namespaces if Spaces is disabled"` + ); + }); + + test(`throws an error when using an empty namespaces array`, async () => { + const checkSavedObjectsPrivileges = createFactory(); + + await expect( + checkSavedObjectsPrivileges(actions, []) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't check saved object privileges for 0 namespaces"` + ); + }); + + test(`uses checkPrivileges.atSpaces when spaces is enabled`, async () => { + const expectedResult = Symbol(); + mockCheckPrivileges.atSpaces.mockReturnValue(expectedResult as any); + const checkSavedObjectsPrivileges = createFactory(); + + const result = await checkSavedObjectsPrivileges(actions, namespaces); + + expect(result).toBe(expectedResult); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenCalledTimes(2); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(1, namespace1); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(2, namespace2); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledTimes(1); + const spaceIds = mockSpacesService!.namespaceToSpaceId.mock.results.map(x => x.value); + expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledWith(spaceIds, actions); + }); + }); + + describe('when checking a single namespace', () => { + test(`uses checkPrivileges.atSpace when Spaces is enabled`, async () => { + const expectedResult = Symbol(); + mockCheckPrivileges.atSpace.mockReturnValue(expectedResult as any); + const checkSavedObjectsPrivileges = createFactory(); + + const result = await checkSavedObjectsPrivileges(actions, namespace1); + + expect(result).toBe(expectedResult); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenCalledTimes(1); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenCalledWith(namespace1); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledTimes(1); + const spaceId = mockSpacesService!.namespaceToSpaceId.mock.results[0].value; + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, actions); + }); - const namespace = 'foo'; + test(`uses checkPrivileges.globally when Spaces is disabled`, async () => { + const expectedResult = Symbol(); + mockCheckPrivileges.globally.mockReturnValue(expectedResult as any); + mockSpacesService = undefined; + const checkSavedObjectsPrivileges = createFactory(); - const result = await checkSavedObjectsPrivileges(privilegeOrPrivileges, namespace); + const result = await checkSavedObjectsPrivileges(actions, namespace1); - expect(result).toBe(expectedResult); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(privilegeOrPrivileges); + expect(result).toBe(expectedResult); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivileges.globally).toHaveBeenCalledTimes(1); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(actions); + }); + }); }); diff --git a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts index 02958fe265efa..43140143a1773 100644 --- a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts @@ -6,32 +6,44 @@ import { KibanaRequest } from '../../../../../src/core/server'; import { SpacesService } from '../plugin'; -import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from './check_privileges'; +import { CheckPrivilegesWithRequest, CheckPrivilegesResponse } from './check_privileges'; export type CheckSavedObjectsPrivilegesWithRequest = ( request: KibanaRequest ) => CheckSavedObjectsPrivileges; + export type CheckSavedObjectsPrivileges = ( actions: string | string[], - namespace?: string -) => Promise; + namespaceOrNamespaces?: string | string[] +) => Promise; export const checkSavedObjectsPrivilegesWithRequestFactory = ( checkPrivilegesWithRequest: CheckPrivilegesWithRequest, getSpacesService: () => SpacesService | undefined ): CheckSavedObjectsPrivilegesWithRequest => { - return function checkSavedObjectsPrivilegesWithRequest(request: KibanaRequest) { + return function checkSavedObjectsPrivilegesWithRequest( + request: KibanaRequest + ): CheckSavedObjectsPrivileges { return async function checkSavedObjectsPrivileges( actions: string | string[], - namespace?: string + namespaceOrNamespaces?: string | string[] ) { const spacesService = getSpacesService(); - return spacesService - ? await checkPrivilegesWithRequest(request).atSpace( - spacesService.namespaceToSpaceId(namespace), - actions - ) - : await checkPrivilegesWithRequest(request).globally(actions); + if (Array.isArray(namespaceOrNamespaces)) { + if (spacesService === undefined) { + throw new Error( + `Can't check saved object privileges for multiple namespaces if Spaces is disabled` + ); + } else if (!namespaceOrNamespaces.length) { + throw new Error(`Can't check saved object privileges for 0 namespaces`); + } + const spaceIds = namespaceOrNamespaces.map(x => spacesService.namespaceToSpaceId(x)); + return await checkPrivilegesWithRequest(request).atSpaces(spaceIds, actions); + } else if (spacesService) { + const spaceId = spacesService.namespaceToSpaceId(namespaceOrNamespaces); + return await checkPrivilegesWithRequest(request).atSpace(spaceId, actions); + } + return await checkPrivilegesWithRequest(request).globally(actions); }; }; }; diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index 912ae60e12065..ea97a1b3b590c 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -11,7 +11,9 @@ import { httpServerMock, loggingServiceMock } from '../../../../../src/core/serv import { authorizationMock } from './index.mock'; import { Feature } from '../../../features/server'; -type MockAuthzOptions = { rejectCheckPrivileges: any } | { resolveCheckPrivileges: any }; +type MockAuthzOptions = + | { rejectCheckPrivileges: any } + | { resolveCheckPrivileges: { privileges: Array<{ privilege: string; authorized: boolean }> } }; const actions = new Actions('1.0.0-zeta1'); const mockRequest = httpServerMock.createKibanaRequest(); @@ -26,7 +28,8 @@ const createMockAuthz = (options: MockAuthzOptions) => { throw options.rejectCheckPrivileges; } - expect(checkActions).toEqual(Object.keys(options.resolveCheckPrivileges.privileges)); + const expected = options.resolveCheckPrivileges.privileges.map(x => x.privilege); + expect(checkActions).toEqual(expected); return options.resolveCheckPrivileges; }); }); @@ -226,17 +229,17 @@ describe('usingPrivileges', () => { test(`disables ui capabilities when they don't have privileges`, async () => { const mockAuthz = createMockAuthz({ resolveCheckPrivileges: { - privileges: { - [actions.ui.get('navLinks', 'foo')]: true, - [actions.ui.get('navLinks', 'bar')]: false, - [actions.ui.get('navLinks', 'quz')]: false, - [actions.ui.get('management', 'kibana', 'indices')]: true, - [actions.ui.get('management', 'kibana', 'settings')]: false, - [actions.ui.get('fooFeature', 'foo')]: true, - [actions.ui.get('fooFeature', 'bar')]: false, - [actions.ui.get('barFeature', 'foo')]: true, - [actions.ui.get('barFeature', 'bar')]: false, - }, + privileges: [ + { privilege: actions.ui.get('navLinks', 'foo'), authorized: true }, + { privilege: actions.ui.get('navLinks', 'bar'), authorized: false }, + { privilege: actions.ui.get('navLinks', 'quz'), authorized: false }, + { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, + { privilege: actions.ui.get('management', 'kibana', 'settings'), authorized: false }, + { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('fooFeature', 'bar'), authorized: false }, + { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('barFeature', 'bar'), authorized: false }, + ], }, }); @@ -314,15 +317,15 @@ describe('usingPrivileges', () => { test(`doesn't re-enable disabled uiCapabilities`, async () => { const mockAuthz = createMockAuthz({ resolveCheckPrivileges: { - privileges: { - [actions.ui.get('navLinks', 'foo')]: true, - [actions.ui.get('navLinks', 'bar')]: true, - [actions.ui.get('management', 'kibana', 'indices')]: true, - [actions.ui.get('fooFeature', 'foo')]: true, - [actions.ui.get('fooFeature', 'bar')]: true, - [actions.ui.get('barFeature', 'foo')]: true, - [actions.ui.get('barFeature', 'bar')]: true, - }, + privileges: [ + { privilege: actions.ui.get('navLinks', 'foo'), authorized: true }, + { privilege: actions.ui.get('navLinks', 'bar'), authorized: true }, + { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, + { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('fooFeature', 'bar'), authorized: true }, + { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('barFeature', 'bar'), authorized: true }, + ], }, }); diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index be26f52fbf756..f0f1a42ad0bd5 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -9,7 +9,7 @@ import { UICapabilities } from 'ui/capabilities'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { Feature } from '../../../features/server'; -import { CheckPrivilegesAtResourceResponse } from './check_privileges'; +import { CheckPrivilegesResponse } from './check_privileges'; import { Authorization } from './index'; export function disableUICapabilitiesFactory( @@ -77,7 +77,7 @@ export function disableUICapabilitiesFactory( [] ); - let checkPrivilegesResponse: CheckPrivilegesAtResourceResponse; + let checkPrivilegesResponse: CheckPrivilegesResponse; try { const checkPrivilegesDynamically = authz.checkPrivilegesDynamicallyWithRequest(request); checkPrivilegesResponse = await checkPrivilegesDynamically(uiActions); @@ -105,7 +105,9 @@ export function disableUICapabilitiesFactory( } const action = authz.actions.ui.get(featureId, ...uiCapabilityParts); - return checkPrivilegesResponse.privileges[action] === true; + return checkPrivilegesResponse.privileges.some( + x => x.privilege === action && x.authorized === true + ); }; return mapValues(uiCapabilities, (featureUICapabilities, featureId) => { diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 032d231fe798f..68acf68f46109 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -149,6 +149,7 @@ export class Plugin { auditLogger: new SecurityAuditLogger(() => this.getLegacyAPI().auditLogger), authz, savedObjects: core.savedObjects, + getSpacesService: this.getSpacesService, }); core.capabilities.registerSwitcher(authz.disableUnauthorizedCapabilities); diff --git a/x-pack/plugins/security/server/saved_objects/index.ts b/x-pack/plugins/security/server/saved_objects/index.ts index 5954729562847..7dac745fcf84b 100644 --- a/x-pack/plugins/security/server/saved_objects/index.ts +++ b/x-pack/plugins/security/server/saved_objects/index.ts @@ -13,14 +13,21 @@ import { import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; import { Authorization } from '../authorization'; import { SecurityAuditLogger } from '../audit'; +import { SpacesService } from '../plugin'; interface SetupSavedObjectsParams { auditLogger: SecurityAuditLogger; authz: Pick; savedObjects: CoreSetup['savedObjects']; + getSpacesService(): SpacesService | undefined; } -export function setupSavedObjects({ auditLogger, authz, savedObjects }: SetupSavedObjectsParams) { +export function setupSavedObjects({ + auditLogger, + authz, + savedObjects, + getSpacesService, +}: SetupSavedObjectsParams) { const getKibanaRequest = (request: KibanaRequest | LegacyRequest) => request instanceof KibanaRequest ? request : KibanaRequest.from(request); @@ -44,6 +51,7 @@ export function setupSavedObjects({ auditLogger, authz, savedObjects }: SetupSav kibanaRequest ), errors: SavedObjectsClient.errors, + getSpacesService, }) : client; }); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 3c04508e3a74a..3c4034e07f995 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -9,6 +9,11 @@ import { Actions } from '../authorization'; import { securityAuditLoggerMock } from '../audit/index.mock'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectActions } from '../authorization/actions/saved_object'; + +let clientOpts: ReturnType; +let client: SecureSavedObjectsClientWrapper; +const USERNAME = Symbol(); const createSecureSavedObjectsClientWrapperOptions = () => { const actions = new Actions('some-version'); @@ -22,810 +27,677 @@ const createSecureSavedObjectsClientWrapperOptions = () => { const errors = ({ decorateForbiddenError: jest.fn().mockReturnValue(forbiddenError), decorateGeneralError: jest.fn().mockReturnValue(generalError), + isNotFoundError: jest.fn().mockReturnValue(false), } as unknown) as jest.Mocked; + const getSpacesService = jest.fn().mockReturnValue(true); return { actions, baseClient: savedObjectsClientMock.create(), checkSavedObjectsPrivilegesAsCurrentUser: jest.fn(), errors, + getSpacesService, auditLogger: securityAuditLoggerMock.create(), forbiddenError, generalError, }; }; -describe('#errors', () => { - test(`assigns errors from constructor to .errors`, () => { - const options = createSecureSavedObjectsClientWrapperOptions(); - const client = new SecureSavedObjectsClientWrapper(options); +const expectGeneralError = async (fn: Function, args: Record) => { + // mock the checkPrivileges.globally rejection + const rejection = new Error('An actual error would happen here'); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(rejection); + + await expect(fn.bind(client)(...Object.values(args))).rejects.toThrowError( + clientOpts.generalError + ); + expect(clientOpts.errors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); +}; + +/** + * Fails the first authorization check, passes any others + * Requires that function args are passed in as key/value pairs + * The argument properties must be in the correct order to be spread properly + */ +const expectForbiddenError = async (fn: Function, args: Record) => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + + await expect(fn.bind(client)(...Object.values(args))).rejects.toThrowError( + clientOpts.forbiddenError + ); + const getCalls = (clientOpts.actions.savedObject.get as jest.MockedFunction< + SavedObjectActions['get'] + >).mock.calls; + const actions = clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mock.calls[0][0]; + const spaceId = args.options?.namespace || 'default'; + + const ACTION = getCalls[0][1]; + const types = getCalls.map(x => x[0]); + const missing = [{ spaceId, privilege: actions[0] }]; // if there was more than one type, only the first type was unauthorized + const spaceIds = [spaceId]; + + expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + USERNAME, + ACTION, + types, + spaceIds, + missing, + args + ); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); +}; + +const expectSuccess = async (fn: Function, args: Record) => { + const result = await fn.bind(client)(...Object.values(args)); + const getCalls = (clientOpts.actions.savedObject.get as jest.MockedFunction< + SavedObjectActions['get'] + >).mock.calls; + const ACTION = getCalls[0][1]; + const types = getCalls.map(x => x[0]); + const spaceIds = [args.options?.namespace || 'default']; + + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + USERNAME, + ACTION, + types, + spaceIds, + args + ); + return result; +}; + +const expectPrivilegeCheck = async (fn: Function, args: Record) => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + + await expect(fn.bind(client)(...Object.values(args))).rejects.toThrow(); // test is simpler with error case + const getResults = (clientOpts.actions.savedObject.get as jest.MockedFunction< + SavedObjectActions['get'] + >).mock.results; + const actions = getResults.map(x => x.value); + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(1); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + actions, + args.options?.namespace + ); +}; + +const expectObjectNamespaceFiltering = async (fn: Function, args: Record) => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( + getMockCheckPrivilegesSuccess // privilege check for authorization + ); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure // privilege check for namespace filtering + ); + + const authorizedNamespace = args.options.namespace || 'default'; + const namespaces = ['some-other-namespace', authorizedNamespace]; + const returnValue = { namespaces, foo: 'bar' }; + // we don't know which base client method will be called; mock them all + clientOpts.baseClient.create.mockReturnValue(returnValue as any); + clientOpts.baseClient.get.mockReturnValue(returnValue as any); + clientOpts.baseClient.update.mockReturnValue(returnValue as any); + + const result = await fn.bind(client)(...Object.values(args)); + expect(result).toEqual(expect.objectContaining({ namespaces: [authorizedNamespace, '?'] })); + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenLastCalledWith( + 'login:', + namespaces + ); +}; + +const expectObjectsNamespaceFiltering = async (fn: Function, args: Record) => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( + getMockCheckPrivilegesSuccess // privilege check for authorization + ); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure // privilege check for namespace filtering + ); + + const authorizedNamespace = args.options.namespace || 'default'; + const returnValue = { + saved_objects: [ + { namespaces: ['foo'] }, + { namespaces: [authorizedNamespace] }, + { namespaces: ['foo', authorizedNamespace] }, + ], + }; + + // we don't know which base client method will be called; mock them all + clientOpts.baseClient.bulkCreate.mockReturnValue(returnValue as any); + clientOpts.baseClient.bulkGet.mockReturnValue(returnValue as any); + clientOpts.baseClient.bulkUpdate.mockReturnValue(returnValue as any); + clientOpts.baseClient.find.mockReturnValue(returnValue as any); + + const result = await fn.bind(client)(...Object.values(args)); + expect(result).toEqual( + expect.objectContaining({ + saved_objects: [ + { namespaces: ['?'] }, + { namespaces: [authorizedNamespace] }, + { namespaces: [authorizedNamespace, '?'] }, + ], + }) + ); + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenLastCalledWith('login:', [ + 'foo', + authorizedNamespace, + ]); +}; + +function getMockCheckPrivilegesSuccess(actions: string | string[], namespaces?: string | string[]) { + const _namespaces = Array.isArray(namespaces) ? namespaces : [namespaces || 'default']; + const _actions = Array.isArray(actions) ? actions : [actions]; + return { + hasAllRequested: true, + username: USERNAME, + privileges: _namespaces + .map(resource => + _actions.map(action => ({ + resource, + privilege: action, + authorized: true, + })) + ) + .flat(), + }; +} + +/** + * Fails the authorization check for the first privilege, and passes any others + * This check may be for an action for two different types in the same namespace + * Or, it may be for an action for the same type in two different namespaces + * Either way, the first privilege check returned is false, and any others return true + */ +function getMockCheckPrivilegesFailure(actions: string | string[], namespaces?: string | string[]) { + const _namespaces = Array.isArray(namespaces) ? namespaces : [namespaces || 'default']; + const _actions = Array.isArray(actions) ? actions : [actions]; + return { + hasAllRequested: false, + username: USERNAME, + privileges: _namespaces + .map((resource, idxa) => + _actions.map((action, idxb) => ({ + resource, + privilege: action, + authorized: idxa > 0 || idxb > 0, + })) + ) + .flat(), + }; +} + +/** + * Before each test, create the Client with its Options + */ +beforeEach(() => { + clientOpts = createSecureSavedObjectsClientWrapperOptions(); + client = new SecureSavedObjectsClientWrapper(clientOpts); + + // succeed privilege checks by default + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesSuccess + ); +}); + +describe('#addToNamespaces', () => { + const type = 'foo'; + const id = `${type}-id`; + const newNs1 = 'foo-namespace'; + const newNs2 = 'bar-namespace'; + const namespaces = [newNs1, newNs2]; + const currentNs = 'default'; + const privilege1 = `mock-saved_object:${type}/create`; + const privilege2 = `mock-saved_object:${type}/update`; + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.addToNamespaces, { type, id, namespaces }); + }); + + test(`throws decorated ForbiddenError when unauthorized to create in new space`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + + await expect(client.addToNamespaces(type, id, namespaces)).rejects.toThrowError( + clientOpts.forbiddenError + ); + + expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + USERNAME, + 'addToNamespacesCreate', + [type], + namespaces.sort(), + [{ privilege: privilege1, spaceId: newNs1 }], + { id, type, namespaces, options: {} } + ); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized to update in current space`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( + getMockCheckPrivilegesSuccess // create + ); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure // update + ); + + await expect(client.addToNamespaces(type, id, namespaces)).rejects.toThrowError( + clientOpts.forbiddenError + ); + + expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect( + clientOpts.auditLogger.savedObjectsAuthorizationFailure + ).toHaveBeenLastCalledWith( + USERNAME, + 'addToNamespacesUpdate', + [type], + [currentNs], + [{ privilege: privilege2, spaceId: currentNs }], + { id, type, namespaces, options: {} } + ); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); + }); + + test(`returns result of baseClient.addToNamespaces when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.addToNamespaces.mockReturnValue(apiCallReturnValue as any); + + const result = await client.addToNamespaces(type, id, namespaces); + expect(result).toBe(apiCallReturnValue); + + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(2); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( + 1, + USERNAME, + 'addToNamespacesCreate', // action for privilege check is 'create', but auditAction is 'addToNamespacesCreate' + [type], + namespaces.sort(), + { type, id, namespaces, options: {} } + ); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( + 2, + USERNAME, + 'addToNamespacesUpdate', // action for privilege check is 'update', but auditAction is 'addToNamespacesUpdate' + [type], + [currentNs], + { type, id, namespaces, options: {} } + ); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( + getMockCheckPrivilegesSuccess // create + ); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure // update + ); + + await expect(client.addToNamespaces(type, id, namespaces)).rejects.toThrow(); // test is simpler with error case + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenNthCalledWith( + 1, + [privilege1], + namespaces + ); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenNthCalledWith( + 2, + [privilege2], + undefined // default namespace + ); + }); +}); + +describe('#bulkCreate', () => { + const attributes = { some: 'attr' }; + const obj1 = Object.freeze({ type: 'foo', otherThing: 'sup', attributes }); + const obj2 = Object.freeze({ type: 'bar', otherThing: 'everyone', attributes }); + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const objects = [obj1]; + await expectGeneralError(client.bulkCreate, { objects }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const objects = [obj1, obj2]; + await expectForbiddenError(client.bulkCreate, { objects, options }); + }); + + test(`returns result of baseClient.bulkCreate when authorized`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.bulkCreate.mockReturnValue(apiCallReturnValue as any); + + const objects = [obj1, obj2]; + const result = await expectSuccess(client.bulkCreate, { objects, options }); + expect(result).toEqual(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + const objects = [obj1, obj2]; + await expectPrivilegeCheck(client.bulkCreate, { objects, options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + const objects = [obj1, obj2]; + await expectObjectsNamespaceFiltering(client.bulkCreate, { objects, options }); + }); +}); + +describe('#bulkGet', () => { + const obj1 = Object.freeze({ type: 'foo', id: 'foo-id' }); + const obj2 = Object.freeze({ type: 'bar', id: 'bar-id' }); + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const objects = [obj1]; + await expectGeneralError(client.bulkGet, { objects }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const objects = [obj1, obj2]; + await expectForbiddenError(client.bulkGet, { objects, options }); + }); + + test(`returns result of baseClient.bulkGet when authorized`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.bulkGet.mockReturnValue(apiCallReturnValue as any); + + const objects = [obj1, obj2]; + const result = await expectSuccess(client.bulkGet, { objects, options }); + expect(result).toEqual(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + const objects = [obj1, obj2]; + await expectPrivilegeCheck(client.bulkGet, { objects, options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + const objects = [obj1, obj2]; + await expectObjectsNamespaceFiltering(client.bulkGet, { objects, options }); + }); +}); + +describe('#bulkUpdate', () => { + const obj1 = Object.freeze({ type: 'foo', id: 'foo-id', attributes: { some: 'attr' } }); + const obj2 = Object.freeze({ type: 'bar', id: 'bar-id', attributes: { other: 'attr' } }); + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const objects = [obj1]; + await expectGeneralError(client.bulkUpdate, { objects }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const objects = [obj1, obj2]; + await expectForbiddenError(client.bulkUpdate, { objects, options }); + }); + + test(`returns result of baseClient.bulkUpdate when authorized`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.bulkUpdate.mockReturnValue(apiCallReturnValue as any); + + const objects = [obj1, obj2]; + const result = await expectSuccess(client.bulkUpdate, { objects, options }); + expect(result).toEqual(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + const objects = [obj1, obj2]; + await expectPrivilegeCheck(client.bulkUpdate, { objects, options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + const objects = [obj1, obj2]; + await expectObjectsNamespaceFiltering(client.bulkUpdate, { objects, options }); + }); +}); + +describe('#create', () => { + const type = 'foo'; + const attributes = { some_attr: 's' }; + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { + await expectGeneralError(client.create, { type }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + await expectForbiddenError(client.create, { type, attributes, options }); + }); + + test(`returns result of baseClient.create when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.create.mockResolvedValue(apiCallReturnValue as any); + + const result = await expectSuccess(client.create, { type, attributes, options }); + expect(result).toBe(apiCallReturnValue); + }); - expect(client.errors).toBe(options.errors); + test(`checks privileges for user, actions, and namespace`, async () => { + await expectPrivilegeCheck(client.create, { type, attributes, options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + await expectObjectNamespaceFiltering(client.create, { type, attributes, options }); }); }); -describe(`spaces disabled`, () => { - describe('#create', () => { - test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.create(type)).rejects.toThrowError(options.generalError); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'create')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { [options.actions.savedObject.get(type, 'create')]: false }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const attributes = { some_attr: 's' }; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.create(type, attributes, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'create')], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'create', - [type], - [options.actions.savedObject.get(type, 'create')], - { type, attributes, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.create when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { [options.actions.savedObject.get(type, 'create')]: true }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.create.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const attributes = { some_attr: 's' }; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.create(type, attributes, apiCallOptions)).resolves.toBe( - apiCallReturnValue - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'create')], - apiCallOptions.namespace - ); - expect(options.baseClient.create).toHaveBeenCalledWith(type, attributes, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'create', - [type], - { type, attributes, options: apiCallOptions } - ); - }); - }); - - describe('#bulkCreate', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect( - client.bulkCreate([{ type, attributes: {} }], apiCallOptions) - ).rejects.toThrowError(options.generalError); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'bulk_create')], - apiCallOptions.namespace - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type1, 'bulk_create')]: false, - [options.actions.savedObject.get(type2, 'bulk_create')]: true, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const objects = [ - { type: type1, attributes: {} }, - { type: type2, attributes: {} }, - ]; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.bulkCreate(objects, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [ - options.actions.savedObject.get(type1, 'bulk_create'), - options.actions.savedObject.get(type2, 'bulk_create'), - ], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_create', - [type1, type2], - [options.actions.savedObject.get(type1, 'bulk_create')], - { objects, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.bulkCreate when authorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { - [options.actions.savedObject.get(type1, 'bulk_create')]: true, - [options.actions.savedObject.get(type2, 'bulk_create')]: true, - }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.bulkCreate.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const objects = [ - { type: type1, otherThing: 'sup', attributes: {} }, - { type: type2, otherThing: 'everyone', attributes: {} }, - ]; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.bulkCreate(objects, apiCallOptions)).resolves.toBe(apiCallReturnValue); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [ - options.actions.savedObject.get(type1, 'bulk_create'), - options.actions.savedObject.get(type2, 'bulk_create'), - ], - apiCallOptions.namespace - ); - expect(options.baseClient.bulkCreate).toHaveBeenCalledWith(objects, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'bulk_create', - [type1, type2], - { objects, options: apiCallOptions } - ); - }); - }); - - describe('#delete', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.delete(type, 'bar')).rejects.toThrowError(options.generalError); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'delete')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const id = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type, 'delete')]: false, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.delete(type, id, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'delete')], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'delete', - [type], - [options.actions.savedObject.get(type, 'delete')], - { type, id, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of internalRepository.delete when authorized`, async () => { - const type = 'foo'; - const id = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { [options.actions.savedObject.get(type, 'delete')]: true }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.delete.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.delete(type, id, apiCallOptions)).resolves.toBe(apiCallReturnValue); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'delete')], - apiCallOptions.namespace - ); - expect(options.baseClient.delete).toHaveBeenCalledWith(type, id, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'delete', - [type], - { type, id, options: apiCallOptions } - ); - }); - }); - - describe('#find', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.find({ type })).rejects.toThrowError(options.generalError); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'find')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { [options.actions.savedObject.get(type, 'find')]: false }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ type, namespace: 'some-ns' }); - await expect(client.find(apiCallOptions)).rejects.toThrowError(options.forbiddenError); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'find')], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'find', - [type], - [options.actions.savedObject.get(type, 'find')], - { options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type1, 'find')]: false, - [options.actions.savedObject.get(type2, 'find')]: true, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ type: [type1, type2], namespace: 'some-ns' }); - await expect(client.find(apiCallOptions)).rejects.toThrowError(options.forbiddenError); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [ - options.actions.savedObject.get(type1, 'find'), - options.actions.savedObject.get(type2, 'find'), - ], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'find', - [type1, type2], - [options.actions.savedObject.get(type1, 'find')], - { options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.find when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { [options.actions.savedObject.get(type, 'find')]: true }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.find.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ type, namespace: 'some-ns' }); - await expect(client.find(apiCallOptions)).resolves.toBe(apiCallReturnValue); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'find')], - apiCallOptions.namespace - ); - expect(options.baseClient.find).toHaveBeenCalledWith(apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'find', - [type], - { options: apiCallOptions } - ); - }); - }); - - describe('#bulkGet', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.bulkGet([{ id: 'bar', type }])).rejects.toThrowError( - options.generalError - ); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'bulk_get')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type1, 'bulk_get')]: false, - [options.actions.savedObject.get(type2, 'bulk_get')]: true, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const objects = [ - { type: type1, id: `bar-${type1}` }, - { type: type2, id: `bar-${type2}` }, - ]; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.bulkGet(objects, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [ - options.actions.savedObject.get(type1, 'bulk_get'), - options.actions.savedObject.get(type2, 'bulk_get'), - ], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_get', - [type1, type2], - [options.actions.savedObject.get(type1, 'bulk_get')], - { objects, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.bulkGet when authorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { - [options.actions.savedObject.get(type1, 'bulk_get')]: true, - [options.actions.savedObject.get(type2, 'bulk_get')]: true, - }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.bulkGet.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const objects = [ - { type: type1, id: `id-${type1}` }, - { type: type2, id: `id-${type2}` }, - ]; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.bulkGet(objects, apiCallOptions)).resolves.toBe(apiCallReturnValue); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [ - options.actions.savedObject.get(type1, 'bulk_get'), - options.actions.savedObject.get(type2, 'bulk_get'), - ], - apiCallOptions.namespace - ); - expect(options.baseClient.bulkGet).toHaveBeenCalledWith(objects, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'bulk_get', - [type1, type2], - { objects, options: apiCallOptions } - ); - }); - }); - - describe('#get', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.get(type, 'bar')).rejects.toThrowError(options.generalError); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'get')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const id = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type, 'get')]: false, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.get(type, id, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'get')], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'get', - [type], - [options.actions.savedObject.get(type, 'get')], - { type, id, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.get when authorized`, async () => { - const type = 'foo'; - const id = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { [options.actions.savedObject.get(type, 'get')]: true }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.get.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.get(type, id, apiCallOptions)).resolves.toBe(apiCallReturnValue); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'get')], - apiCallOptions.namespace - ); - expect(options.baseClient.get).toHaveBeenCalledWith(type, id, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'get', - [type], - { type, id, options: apiCallOptions } - ); - }); - }); - - describe('#update', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.update(type, 'bar', {})).rejects.toThrowError(options.generalError); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'update')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const id = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type, 'update')]: false, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const attributes = { some: 'attr' }; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.update(type, id, attributes, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'update')], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'update', - [type], - [options.actions.savedObject.get(type, 'update')], - { type, id, attributes, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.update when authorized`, async () => { - const type = 'foo'; - const id = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { [options.actions.savedObject.get(type, 'update')]: true }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.update.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const attributes = { some: 'attr' }; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.update(type, id, attributes, apiCallOptions)).resolves.toBe( - apiCallReturnValue - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'update')], - apiCallOptions.namespace - ); - expect(options.baseClient.update).toHaveBeenCalledWith(type, id, attributes, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'update', - [type], - { type, id, attributes, options: apiCallOptions } - ); - }); - }); - - describe('#bulkUpdate', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.bulkUpdate([{ id: 'bar', type, attributes: {} }])).rejects.toThrowError( - options.generalError - ); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'bulk_update')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type, 'bulk_update')]: false, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const objects = [{ type, id: `bar-${type}`, attributes: {} }]; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.bulkUpdate(objects, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'bulk_update')], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_update', - [type], - [options.actions.savedObject.get(type, 'bulk_update')], - { objects, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.bulkUpdate when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { - [options.actions.savedObject.get(type, 'bulk_update')]: true, - }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.bulkUpdate.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const objects = [{ type, id: `id-${type}`, attributes: {} }]; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.bulkUpdate(objects, apiCallOptions)).resolves.toBe(apiCallReturnValue); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'bulk_update')], - apiCallOptions.namespace - ); - expect(options.baseClient.bulkUpdate).toHaveBeenCalledWith(objects, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'bulk_update', - [type], - { objects, options: apiCallOptions } - ); - }); +describe('#delete', () => { + const type = 'foo'; + const id = `${type}-id`; + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.delete, { type, id }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + await expectForbiddenError(client.delete, { type, id, options }); + }); + + test(`returns result of internalRepository.delete when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.delete.mockReturnValue(apiCallReturnValue as any); + + const result = await expectSuccess(client.delete, { type, id, options }); + expect(result).toBe(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + await expectPrivilegeCheck(client.delete, { type, id, options }); + }); +}); + +describe('#find', () => { + const type1 = 'foo'; + const type2 = 'bar'; + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.find, { type: type1 }); + }); + + test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => { + const options = Object.freeze({ type: type1, namespace: 'some-ns' }); + await expectForbiddenError(client.find, { options }); + }); + + test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { + const options = Object.freeze({ type: [type1, type2], namespace: 'some-ns' }); + await expectForbiddenError(client.find, { options }); + }); + + test(`returns result of baseClient.find when authorized`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.find.mockReturnValue(apiCallReturnValue as any); + + const options = Object.freeze({ type: type1, namespace: 'some-ns' }); + const result = await expectSuccess(client.find, { options }); + expect(result).toEqual(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + const options = Object.freeze({ type: [type1, type2], namespace: 'some-ns' }); + await expectPrivilegeCheck(client.find, { options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + const options = Object.freeze({ type: [type1, type2], namespace: 'some-ns' }); + await expectObjectsNamespaceFiltering(client.find, { options }); + }); +}); + +describe('#get', () => { + const type = 'foo'; + const id = `${type}-id`; + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.get, { type, id }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + await expectForbiddenError(client.get, { type, id, options }); + }); + + test(`returns result of baseClient.get when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.get.mockReturnValue(apiCallReturnValue as any); + + const result = await expectSuccess(client.get, { type, id, options }); + expect(result).toBe(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + await expectPrivilegeCheck(client.get, { type, id, options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + await expectObjectNamespaceFiltering(client.get, { type, id, options }); + }); +}); + +describe('#deleteFromNamespaces', () => { + const type = 'foo'; + const id = `${type}-id`; + const namespace1 = 'foo-namespace'; + const namespace2 = 'bar-namespace'; + const namespaces = [namespace1, namespace2]; + const privilege = `mock-saved_object:${type}/delete`; + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.deleteFromNamespaces, { type, id, namespaces }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + + await expect(client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrowError( + clientOpts.forbiddenError + ); + + expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + USERNAME, + 'deleteFromNamespaces', // action for privilege check is 'delete', but auditAction is 'deleteFromNamespaces' + [type], + namespaces.sort(), + [{ privilege, spaceId: namespace1 }], + { type, id, namespaces, options: {} } + ); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.deleteFromNamespaces when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(apiCallReturnValue as any); + + const result = await client.deleteFromNamespaces(type, id, namespaces); + expect(result).toBe(apiCallReturnValue); + + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + USERNAME, + 'deleteFromNamespaces', // action for privilege check is 'delete', but auditAction is 'deleteFromNamespaces' + [type], + namespaces.sort(), + { type, id, namespaces, options: {} } + ); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + + await expect(client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrow(); // test is simpler with error case + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(1); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [privilege], + namespaces + ); + }); +}); + +describe('#update', () => { + const type = 'foo'; + const id = `${type}-id`; + const attributes = { some: 'attr' }; + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.update, { type, id, attributes }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + await expectForbiddenError(client.update, { type, id, attributes, options }); + }); + + test(`returns result of baseClient.update when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.update.mockReturnValue(apiCallReturnValue as any); + + const result = await expectSuccess(client.update, { type, id, attributes, options }); + expect(result).toBe(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + await expectPrivilegeCheck(client.update, { type, id, attributes, options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + await expectObjectNamespaceFiltering(client.update, { type, id, attributes, options }); + }); +}); + +describe('other', () => { + test(`assigns errors from constructor to .errors`, () => { + expect(client.errors).toBe(clientOpts.errors); }); }); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 2209e7fb66fcb..29503d475be73 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -13,9 +13,13 @@ import { SavedObjectsCreateOptions, SavedObjectsFindOptions, SavedObjectsUpdateOptions, + SavedObjectsAddToNamespacesOptions, + SavedObjectsDeleteFromNamespacesOptions, } from '../../../../../src/core/server'; import { SecurityAuditLogger } from '../audit'; import { Actions, CheckSavedObjectsPrivileges } from '../authorization'; +import { CheckPrivilegesResponse } from '../authorization/check_privileges'; +import { SpacesService } from '../plugin'; interface SecureSavedObjectsClientWrapperOptions { actions: Actions; @@ -23,6 +27,19 @@ interface SecureSavedObjectsClientWrapperOptions { baseClient: SavedObjectsClientContract; errors: SavedObjectsClientContract['errors']; checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges; + getSpacesService(): SpacesService | undefined; +} + +interface SavedObjectNamespaces { + namespaces?: string[]; +} + +interface SavedObjectsNamespaces { + saved_objects: SavedObjectNamespaces[]; +} + +function uniq(arr: T[]): T[] { + return Array.from(new Set(arr)); } export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContract { @@ -30,19 +47,23 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra private readonly auditLogger: PublicMethodsOf; private readonly baseClient: SavedObjectsClientContract; private readonly checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges; + private getSpacesService: () => SpacesService | undefined; public readonly errors: SavedObjectsClientContract['errors']; + constructor({ actions, auditLogger, baseClient, checkSavedObjectsPrivilegesAsCurrentUser, errors, + getSpacesService, }: SecureSavedObjectsClientWrapperOptions) { this.errors = errors; this.actions = actions; this.auditLogger = auditLogger; this.baseClient = baseClient; this.checkSavedObjectsPrivilegesAsCurrentUser = checkSavedObjectsPrivilegesAsCurrentUser; + this.getSpacesService = getSpacesService; } public async create( @@ -52,7 +73,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { await this.ensureAuthorized(type, 'create', options.namespace, { type, attributes, options }); - return await this.baseClient.create(type, attributes, options); + const savedObject = await this.baseClient.create(type, attributes, options); + return await this.redactSavedObjectNamespaces(savedObject); } public async bulkCreate( @@ -66,7 +88,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra { objects, options } ); - return await this.baseClient.bulkCreate(objects, options); + const response = await this.baseClient.bulkCreate(objects, options); + return await this.redactSavedObjectsNamespaces(response); } public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) { @@ -78,7 +101,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra public async find(options: SavedObjectsFindOptions) { await this.ensureAuthorized(options.type, 'find', options.namespace, { options }); - return this.baseClient.find(options); + const response = await this.baseClient.find(options); + return await this.redactSavedObjectsNamespaces(response); } public async bulkGet( @@ -90,13 +114,15 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra options, }); - return await this.baseClient.bulkGet(objects, options); + const response = await this.baseClient.bulkGet(objects, options); + return await this.redactSavedObjectsNamespaces(response); } public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { await this.ensureAuthorized(type, 'get', options.namespace, { type, id, options }); - return await this.baseClient.get(type, id, options); + const savedObject = await this.baseClient.get(type, id, options); + return await this.redactSavedObjectNamespaces(savedObject); } public async update( @@ -105,14 +131,44 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra attributes: Partial, options: SavedObjectsUpdateOptions = {} ) { - await this.ensureAuthorized(type, 'update', options.namespace, { - type, - id, - attributes, - options, - }); + const args = { type, id, attributes, options }; + await this.ensureAuthorized(type, 'update', options.namespace, args); - return await this.baseClient.update(type, id, attributes, options); + const savedObject = await this.baseClient.update(type, id, attributes, options); + return await this.redactSavedObjectNamespaces(savedObject); + } + + public async addToNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsAddToNamespacesOptions = {} + ) { + const args = { type, id, namespaces, options }; + const { namespace } = options; + // To share an object, the user must have the "create" permission in each of the destination namespaces. + await this.ensureAuthorized(type, 'create', namespaces, args, 'addToNamespacesCreate'); + + // To share an object, the user must also have the "update" permission in one or more of the source namespaces. Because the + // `addToNamespaces` operation is scoped to the current namespace, we can just check if the user has the "update" permission in the + // current namespace. If the user has permission, but the saved object doesn't exist in this namespace, the base client operation will + // result in a 404 error. + await this.ensureAuthorized(type, 'update', namespace, args, 'addToNamespacesUpdate'); + + return await this.baseClient.addToNamespaces(type, id, namespaces, options); + } + + public async deleteFromNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsDeleteFromNamespacesOptions = {} + ) { + const args = { type, id, namespaces, options }; + // To un-share an object, the user must have the "delete" permission in each of the target namespaces. + await this.ensureAuthorized(type, 'delete', namespaces, args, 'deleteFromNamespaces'); + + return await this.baseClient.deleteFromNamespaces(type, id, namespaces, options); } public async bulkUpdate( @@ -126,12 +182,16 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra { objects, options } ); - return await this.baseClient.bulkUpdate(objects, options); + const response = await this.baseClient.bulkUpdate(objects, options); + return await this.redactSavedObjectsNamespaces(response); } - private async checkPrivileges(actions: string | string[], namespace?: string) { + private async checkPrivileges( + actions: string | string[], + namespaceOrNamespaces?: string | string[] + ) { try { - return await this.checkSavedObjectsPrivilegesAsCurrentUser(actions, namespace); + return await this.checkSavedObjectsPrivilegesAsCurrentUser(actions, namespaceOrNamespaces); } catch (error) { throw this.errors.decorateGeneralError(error, error.body && error.body.reason); } @@ -140,43 +200,133 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra private async ensureAuthorized( typeOrTypes: string | string[], action: string, - namespace?: string, - args?: Record + namespaceOrNamespaces?: string | string[], + args?: Record, + auditAction: string = action, + requiresAll = true ) { const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; const actionsToTypesMap = new Map( types.map(type => [this.actions.savedObject.get(type, action), type]) ); const actions = Array.from(actionsToTypesMap.keys()); - const { hasAllRequested, username, privileges } = await this.checkPrivileges( - actions, - namespace - ); + const result = await this.checkPrivileges(actions, namespaceOrNamespaces); + + const { hasAllRequested, username, privileges } = result; + const spaceIds = uniq( + privileges.map(({ resource }) => resource).filter(x => x !== undefined) + ).sort() as string[]; - if (hasAllRequested) { - this.auditLogger.savedObjectsAuthorizationSuccess(username, action, types, args); + const isAuthorized = + (requiresAll && hasAllRequested) || + (!requiresAll && privileges.some(({ authorized }) => authorized)); + if (isAuthorized) { + this.auditLogger.savedObjectsAuthorizationSuccess( + username, + auditAction, + types, + spaceIds, + args + ); } else { const missingPrivileges = this.getMissingPrivileges(privileges); this.auditLogger.savedObjectsAuthorizationFailure( username, - action, + auditAction, types, + spaceIds, missingPrivileges, args ); - const msg = `Unable to ${action} ${missingPrivileges - .map(privilege => actionsToTypesMap.get(privilege)) - .sort() - .join(',')}`; + const targetTypes = uniq( + missingPrivileges.map(({ privilege }) => actionsToTypesMap.get(privilege)).sort() + ).join(','); + const msg = `Unable to ${action} ${targetTypes}`; throw this.errors.decorateForbiddenError(new Error(msg)); } } - private getMissingPrivileges(privileges: Record) { - return Object.keys(privileges).filter(privilege => !privileges[privilege]); + private getMissingPrivileges(privileges: CheckPrivilegesResponse['privileges']) { + return privileges + .filter(({ authorized }) => !authorized) + .map(({ resource, privilege }) => ({ spaceId: resource, privilege })); } private getUniqueObjectTypes(objects: Array<{ type: string }>) { - return [...new Set(objects.map(o => o.type))]; + return uniq(objects.map(o => o.type)); + } + + private async getNamespacesPrivilegeMap(namespaces: string[]) { + const action = this.actions.login; + const checkPrivilegesResult = await this.checkPrivileges(action, namespaces); + // check if the user can log into each namespace + const map = checkPrivilegesResult.privileges.reduce( + (acc: Record, { resource, authorized }) => { + // there should never be a case where more than one privilege is returned for a given space + // if there is, fail-safe (authorized + unauthorized = unauthorized) + if (resource && (!authorized || !acc.hasOwnProperty(resource))) { + acc[resource] = authorized; + } + return acc; + }, + {} + ); + return map; + } + + private redactAndSortNamespaces(spaceIds: string[], privilegeMap: Record) { + const comparator = (a: string, b: string) => { + const _a = a.toLowerCase(); + const _b = b.toLowerCase(); + if (_a === '?') { + return 1; + } else if (_a < _b) { + return -1; + } else if (_a > _b) { + return 1; + } + return 0; + }; + return spaceIds.map(spaceId => (privilegeMap[spaceId] ? spaceId : '?')).sort(comparator); + } + + private async redactSavedObjectNamespaces( + savedObject: T + ): Promise { + if (this.getSpacesService() === undefined || savedObject.namespaces == null) { + return savedObject; + } + + const privilegeMap = await this.getNamespacesPrivilegeMap(savedObject.namespaces); + + return { + ...savedObject, + namespaces: this.redactAndSortNamespaces(savedObject.namespaces, privilegeMap), + }; + } + + private async redactSavedObjectsNamespaces( + response: T + ): Promise { + if (this.getSpacesService() === undefined) { + return response; + } + const { saved_objects: savedObjects } = response; + const namespaces = uniq(savedObjects.flatMap(savedObject => savedObject.namespaces || [])); + if (namespaces.length === 0) { + return response; + } + + const privilegeMap = await this.getNamespacesPrivilegeMap(namespaces); + + return { + ...response, + saved_objects: savedObjects.map(savedObject => ({ + ...savedObject, + namespaces: + savedObject.namespaces && + this.redactAndSortNamespaces(savedObject.namespaces, privilegeMap), + })), + }; } } diff --git a/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx b/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx index 28e45bc8cfd2a..ea63905e27b26 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx @@ -99,10 +99,14 @@ export class DeleteSpacesButton extends Component { public deleteSpaces = async () => { const { spacesManager, space } = this.props; + this.setState({ + showConfirmDeleteModal: false, + }); + try { await spacesManager.deleteSpace(space); } catch (error) { - const { message: errorMessage = '' } = error.data || {}; + const { message: errorMessage = '' } = error.data || error.body || {}; this.props.notifications.toasts.addDanger( i18n.translate('xpack.spaces.management.deleteSpacesButton.deleteSpaceErrorTitle', { @@ -110,12 +114,9 @@ export class DeleteSpacesButton extends Component { values: { errorMessage }, }) ); + return; } - this.setState({ - showConfirmDeleteModal: false, - }); - const message = i18n.translate( 'xpack.spaces.management.deleteSpacesButton.spaceSuccessfullyDeletedNotificationMessage', { diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index ff4be84207832..df5e6a2ca34af 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -176,10 +176,14 @@ export class SpacesGridPage extends Component { return; } + this.setState({ + showConfirmDeleteModal: false, + }); + try { await spacesManager.deleteSpace(space); } catch (error) { - const { message: errorMessage = '' } = error.data || {}; + const { message: errorMessage = '' } = error.data || error.body || {}; this.props.notifications.toasts.addDanger( i18n.translate('xpack.spaces.management.spacesGridPage.errorDeletingSpaceErrorMessage', { @@ -189,12 +193,9 @@ export class SpacesGridPage extends Component { }, }) ); + return; } - this.setState({ - showConfirmDeleteModal: false, - }); - this.loadGrid(); const message = i18n.translate( diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 59e157c3fc2db..72faab0d2c892 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -188,11 +188,13 @@ describe('copySavedObjectsToSpaces', () => { }, ], "savedObjectsClient": Object { + "addToNamespaces": [MockFunction], "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], "create": [MockFunction], "delete": [MockFunction], + "deleteFromNamespaces": [MockFunction], "errors": [Function], "find": [MockFunction], "get": [MockFunction], @@ -252,11 +254,13 @@ describe('copySavedObjectsToSpaces', () => { "readable": false, }, "savedObjectsClient": Object { + "addToNamespaces": [MockFunction], "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], "create": [MockFunction], "delete": [MockFunction], + "deleteFromNamespaces": [MockFunction], "errors": [Function], "find": [MockFunction], "get": [MockFunction], @@ -315,11 +319,13 @@ describe('copySavedObjectsToSpaces', () => { "readable": false, }, "savedObjectsClient": Object { + "addToNamespaces": [MockFunction], "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], "create": [MockFunction], "delete": [MockFunction], + "deleteFromNamespaces": [MockFunction], "errors": [Function], "find": [MockFunction], "get": [MockFunction], diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 7809f1f8be66f..aa1d5e9a47832 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -204,11 +204,13 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, ], "savedObjectsClient": Object { + "addToNamespaces": [MockFunction], "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], "create": [MockFunction], "delete": [MockFunction], + "deleteFromNamespaces": [MockFunction], "errors": [Function], "find": [MockFunction], "get": [MockFunction], @@ -275,11 +277,13 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, ], "savedObjectsClient": Object { + "addToNamespaces": [MockFunction], "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], "create": [MockFunction], "delete": [MockFunction], + "deleteFromNamespaces": [MockFunction], "errors": [Function], "find": [MockFunction], "get": [MockFunction], @@ -345,11 +349,13 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, ], "savedObjectsClient": Object { + "addToNamespaces": [MockFunction], "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], "create": [MockFunction], "delete": [MockFunction], + "deleteFromNamespaces": [MockFunction], "errors": [Function], "find": [MockFunction], "get": [MockFunction], diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts index 74e75fb8f12c7..c83830f6feace 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts @@ -250,14 +250,10 @@ describe('#getAll', () => { mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); mockCheckPrivilegesAtSpaces.mockReturnValue({ username, - spacePrivileges: { - [savedObjects[0].id]: { - [privilege]: false, - }, - [savedObjects[1].id]: { - [privilege]: false, - }, - }, + privileges: [ + { resource: savedObjects[0].id, privilege, authorized: false }, + { resource: savedObjects[1].id, privilege, authorized: false }, + ], }); const maxSpaces = 1234; const mockConfig = createMockConfig({ @@ -314,14 +310,10 @@ describe('#getAll', () => { mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); mockCheckPrivilegesAtSpaces.mockReturnValue({ username, - spacePrivileges: { - [savedObjects[0].id]: { - [privilege]: true, - }, - [savedObjects[1].id]: { - [privilege]: false, - }, - }, + privileges: [ + { resource: savedObjects[0].id, privilege, authorized: true }, + { resource: savedObjects[1].id, privilege, authorized: false }, + ], }); const mockInternalRepository = { find: jest.fn().mockReturnValue({ diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index 22c34c03368e3..0c066fb76994f 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -74,16 +74,14 @@ export class SpacesClient { const privilege = privilegeFactory(this.authorization!); - const { username, spacePrivileges } = await checkPrivileges.atSpaces(spaceIds, privilege); + const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, privilege); - const authorized = Object.keys(spacePrivileges).filter(spaceId => { - return spacePrivileges[spaceId][privilege]; - }); + const authorized = privileges.filter(x => x.authorized).map(x => x.resource); this.debugLogger( `SpacesClient.getAll(), authorized for ${ authorized.length - } spaces, derived from ES privilege check: ${JSON.stringify(spacePrivileges)}` + } spaces, derived from ES privilege check: ${JSON.stringify(privileges)}` ); if (authorized.length === 0) { @@ -94,7 +92,7 @@ export class SpacesClient { throw Boom.forbidden(); } - this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorized); + this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorized as string[]); const filteredSpaces: Space[] = spaces.filter((space: any) => authorized.includes(space.id)); this.debugLogger( `SpacesClient.getAll(), using RBAC. returning spaces: ${filteredSpaces @@ -211,9 +209,9 @@ export class SpacesClient { throw Boom.badRequest('This Space cannot be deleted because it is reserved.'); } - await repository.delete('space', id); - await repository.deleteByNamespace(id); + + await repository.delete('space', id); } private useRbac(): boolean { diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index f2ba8785f5a3f..511e9676940d2 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -11,7 +11,13 @@ import { mockRouteContext, mockRouteContextWithInvalidLicense, } from '../__fixtures__'; -import { CoreSetup, IRouter, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; +import { + CoreSetup, + IRouter, + kibanaResponseFactory, + RouteValidatorConfig, + SavedObjectsErrorHelpers, +} from 'src/core/server'; import { loggingServiceMock, httpServiceMock, @@ -75,6 +81,7 @@ describe('Spaces Public API', () => { return { routeValidation: routeDefinition.validate as RouteValidatorConfig<{}, {}, {}>, routeHandler, + savedObjectsRepositoryMock, }; }; @@ -143,6 +150,27 @@ describe('Spaces Public API', () => { expect(status).toEqual(404); }); + it(`returns http/400 when scripts cannot be executed in Elasticsearch`, async () => { + const { routeHandler, savedObjectsRepositoryMock } = await setup(); + + const request = httpServerMock.createKibanaRequest({ + params: { + id: 'a-space', + }, + method: 'delete', + }); + // @ts-ignore + savedObjectsRepositoryMock.deleteByNamespace.mockRejectedValue( + SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(new Error()) + ); + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + const { status, payload } = response; + + expect(status).toEqual(400); + expect(payload.message).toEqual('Cannot execute script in Elasticsearch query'); + }); + it(`DELETE spaces/{id}' cannot delete reserved spaces`, async () => { const { routeHandler } = await setup(); diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.ts index 4b7e6b00182ac..150f1d198cdf6 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import Boom from 'boom'; import { schema } from '@kbn/config-schema'; import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server'; import { wrapError } from '../../../lib/errors'; @@ -12,7 +13,7 @@ import { ExternalRouteDeps } from '.'; import { createLicensedRouteHandler } from '../../lib'; export function initDeleteSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, spacesService } = deps; + const { externalRouter, log, spacesService } = deps; externalRouter.delete( { @@ -33,6 +34,13 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) { } catch (error) { if (SavedObjectsErrorHelpers.isNotFoundError(error)) { return response.notFound(); + } else if (SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)) { + log.error( + `Failed to delete space '${id}', cannot execute script in Elasticsearch query: ${error.message}` + ); + return response.customError( + wrapError(Boom.badRequest('Cannot execute script in Elasticsearch query')) + ); } return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/index.ts b/x-pack/plugins/spaces/server/routes/api/external/index.ts index 1bdb7ceb8a3f7..079f690bfe546 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/index.ts @@ -12,6 +12,8 @@ import { initPostSpacesApi } from './post'; import { initPutSpacesApi } from './put'; import { SpacesServiceSetup } from '../../../spaces_service/spaces_service'; import { initCopyToSpacesApi } from './copy_to_space'; +import { initShareAddSpacesApi } from './share_add_spaces'; +import { initShareRemoveSpacesApi } from './share_remove_spaces'; export interface ExternalRouteDeps { externalRouter: IRouter; @@ -28,4 +30,6 @@ export function initExternalSpacesApi(deps: ExternalRouteDeps) { initPostSpacesApi(deps); initPutSpacesApi(deps); initCopyToSpacesApi(deps); + initShareAddSpacesApi(deps); + initShareRemoveSpacesApi(deps); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/share_add_spaces.ts b/x-pack/plugins/spaces/server/routes/api/external/share_add_spaces.ts new file mode 100644 index 0000000000000..f40cc5cc50572 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/external/share_add_spaces.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { wrapError } from '../../../lib/errors'; +import { ExternalRouteDeps } from '.'; +import { SPACE_ID_REGEX } from '../../../lib/space_schema'; +import { createLicensedRouteHandler } from '../../lib'; + +const uniq = (arr: T[]): T[] => Array.from(new Set(arr)); +export function initShareAddSpacesApi(deps: ExternalRouteDeps) { + const { externalRouter, getStartServices } = deps; + + externalRouter.post( + { + path: '/api/spaces/_share_saved_object_add', + validate: { + body: schema.object({ + spaces: schema.arrayOf( + schema.string({ + validate: value => { + if (!SPACE_ID_REGEX.test(value)) { + return `lower case, a-z, 0-9, "_", and "-" are allowed`; + } + }, + }), + { + validate: spaceIds => { + if (!spaceIds.length) { + return 'must specify one or more space ids'; + } else if (uniq(spaceIds).length !== spaceIds.length) { + return 'duplicate space ids are not allowed'; + } + }, + } + ), + object: schema.object({ + type: schema.string(), + id: schema.string(), + }), + }), + }, + }, + createLicensedRouteHandler(async (_context, request, response) => { + const [startServices] = await getStartServices(); + const scopedClient = startServices.savedObjects.getScopedClient(request); + + const spaces = request.body.spaces; + const { type, id } = request.body.object; + + try { + await scopedClient.addToNamespaces(type, id, spaces); + } catch (error) { + return response.customError(wrapError(error)); + } + return response.noContent(); + }) + ); +} diff --git a/x-pack/plugins/spaces/server/routes/api/external/share_remove_spaces.ts b/x-pack/plugins/spaces/server/routes/api/external/share_remove_spaces.ts new file mode 100644 index 0000000000000..5f58a5dfd5e5f --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/external/share_remove_spaces.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { wrapError } from '../../../lib/errors'; +import { ExternalRouteDeps } from '.'; +import { SPACE_ID_REGEX } from '../../../lib/space_schema'; +import { createLicensedRouteHandler } from '../../lib'; + +const uniq = (arr: T[]): T[] => Array.from(new Set(arr)); +export function initShareRemoveSpacesApi(deps: ExternalRouteDeps) { + const { externalRouter, getStartServices } = deps; + + externalRouter.post( + { + path: '/api/spaces/_share_saved_object_remove', + validate: { + body: schema.object({ + spaces: schema.arrayOf( + schema.string({ + validate: value => { + if (!SPACE_ID_REGEX.test(value)) { + return `lower case, a-z, 0-9, "_", and "-" are allowed`; + } + }, + }), + { + validate: spaceIds => { + if (!spaceIds.length) { + return 'must specify one or more space ids'; + } else if (uniq(spaceIds).length !== spaceIds.length) { + return 'duplicate space ids are not allowed'; + } + }, + } + ), + object: schema.object({ + type: schema.string(), + id: schema.string(), + }), + }), + }, + }, + createLicensedRouteHandler(async (_context, request, response) => { + const [startServices] = await getStartServices(); + const scopedClient = startServices.savedObjects.getScopedClient(request); + + const spaces = request.body.spaces; + const { type, id } = request.body.object; + + try { + await scopedClient.deleteFromNamespaces(type, id, spaces); + } catch (error) { + return response.customError(wrapError(error)); + } + return response.noContent(); + }) + ); +} diff --git a/x-pack/plugins/spaces/server/saved_objects/__snapshots__/spaces_saved_objects_client.test.ts.snap b/x-pack/plugins/spaces/server/saved_objects/__snapshots__/spaces_saved_objects_client.test.ts.snap deleted file mode 100644 index 8b1a258138355..0000000000000 --- a/x-pack/plugins/spaces/server/saved_objects/__snapshots__/spaces_saved_objects_client.test.ts.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`default space #bulkCreate throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`default space #bulkGet throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`default space #create throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`default space #delete throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`default space #find throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`default space #get throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`default space #update throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #bulkCreate throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #bulkGet throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #create throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #delete throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #find throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #get throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #update throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index 2d6fe36792c40..f9961329c088b 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -50,42 +50,41 @@ const createMockResponse = () => ({ references: [], }); +const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; + [ { id: DEFAULT_SPACE_ID, expectedNamespace: undefined }, { id: 'space_1', expectedNamespace: 'space_1' }, ].forEach(currentSpace => { describe(`${currentSpace.id} space`, () => { + const createSpacesSavedObjectsClient = async () => { + const request = createMockRequest(); + const baseClient = createMockClient(); + const spacesService = await createSpacesService(currentSpace.id); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + typeRegistry, + }); + return { client, baseClient }; + }; + describe('#get', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); - await expect( - client.get('foo', '', { namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.get('foo', '', { namespace: 'bar' })).rejects.toThrow( + ERROR_NAMESPACE_SPECIFIED + ); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = createMockResponse(); baseClient.get.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const type = Symbol(); const id = Symbol(); const options = Object.freeze({ foo: 'bar' }); @@ -102,37 +101,17 @@ const createMockResponse = () => ({ describe('#bulkGet', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); await expect( client.bulkGet([{ id: '', type: 'foo' }], { namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const expectedReturnValue = { - saved_objects: [createMockResponse()], - }; + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); + const expectedReturnValue = { saved_objects: [createMockResponse()] }; baseClient.bulkGet.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const objects = [{ type: 'foo' }]; const options = Object.freeze({ foo: 'bar' }); @@ -149,25 +128,15 @@ const createMockResponse = () => ({ describe('#find', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); - await expect( - client.find({ type: 'foo', namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.find({ type: 'foo', namespace: 'bar' })).rejects.toThrow( + ERROR_NAMESPACE_SPECIFIED + ); }); test(`passes options.type to baseClient if valid singular type specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); + const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = { saved_objects: [createMockResponse()], total: 1, @@ -175,16 +144,8 @@ const createMockResponse = () => ({ page: 0, }; baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const options = Object.freeze({ type: 'foo' }); - const actualReturnValue = await client.find(options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -194,9 +155,8 @@ const createMockResponse = () => ({ }); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = { saved_objects: [createMockResponse()], total: 1, @@ -204,14 +164,6 @@ const createMockResponse = () => ({ page: 0, }; baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const options = Object.freeze({ type: ['foo', 'bar'] }); const actualReturnValue = await client.find(options); @@ -226,35 +178,17 @@ const createMockResponse = () => ({ describe('#create', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); - await expect( - client.create('foo', {}, { namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.create('foo', {}, { namespace: 'bar' })).rejects.toThrow( + ERROR_NAMESPACE_SPECIFIED + ); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = createMockResponse(); baseClient.create.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const type = Symbol(); const attributes = Symbol(); @@ -272,37 +206,17 @@ const createMockResponse = () => ({ describe('#bulkCreate', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); await expect( client.bulkCreate([{ id: '', type: 'foo', attributes: {} }], { namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const expectedReturnValue = { - saved_objects: [createMockResponse()], - }; + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); + const expectedReturnValue = { saved_objects: [createMockResponse()] }; baseClient.bulkCreate.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const objects = [{ type: 'foo' }]; const options = Object.freeze({ foo: 'bar' }); @@ -319,36 +233,18 @@ const createMockResponse = () => ({ describe('#update', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); await expect( // @ts-ignore client.update(null, null, null, { namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = createMockResponse(); baseClient.update.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const type = Symbol(); const id = Symbol(); @@ -366,21 +262,19 @@ const createMockResponse = () => ({ }); describe('#bulkUpdate', () => { - test(`supplements options with the spaces namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const expectedReturnValue = { - saved_objects: [createMockResponse()], - }; - baseClient.bulkUpdate.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); + test(`throws error if options.namespace is specified`, async () => { + const { client } = await createSpacesSavedObjectsClient(); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + await expect( + // @ts-ignore + client.bulkUpdate(null, { namespace: 'bar' }) + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); + const expectedReturnValue = { saved_objects: [createMockResponse()] }; + baseClient.bulkUpdate.mockReturnValue(Promise.resolve(expectedReturnValue)); const actualReturnValue = await client.bulkUpdate([ { id: 'id', type: 'foo', attributes: {}, references: [] }, @@ -403,36 +297,18 @@ const createMockResponse = () => ({ describe('#delete', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); await expect( // @ts-ignore client.delete(null, null, { namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = createMockResponse(); baseClient.delete.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const type = Symbol(); const id = Symbol(); @@ -447,5 +323,65 @@ const createMockResponse = () => ({ }); }); }); + + describe('#addToNamespaces', () => { + test(`throws error if options.namespace is specified`, async () => { + const { client } = await createSpacesSavedObjectsClient(); + + await expect( + // @ts-ignore + client.addToNamespaces(null, null, null, { namespace: 'bar' }) + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); + const expectedReturnValue = createMockResponse(); + baseClient.addToNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue)); + + const type = Symbol(); + const id = Symbol(); + const namespaces = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + // @ts-ignore + const actualReturnValue = await client.addToNamespaces(type, id, namespaces, options); + + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.addToNamespaces).toHaveBeenCalledWith(type, id, namespaces, { + foo: 'bar', + namespace: currentSpace.expectedNamespace, + }); + }); + }); + + describe('#deleteFromNamespaces', () => { + test(`throws error if options.namespace is specified`, async () => { + const { client } = await createSpacesSavedObjectsClient(); + + await expect( + // @ts-ignore + client.deleteFromNamespaces(null, null, null, { namespace: 'bar' }) + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); + const expectedReturnValue = createMockResponse(); + baseClient.deleteFromNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue)); + + const type = Symbol(); + const id = Symbol(); + const namespaces = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + // @ts-ignore + const actualReturnValue = await client.deleteFromNamespaces(type, id, namespaces, options); + + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.deleteFromNamespaces).toHaveBeenCalledWith(type, id, namespaces, { + foo: 'bar', + namespace: currentSpace.expectedNamespace, + }); + }); + }); }); }); diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index f216d5743cf89..e31bc7cef6900 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -13,6 +13,8 @@ import { SavedObjectsCreateOptions, SavedObjectsFindOptions, SavedObjectsUpdateOptions, + SavedObjectsAddToNamespacesOptions, + SavedObjectsDeleteFromNamespacesOptions, ISavedObjectTypeRegistry, } from 'src/core/server'; import { SpacesServiceSetup } from '../spaces_service/spaces_service'; @@ -213,6 +215,50 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { }); } + /** + * Adds namespaces to a SavedObject + * + * @param type + * @param id + * @param namespaces + * @param options + */ + public async addToNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsAddToNamespacesOptions = {} + ) { + throwErrorIfNamespaceSpecified(options); + + return await this.client.addToNamespaces(type, id, namespaces, { + ...options, + namespace: spaceIdToNamespace(this.spaceId), + }); + } + + /** + * Removes namespaces from a SavedObject + * + * @param type + * @param id + * @param namespaces + * @param options + */ + public async deleteFromNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsDeleteFromNamespacesOptions = {} + ) { + throwErrorIfNamespaceSpecified(options); + + return await this.client.deleteFromNamespaces(type, id, namespaces, { + ...options, + namespace: spaceIdToNamespace(this.spaceId), + }); + } + /** * Updates an array of objects by id * diff --git a/x-pack/plugins/uptime/server/rest_api/types.ts b/x-pack/plugins/uptime/server/rest_api/types.ts index aecb099b7bed5..e05e7a4d7faf1 100644 --- a/x-pack/plugins/uptime/server/rest_api/types.ts +++ b/x-pack/plugins/uptime/server/rest_api/types.ts @@ -10,7 +10,7 @@ import { RouteConfig, RouteMethod, CallAPIOptions, - SavedObjectsClient, + SavedObjectsClientContract, RequestHandlerContext, KibanaRequest, KibanaResponseFactory, @@ -69,18 +69,7 @@ export interface UMRouteParams { options?: CallAPIOptions | undefined ) => Promise; dynamicSettings: DynamicSettings; - savedObjectsClient: Pick< - SavedObjectsClient, - | 'errors' - | 'create' - | 'bulkCreate' - | 'delete' - | 'find' - | 'bulkGet' - | 'get' - | 'update' - | 'bulkUpdate' - >; + savedObjectsClient: SavedObjectsClientContract; } /** diff --git a/x-pack/test/api_integration/apis/spaces/saved_objects.ts b/x-pack/test/api_integration/apis/spaces/saved_objects.ts index 05f14a50f2170..7584be7fd8498 100644 --- a/x-pack/test/api_integration/apis/spaces/saved_objects.ts +++ b/x-pack/test/api_integration/apis/spaces/saved_objects.ts @@ -89,7 +89,7 @@ export default function({ getService }: FtrProviderContext) { .expect(404) .then((response: Record) => { expect(response.body).to.eql({ - message: 'Not Found', + message: 'Saved object [space/default] not found', statusCode: 404, error: 'Not Found', }); diff --git a/x-pack/test/saved_object_api_integration/common/config.ts b/x-pack/test/saved_object_api_integration/common/config.ts index dda7934ce875a..eaf7230a832a8 100644 --- a/x-pack/test/saved_object_api_integration/common/config.ts +++ b/x-pack/test/saved_object_api_integration/common/config.ts @@ -56,8 +56,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...config.xpack.api.get('kbnTestServer.serverArgs'), '--optimize.enabled=false', '--server.xsrf.disableProtection=true', + `--plugin-path=${path.join(__dirname, 'fixtures', 'isolated_type_plugin')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'namespace_agnostic_type_plugin')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'hidden_type_plugin')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'shared_type_plugin')}`, ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), ], }, diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 34361ad9df542..d2c14189e2529 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -53,7 +53,7 @@ { "type": "doc", "value": { - "id": "index-pattern:91200a00-9efd-11e7-acb3-3dab96693fab", + "id": "index-pattern:defaultspace-index-pattern-id", "index": ".kibana", "source": { "index-pattern": { @@ -76,7 +76,7 @@ "source": { "config": { "buildNum": 8467, - "defaultIndex": "91200a00-9efd-11e7-acb3-3dab96693fab" + "defaultIndex": "defaultspace-index-pattern-id" }, "type": "config", "updated_at": "2017-09-21T18:49:16.302Z" @@ -88,15 +88,15 @@ { "type": "doc", "value": { - "id": "visualization:dd7caf20-9efd-11e7-acb3-3dab96693fab", + "id": "isolatedtype:defaultspace-isolatedtype-id", "index": ".kibana", "source": { - "type": "visualization", + "type": "isolatedtype", "updated_at": "2017-09-21T18:51:23.794Z", - "visualization": { + "isolatedtype": { "description": "", "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + "searchSourceJSON": "{\"index\":\"defaultspace-index-pattern-id\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" }, "title": "Count of requests", "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", @@ -111,7 +111,7 @@ { "type": "doc", "value": { - "id": "dashboard:be3733a0-9efe-11e7-acb3-3dab96693fab", + "id": "dashboard:defaultspace-dashboard-id", "index": ".kibana", "source": { "dashboard": { @@ -121,7 +121,7 @@ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" }, "optionsJSON": "{}", - "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]", + "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"isolatedtype\",\"id\":\"defaultspace-isolatedtype-id\",\"col\":1,\"row\":1}]", "refreshInterval": { "display": "Off", "pause": false, @@ -144,7 +144,7 @@ { "type": "doc", "value": { - "id": "space_1:index-pattern:space_1-91200a00-9efd-11e7-acb3-3dab96693fab", + "id": "space_1:index-pattern:space1-index-pattern-id", "index": ".kibana", "source": { "index-pattern": { @@ -168,7 +168,7 @@ "source": { "config": { "buildNum": 8467, - "defaultIndex": "91200a00-9efd-11e7-acb3-3dab96693fab" + "defaultIndex": "defaultspace-index-pattern-id" }, "namespace": "space_1", "type": "config", @@ -181,16 +181,16 @@ { "type": "doc", "value": { - "id": "space_1:visualization:space_1-dd7caf20-9efd-11e7-acb3-3dab96693fab", + "id": "space_1:isolatedtype:space1-isolatedtype-id", "index": ".kibana", "source": { "namespace": "space_1", - "type": "visualization", + "type": "isolatedtype", "updated_at": "2017-09-21T18:51:23.794Z", - "visualization": { + "isolatedtype": { "description": "", "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"space_1-91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + "searchSourceJSON": "{\"index\":\"space1-index-pattern-id\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" }, "title": "Count of requests", "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", @@ -205,7 +205,7 @@ { "type": "doc", "value": { - "id": "space_1:dashboard:space_1-be3733a0-9efe-11e7-acb3-3dab96693fab", + "id": "space_1:dashboard:space1-dashboard-id", "index": ".kibana", "source": { "dashboard": { @@ -215,7 +215,7 @@ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" }, "optionsJSON": "{}", - "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"space_1-dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]", + "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"isolatedtype\",\"id\":\"space1-isolatedtype-id\",\"col\":1,\"row\":1}]", "refreshInterval": { "display": "Off", "pause": false, @@ -239,7 +239,7 @@ { "type": "doc", "value": { - "id": "space_2:index-pattern:space_2-91200a00-9efd-11e7-acb3-3dab96693fab", + "id": "space_2:index-pattern:space2-index-pattern-id", "index": ".kibana", "source": { "index-pattern": { @@ -263,7 +263,7 @@ "source": { "config": { "buildNum": 8467, - "defaultIndex": "91200a00-9efd-11e7-acb3-3dab96693fab" + "defaultIndex": "defaultspace-index-pattern-id" }, "namespace": "space_2", "type": "config", @@ -276,16 +276,16 @@ { "type": "doc", "value": { - "id": "space_2:visualization:space_2-dd7caf20-9efd-11e7-acb3-3dab96693fab", + "id": "space_2:isolatedtype:space2-isolatedtype-id", "index": ".kibana", "source": { "namespace": "space_2", - "type": "visualization", + "type": "isolatedtype", "updated_at": "2017-09-21T18:51:23.794Z", - "visualization": { + "isolatedtype": { "description": "", "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"space_2-91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + "searchSourceJSON": "{\"index\":\"space2-index-pattern-id\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" }, "title": "Count of requests", "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", @@ -300,7 +300,7 @@ { "type": "doc", "value": { - "id": "space_2:dashboard:space_2-be3733a0-9efe-11e7-acb3-3dab96693fab", + "id": "space_2:dashboard:space2-dashboard-id", "index": ".kibana", "source": { "dashboard": { @@ -310,7 +310,7 @@ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" }, "optionsJSON": "{}", - "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"space_2-dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]", + "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"isolatedtype\",\"id\":\"space2-isolatedtype-id\",\"col\":1,\"row\":1}]", "refreshInterval": { "display": "Off", "pause": false, @@ -334,11 +334,11 @@ { "type": "doc", "value": { - "id": "globaltype:8121a00-8efd-21e7-1cb3-34ab966434445", + "id": "globaltype:globaltype-id", "index": ".kibana", "source": { "globaltype": { - "name": "My favorite global object" + "title": "My favorite global object" }, "type": "globaltype", "updated_at": "2017-09-21T18:59:16.270Z" @@ -346,3 +346,54 @@ "type": "doc" } } + +{ + "type": "doc", + "value": { + "id": "sharedtype:default_and_space_1", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the default and space_1 spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:only_space_1", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object only in space_1" + }, + "type": "sharedtype", + "namespaces": ["space_1"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:only_space_2", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object only in space_2" + }, + "type": "sharedtype", + "namespaces": ["space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index c2489f2a906c8..7b5b1d86f6bcc 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -82,7 +82,7 @@ }, "globaltype": { "properties": { - "name": { + "title": { "fields": { "keyword": { "ignore_above": 2048, @@ -147,9 +147,41 @@ } } }, + "isolatedtype": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, "namespace": { "type": "keyword" }, + "namespaces": { + "type": "keyword" + }, "search": { "properties": { "columns": { @@ -186,6 +218,19 @@ } } }, + "sharedtype": { + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, "space": { "properties": { "_reserved": { @@ -282,35 +327,6 @@ "type": "text" } } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } } } }, @@ -322,4 +338,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/index.js b/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/index.js index ea32811794c47..5989db84e2290 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/index.js +++ b/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/index.js @@ -21,5 +21,7 @@ export default function(kibana) { }, config() {}, + + init() {}, // need empty init for plugin to load }); } diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/mappings.json index e4815273964a1..45f898e10e2ba 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/mappings.json @@ -1,7 +1,7 @@ { "hiddentype": { "properties": { - "name": { + "title": { "type": "text", "fields": { "keyword": { diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/index.js b/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/index.js new file mode 100644 index 0000000000000..a406c6737da5f --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/index.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import mappings from './mappings.json'; + +export default function(kibana) { + return new kibana.Plugin({ + require: ['kibana', 'elasticsearch', 'xpack_main'], + name: 'isolated_type_plugin', + uiExports: { + savedObjectsManagement: { + isolatedtype: { + isImportableAndExportable: true, + }, + }, + mappings, + }, + + config() {}, + + init() {}, // need empty init for plugin to load + }); +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/mappings.json new file mode 100644 index 0000000000000..141ebbc93c290 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/mappings.json @@ -0,0 +1,31 @@ +{ + "isolatedtype": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/package.json b/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/package.json new file mode 100644 index 0000000000000..665ecb1b31d7e --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/package.json @@ -0,0 +1,7 @@ +{ + "name": "isolated_type_plugin", + "version": "0.0.0", + "kibana": { + "version": "kibana" + } +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json index b30a2c3877b88..64d309b4209a2 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json @@ -1,7 +1,7 @@ { "globaltype": { "properties": { - "name": { + "title": { "type": "text", "fields": { "keyword": { diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/index.js b/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/index.js new file mode 100644 index 0000000000000..91a24fb9f4f56 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/index.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import mappings from './mappings.json'; + +export default function(kibana) { + return new kibana.Plugin({ + require: ['kibana', 'elasticsearch', 'xpack_main'], + name: 'shared_type_plugin', + uiExports: { + savedObjectsManagement: {}, + savedObjectSchemas: { + sharedtype: { + multiNamespace: true, + }, + }, + mappings, + }, + + config() {}, + + init() {}, // need empty init for plugin to load + }); +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/mappings.json new file mode 100644 index 0000000000000..918958aec0d6d --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/mappings.json @@ -0,0 +1,15 @@ +{ + "sharedtype": { + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + } +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/package.json b/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/package.json new file mode 100644 index 0000000000000..c52f4256c5c06 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/package.json @@ -0,0 +1,7 @@ +{ + "name": "shared_type_plugin", + "version": "0.0.0", + "kibana": { + "version": "kibana" + } +} diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts new file mode 100644 index 0000000000000..b32950538f8e5 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const SAVED_OBJECT_TEST_CASES = Object.freeze({ + SINGLE_NAMESPACE_DEFAULT_SPACE: Object.freeze({ + type: 'isolatedtype', + id: 'defaultspace-isolatedtype-id', + }), + SINGLE_NAMESPACE_SPACE_1: Object.freeze({ + type: 'isolatedtype', + id: 'space1-isolatedtype-id', + }), + SINGLE_NAMESPACE_SPACE_2: Object.freeze({ + type: 'isolatedtype', + id: 'space2-isolatedtype-id', + }), + MULTI_NAMESPACE_DEFAULT_AND_SPACE_1: Object.freeze({ + type: 'sharedtype', + id: 'default_and_space_1', + }), + MULTI_NAMESPACE_ONLY_SPACE_1: Object.freeze({ + type: 'sharedtype', + id: 'only_space_1', + }), + MULTI_NAMESPACE_ONLY_SPACE_2: Object.freeze({ + type: 'sharedtype', + id: 'only_space_2', + }), + NAMESPACE_AGNOSTIC: Object.freeze({ + type: 'globaltype', + id: 'globaltype-id', + }), + HIDDEN: Object.freeze({ + type: 'hiddentype', + id: 'any', + }), +}); diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts new file mode 100644 index 0000000000000..5640dfefa4f8d --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts @@ -0,0 +1,302 @@ +/* + * 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 expect from '@kbn/expect'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { SAVED_OBJECT_TEST_CASES as CASES } from './saved_object_test_cases'; +import { SPACES } from './spaces'; +import { AUTHENTICATION } from './authentication'; +import { TestCase, TestUser, ExpectResponseBody } from './types'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { + NOT_A_KIBANA_USER, + SUPERUSER, + KIBANA_LEGACY_USER, + KIBANA_DUAL_PRIVILEGES_USER, + KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + KIBANA_RBAC_USER, + KIBANA_RBAC_DASHBOARD_ONLY_USER, + KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + KIBANA_RBAC_SPACE_1_ALL_USER, + KIBANA_RBAC_SPACE_1_READ_USER, +} = AUTHENTICATION; + +export function getUrlPrefix(spaceId: string) { + return spaceId && spaceId !== DEFAULT_SPACE_ID ? `/s/${spaceId}` : ``; +} + +export function getExpectedSpaceIdProperty(spaceId: string) { + if (spaceId === DEFAULT_SPACE_ID) { + return {}; + } + return { + spaceId, + }; +} + +export const getTestTitle = ( + testCaseOrCases: TestCase | TestCase[], + bulkStatusCode?: 200 | 403 // only used for bulk test suites; other suites specify forbidden/permitted in each test case +) => { + const testCases = Array.isArray(testCaseOrCases) ? testCaseOrCases : [testCaseOrCases]; + const stringify = (array: TestCase[]) => array.map(x => `${x.type}/${x.id}`).join(); + if (bulkStatusCode === 403 || (testCases.length === 1 && testCases[0].failure === 403)) { + return `forbidden [${stringify(testCases)}]`; + } + if (testCases.find(x => x.failure === 403)) { + throw new Error( + 'Cannot create test title for multiple forbidden test cases; specify individual tests for each of these test cases' + ); + } + // permitted + const list: string[] = []; + Object.entries({ + success: undefined, + 'bad request': 400, + 'not found': 404, + conflict: 409, + }).forEach(([descriptor, failure]) => { + const filtered = testCases.filter(x => x.failure === failure); + if (filtered.length) { + list.push(`${descriptor} [${stringify(filtered)}]`); + } + }); + return `${list.join(' and ')}`; +}; + +export const testCaseFailures = { + // test suites need explicit return types for number primitives + fail400: (condition?: boolean): { failure?: 400 } => + condition !== false ? { failure: 400 } : {}, + fail404: (condition?: boolean): { failure?: 404 } => + condition !== false ? { failure: 404 } : {}, + fail409: (condition?: boolean): { failure?: 409 } => + condition !== false ? { failure: 409 } : {}, +}; + +/** + * Test cases have additional properties that we don't want to send in HTTP Requests + */ +export const createRequest = ({ type, id }: TestCase) => ({ type, id }); + +const uniq = (arr: T[]): T[] => Array.from(new Set(arr)); +const isNamespaceAgnostic = (type: string) => type === 'globaltype'; +const isMultiNamespace = (type: string) => type === 'sharedtype'; +export const expectResponses = { + forbidden: (action: string) => (typeOrTypes: string | string[]): ExpectResponseBody => async ( + response: Record + ) => { + const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; + const uniqueSorted = uniq(types).sort(); + expect(response.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to ${action} ${uniqueSorted.join()}`, + }); + }, + permitted: async (object: Record, testCase: TestCase) => { + const { type, id, failure } = testCase; + if (failure) { + let error: ReturnType; + if (failure === 400) { + error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type); + } else if (failure === 404) { + error = SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } else if (failure === 409) { + error = SavedObjectsErrorHelpers.createConflictError(type, id); + } else { + throw new Error(`Encountered unexpected error code ${failure}`); + } + // should not call permitted with a 403 failure case + if (object.type && object.id) { + // bulk request error + expect(object.type).to.eql(type); + expect(object.id).to.eql(id); + expect(object.error).to.eql(error.output.payload); + } else { + // non-bulk request error + expect(object.error).to.eql(error.output.payload.error); + expect(object.statusCode).to.eql(error.output.payload.statusCode); + // ignore the error.message, because it can vary for decorated non-bulk errors (e.g., conflict) + } + } else { + // fall back to default behavior of testing the success outcome + expect(object.type).to.eql(type); + if (id) { + expect(object.id).to.eql(id); + } else { + // created an object without specifying the ID, so it was auto-generated + expect(object.id).to.match(/^[0-9a-f-]{36}$/); + } + expect(object).not.to.have.property('error'); + expect(object.updated_at).to.match(/^[\d-]{10}T[\d:\.]{12}Z$/); + // don't test attributes, version, or references + } + }, + /** + * Additional assertions that we use in `bulk_create` and `create` to ensure that + * newly-created (or overwritten) objects don't have unexpected properties + */ + successCreated: async (es: any, spaceId: string, type: string, id: string) => { + const isNamespaceUndefined = + spaceId === SPACES.DEFAULT.spaceId || isNamespaceAgnostic(type) || isMultiNamespace(type); + const expectedSpacePrefix = isNamespaceUndefined ? '' : `${spaceId}:`; + const savedObject = await es.get({ + id: `${expectedSpacePrefix}${type}:${id}`, + index: '.kibana', + }); + const { namespace: actualNamespace, namespaces: actualNamespaces } = savedObject._source; + if (isNamespaceUndefined) { + expect(actualNamespace).to.eql(undefined); + } else { + expect(actualNamespace).to.eql(spaceId); + } + if (isMultiNamespace(type)) { + if (id === CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1.id) { + expect(actualNamespaces).to.eql([DEFAULT_SPACE_ID, SPACE_1_ID]); + } else if (id === CASES.MULTI_NAMESPACE_ONLY_SPACE_1.id) { + expect(actualNamespaces).to.eql([SPACE_1_ID]); + } else if (id === CASES.MULTI_NAMESPACE_ONLY_SPACE_2.id) { + expect(actualNamespaces).to.eql([SPACE_2_ID]); + } else { + // newly created in this space + expect(actualNamespaces).to.eql([spaceId]); + } + } + return savedObject; + }, +}; + +/** + * Get test scenarios for each type of suite. + * @param modifier Use this to generate additional permutations of test scenarios. + * For instance, a modifier of ['foo', 'bar'] would return + * a `securityAndSpaces` of: [ + * { spaceId: DEFAULT_SPACE_ID, users: {...}, modifier: 'foo' }, + * { spaceId: DEFAULT_SPACE_ID, users: {...}, modifier: 'bar' }, + * { spaceId: SPACE_1_ID, users: {...}, modifier: 'foo' }, + * { spaceId: SPACE_1_ID, users: {...}, modifier: 'bar' }, + * ] + */ +export const getTestScenarios = (modifiers?: T[]) => { + const commonUsers = { + noAccess: { ...NOT_A_KIBANA_USER, description: 'user with no access' }, + superuser: { ...SUPERUSER, description: 'superuser' }, + legacyAll: { ...KIBANA_LEGACY_USER, description: 'legacy user' }, + allGlobally: { ...KIBANA_RBAC_USER, description: 'rbac user with all globally' }, + readGlobally: { + ...KIBANA_RBAC_DASHBOARD_ONLY_USER, + description: 'rbac user with read globally', + }, + dualAll: { ...KIBANA_DUAL_PRIVILEGES_USER, description: 'dual-privileges user' }, + dualRead: { + ...KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + description: 'dual-privileges readonly user', + }, + }; + + interface Security { + modifier?: T; + users: Record< + | keyof typeof commonUsers + | 'allAtDefaultSpace' + | 'readAtDefaultSpace' + | 'allAtSpace1' + | 'readAtSpace1', + TestUser + >; + } + interface SecurityAndSpaces { + modifier?: T; + users: Record< + keyof typeof commonUsers | 'allAtSpace' | 'readAtSpace' | 'allAtOtherSpace', + TestUser + >; + spaceId: string; + } + interface Spaces { + modifier?: T; + spaceId: string; + } + + let spaces: Spaces[] = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID].map(x => ({ spaceId: x })); + let security: Security[] = [ + { + users: { + ...commonUsers, + allAtDefaultSpace: { + ...KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + description: 'rbac user with all at default space', + }, + readAtDefaultSpace: { + ...KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + description: 'rbac user with read at default space', + }, + allAtSpace1: { + ...KIBANA_RBAC_SPACE_1_ALL_USER, + description: 'rbac user with all at space_1', + }, + readAtSpace1: { + ...KIBANA_RBAC_SPACE_1_READ_USER, + description: 'rbac user with read at space_1', + }, + }, + }, + ]; + let securityAndSpaces: SecurityAndSpaces[] = [ + { + spaceId: DEFAULT_SPACE_ID, + users: { + ...commonUsers, + allAtSpace: { + ...KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + description: 'user with all at the space', + }, + readAtSpace: { + ...KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + description: 'user with read at the space', + }, + allAtOtherSpace: { + ...KIBANA_RBAC_SPACE_1_ALL_USER, + description: 'user with all at other space', + }, + }, + }, + { + spaceId: SPACE_1_ID, + users: { + ...commonUsers, + allAtSpace: { ...KIBANA_RBAC_SPACE_1_ALL_USER, description: 'user with all at the space' }, + readAtSpace: { + ...KIBANA_RBAC_SPACE_1_READ_USER, + description: 'user with read at the space', + }, + allAtOtherSpace: { + ...KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + description: 'user with all at other space', + }, + }, + }, + ]; + if (modifiers) { + const addModifier = (list: T[]) => + list.map(x => modifiers.map(modifier => ({ ...x, modifier }))).flat(); + spaces = addModifier(spaces); + security = addModifier(security); + securityAndSpaces = addModifier(securityAndSpaces); + } + return { + spaces, + security, + securityAndSpaces, + }; +}; diff --git a/x-pack/test/saved_object_api_integration/common/lib/space_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/space_test_utils.ts deleted file mode 100644 index 1619d77761c84..0000000000000 --- a/x-pack/test/saved_object_api_integration/common/lib/space_test_utils.ts +++ /dev/null @@ -1,24 +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 { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; - -export function getUrlPrefix(spaceId: string) { - return spaceId && spaceId !== DEFAULT_SPACE_ID ? `/s/${spaceId}` : ``; -} - -export function getIdPrefix(spaceId: string) { - return spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}-`; -} - -export function getExpectedSpaceIdProperty(spaceId: string) { - if (spaceId === DEFAULT_SPACE_ID) { - return {}; - } - return { - spaceId, - }; -} diff --git a/x-pack/test/saved_object_api_integration/common/lib/types.ts b/x-pack/test/saved_object_api_integration/common/lib/types.ts index 487afff1494c0..f6e6d391ae905 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/types.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/types.ts @@ -4,9 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -export type DescribeFn = (text: string, fn: () => void) => void; +export type ExpectResponseBody = (response: Record) => Promise; -export interface TestDefinitionAuthentication { - username?: string; - password?: string; +export interface TestDefinition { + title: string; + responseStatusCode: number; + responseBody: ExpectResponseBody; +} + +export interface TestSuite { + user?: TestUser; + spaceId?: string; + tests: T[]; +} + +export interface TestCase { + type: string; + id: string; + failure?: 400 | 403 | 404 | 409; +} + +export interface TestUser { + username: string; + password: string; + description: string; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index b6f1bb956d72d..0dafe6b7b386d 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -6,224 +6,137 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface BulkCreateTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface BulkCreateCustomTest extends BulkCreateTest { - description: string; - requestBody: { - [key: string]: any; - }; -} - -interface BulkCreateTests { - default: BulkCreateTest; - includingSpace: BulkCreateTest; - custom?: BulkCreateCustomTest; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface BulkCreateTestDefinition extends TestDefinition { + request: Array<{ type: string; id: string }>; + overwrite: boolean; } - -interface BulkCreateTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - tests: BulkCreateTests; +export type BulkCreateTestSuite = TestSuite; +export interface BulkCreateTestCase extends TestCase { + failure?: 400 | 409; // only used for permitted response case } -const createBulkRequests = (spaceId: string) => [ - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - attributes: { - title: 'An existing visualization', - }, - }, - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - attributes: { - title: 'A great new dashboard', - }, - }, - { - type: 'globaltype', - id: '05976c65-1145-4858-bbf0-d225cc78a06e', - attributes: { - name: 'A new globaltype object', - }, - }, - { - type: 'globaltype', - id: '8121a00-8efd-21e7-1cb3-34ab966434445', - attributes: { - name: 'An existing globaltype', - }, - }, -]; +const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake +const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; -const isGlobalType = (type: string) => type === 'globaltype'; +const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); +const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); +const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); +export const TEST_CASES = Object.freeze({ + ...CASES, + NEW_SINGLE_NAMESPACE_OBJ, + NEW_MULTI_NAMESPACE_OBJ, + NEW_NAMESPACE_AGNOSTIC_OBJ, +}); export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest) { - const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - saved_objects: [ - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - error: { - message: 'version conflict, document already exists', - statusCode: 409, - }, - }, - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - updated_at: resp.body.saved_objects[1].updated_at, - version: resp.body.saved_objects[1].version, - attributes: { - title: 'A great new dashboard', - }, - references: [], - }, - { - type: 'globaltype', - id: `05976c65-1145-4858-bbf0-d225cc78a06e`, - updated_at: resp.body.saved_objects[2].updated_at, - version: resp.body.saved_objects[2].version, - attributes: { - name: 'A new globaltype object', - }, - references: [], - }, - { - type: 'globaltype', - id: '8121a00-8efd-21e7-1cb3-34ab966434445', - error: { - message: 'version conflict, document already exists', - statusCode: 409, - }, - }, - ], - }); - - for (const savedObject of createBulkRequests(spaceId)) { - const expectedSpacePrefix = - spaceId === DEFAULT_SPACE_ID || isGlobalType(savedObject.type) ? '' : `${spaceId}:`; - - // query ES directory to ensure namespace was or wasn't specified - const { _source } = await es.get({ - id: `${expectedSpacePrefix}${savedObject.type}:${savedObject.id}`, - index: '.kibana', - }); - - const { namespace: actualNamespace } = _source; - - if (spaceId === DEFAULT_SPACE_ID || isGlobalType(savedObject.type)) { - expect(actualNamespace).to.eql(undefined); - } else { - expect(actualNamespace).to.eql(spaceId); + const expectForbidden = expectResponses.forbidden('bulk_create'); + const expectResponseBody = ( + testCases: BulkCreateTestCase | BulkCreateTestCase[], + statusCode: 200 | 403, + spaceId = SPACES.DEFAULT.spaceId + ): ExpectResponseBody => async (response: Record) => { + const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; + if (statusCode === 403) { + const types = testCaseArray.map(x => x.type); + await expectForbidden(types)(response); + } else { + // permitted + const savedObjects = response.body.saved_objects; + expect(savedObjects).length(testCaseArray.length); + for (let i = 0; i < savedObjects.length; i++) { + const object = savedObjects[i]; + const testCase = testCaseArray[i]; + await expectResponses.permitted(object, testCase); + if (!testCase.failure) { + expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + await expectResponses.successCreated(es, spaceId, object.type, object.id); + } } } }; - - const expectBadRequestForHiddenType = (resp: { [key: string]: any }) => { - const spaceEntry = resp.body.saved_objects.find( - (entry: any) => entry.id === 'my-hiddentype' && entry.type === 'hiddentype' - ); - expect(spaceEntry).to.eql({ - id: 'my-hiddentype', - type: 'hiddentype', - error: { - message: "Unsupported saved object type: 'hiddentype': Bad Request", - statusCode: 400, - error: 'Bad Request', + const createTestDefinitions = ( + testCases: BulkCreateTestCase | BulkCreateTestCase[], + forbidden: boolean, + overwrite: boolean, + options?: { + spaceId?: string; + singleRequest?: boolean; + responseBodyOverride?: ExpectResponseBody; + } + ): BulkCreateTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + if (!options?.singleRequest) { + // if we are testing cases that should result in a forbidden response, we can do each case individually + // this ensures that multiple test cases of a single type will each result in a forbidden error + return cases.map(x => ({ + title: getTestTitle(x, responseStatusCode), + request: [createRequest(x)], + responseStatusCode, + responseBody: + options?.responseBodyOverride || + expectResponseBody(x, responseStatusCode, options?.spaceId), + overwrite, + })); + } + // batch into a single request to save time during test execution + return [ + { + title: getTestTitle(cases, responseStatusCode), + request: cases.map(x => createRequest(x)), + responseStatusCode, + responseBody: + options?.responseBodyOverride || + expectResponseBody(cases, responseStatusCode, options?.spaceId), + overwrite, }, - }); + ]; }; - const expectedForbiddenTypes = ['dashboard', 'globaltype', 'visualization']; - const expectedForbiddenTypesWithHiddenType = [ - 'dashboard', - 'globaltype', - 'hiddentype', - 'visualization', - ]; - const createExpectRbacForbidden = (types: string[] = expectedForbiddenTypes) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_create ${types.join(',')}`, - }); - }; - - const makeBulkCreateTest = (describeFn: DescribeFn) => ( + const makeBulkCreateTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: BulkCreateTestDefinition + definition: BulkCreateTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.default.statusCode}`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_create`) - .auth(user.username, user.password) - .send(createBulkRequests(spaceId)) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); - - it(`including a hiddentype saved object should return ${tests.includingSpace.statusCode}`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_create`) - .auth(user.username, user.password) - .send( - createBulkRequests(spaceId).concat([ - { - type: 'hiddentype', - id: `my-hiddentype`, - attributes: { - name: 'My awesome hiddentype', - }, - }, - ]) - ) - .expect(tests.includingSpace.statusCode) - .then(tests.includingSpace.response); - }); + const attrs = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; - if (tests.custom) { - it(tests.custom!.description, async () => { + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request.map(x => ({ ...x, ...attrs })); + const query = test.overwrite ? '?overwrite=true' : ''; await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_create`) - .auth(user.username, user.password) - .send(tests.custom!.requestBody) - .expect(tests.custom!.statusCode) - .then(tests.custom!.response); + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_create${query}`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); }); } }); }; - const bulkCreateTest = makeBulkCreateTest(describe); + const addTests = makeBulkCreateTest(describe); // @ts-ignore - bulkCreateTest.only = makeBulkCreateTest(describe.only); + addTests.only = makeBulkCreateTest(describe.only); return { - bulkCreateTest, - createExpectResults, - createExpectRbacForbidden, - expectBadRequestForHiddenType, - expectedForbiddenTypesWithHiddenType, + addTests, + createTestDefinitions, + expectForbidden, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts index 9c5cc375502d1..f03dac597294c 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts @@ -6,203 +6,110 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; -interface BulkGetTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; +export interface BulkGetTestDefinition extends TestDefinition { + request: Array<{ type: string; id: string }>; } - -interface BulkGetTests { - default: BulkGetTest; - includingHiddenType: BulkGetTest; -} - -interface BulkGetTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - otherSpaceId?: string; - tests: BulkGetTests; +export type BulkGetTestSuite = TestSuite; +export interface BulkGetTestCase extends TestCase { + failure?: 400 | 404; // only used for permitted response case } -const createBulkRequests = (spaceId: string) => [ - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - }, - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}does not exist`, - }, - { - type: 'globaltype', - id: '8121a00-8efd-21e7-1cb3-34ab966434445', - }, -]; +const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); +export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectNotFoundResults = (spaceId: string) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - saved_objects: [ - { - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - type: 'visualization', - error: { - statusCode: 404, - message: 'Not found', - }, - }, - { - id: `${getIdPrefix(spaceId)}does not exist`, - type: 'dashboard', - error: { - statusCode: 404, - message: 'Not found', - }, - }, - { - id: `8121a00-8efd-21e7-1cb3-34ab966434445`, - type: 'globaltype', - updated_at: '2017-09-21T18:59:16.270Z', - version: resp.body.saved_objects[2].version, - attributes: { - name: 'My favorite global object', - }, - references: [], - }, - ], - }); + const expectForbidden = expectResponses.forbidden('bulk_get'); + const expectResponseBody = ( + testCases: BulkGetTestCase | BulkGetTestCase[], + statusCode: 200 | 403 + ): ExpectResponseBody => async (response: Record) => { + const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; + if (statusCode === 403) { + const types = testCaseArray.map(x => x.type); + await expectForbidden(types)(response); + } else { + // permitted + const savedObjects = response.body.saved_objects; + expect(savedObjects).length(testCaseArray.length); + for (let i = 0; i < savedObjects.length; i++) { + const object = savedObjects[i]; + const testCase = testCaseArray[i]; + await expectResponses.permitted(object, testCase); + } + } }; - - const expectBadRequestForHiddenType = (resp: { [key: string]: any }) => { - const spaceEntry = resp.body.saved_objects.find( - (entry: any) => entry.id === 'my-hiddentype' && entry.type === 'hiddentype' - ); - expect(spaceEntry).to.eql({ - id: 'my-hiddentype', - type: 'hiddentype', - error: { - message: "Unsupported saved object type: 'hiddentype': Bad Request", - statusCode: 400, - error: 'Bad Request', + const createTestDefinitions = ( + testCases: BulkGetTestCase | BulkGetTestCase[], + forbidden: boolean, + options?: { + singleRequest?: boolean; + responseBodyOverride?: ExpectResponseBody; + } + ): BulkGetTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + if (!options?.singleRequest) { + // if we are testing cases that should result in a forbidden response, we can do each case individually + // this ensures that multiple test cases of a single type will each result in a forbidden error + return cases.map(x => ({ + title: getTestTitle(x, responseStatusCode), + request: [createRequest(x)], + responseStatusCode, + responseBody: options?.responseBodyOverride || expectResponseBody(x, responseStatusCode), + })); + } + // batch into a single request to save time during test execution + return [ + { + title: getTestTitle(cases, responseStatusCode), + request: cases.map(x => createRequest(x)), + responseStatusCode, + responseBody: + options?.responseBodyOverride || expectResponseBody(cases, responseStatusCode), }, - }); + ]; }; - const expectedForbiddenTypes = ['dashboard', 'globaltype', 'visualization']; - const expectedForbiddenTypesWithHiddenType = [ - 'dashboard', - 'globaltype', - 'hiddentype', - 'visualization', - ]; - const createExpectRbacForbidden = (types: string[] = expectedForbiddenTypes) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_get ${types.join(',')}`, - }); - }; - - const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - saved_objects: [ - { - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - type: 'visualization', - migrationVersion: resp.body.saved_objects[0].migrationVersion, - updated_at: '2017-09-21T18:51:23.794Z', - version: resp.body.saved_objects[0].version, - attributes: { - title: 'Count of requests', - description: '', - version: 1, - // cheat for some of the more complex attributes - visState: resp.body.saved_objects[0].attributes.visState, - uiStateJSON: resp.body.saved_objects[0].attributes.uiStateJSON, - kibanaSavedObjectMeta: resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, - }, - ], - }, - { - id: `${getIdPrefix(spaceId)}does not exist`, - type: 'dashboard', - error: { - statusCode: 404, - message: 'Not found', - }, - }, - { - id: `8121a00-8efd-21e7-1cb3-34ab966434445`, - type: 'globaltype', - updated_at: '2017-09-21T18:59:16.270Z', - version: resp.body.saved_objects[2].version, - attributes: { - name: 'My favorite global object', - }, - references: [], - }, - ], - }); - }; - - const makeBulkGetTest = (describeFn: DescribeFn) => ( + const makeBulkGetTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: BulkGetTestDefinition + definition: BulkGetTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, otherSpaceId, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.default.statusCode}`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_get`) - .auth(user.username, user.password) - .send(createBulkRequests(otherSpaceId || spaceId)) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); - - it(`with a hiddentype saved object included should return ${tests.includingHiddenType.statusCode}`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_get`) - .auth(user.username, user.password) - .send( - createBulkRequests(otherSpaceId || spaceId).concat([ - { - type: 'hiddentype', - id: `my-hiddentype`, - }, - ]) - ) - .expect(tests.includingHiddenType.statusCode) - .then(tests.includingHiddenType.response); - }); + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + await supertest + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_get`) + .auth(user?.username, user?.password) + .send(test.request) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } }); }; - const bulkGetTest = makeBulkGetTest(describe); + const addTests = makeBulkGetTest(describe); // @ts-ignore - bulkGetTest.only = makeBulkGetTest(describe.only); + addTests.only = makeBulkGetTest(describe.only); return { - bulkGetTest, - createExpectNotFoundResults, - createExpectResults, - createExpectRbacForbidden, - expectBadRequestForHiddenType, - expectedForbiddenTypesWithHiddenType, + addTests, + createTestDefinitions, + expectForbidden, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts index d14c5ccbd1d0e..e0e2118300ef4 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts @@ -6,234 +6,119 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface BulkUpdateTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface BulkUpdateTests { - spaceAware: BulkUpdateTest; - notSpaceAware: BulkUpdateTest; - hiddenType: BulkUpdateTest; - doesntExist: BulkUpdateTest; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface BulkUpdateTestDefinition extends TestDefinition { + request: Array<{ type: string; id: string }>; } - -interface BulkUpdateTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - otherSpaceId?: string; - tests: BulkUpdateTests; +export type BulkUpdateTestSuite = TestSuite; +export interface BulkUpdateTestCase extends TestCase { + failure?: 404; // only used for permitted response case } -export function bulkUpdateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectNotFound = (type: string, id: string, spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - const [, savedObject] = resp.body.saved_objects; - expect(savedObject.error).eql({ - statusCode: 404, - error: 'Not Found', - message: `Saved object [${type}/${getIdPrefix(spaceId)}${id}] not found`, - }); - }; - - const createExpectDoesntExistNotFound = (spaceId?: string) => { - return createExpectNotFound('visualization', 'not an id', spaceId); - }; - - const createExpectSpaceAwareNotFound = (spaceId?: string) => { - return createExpectNotFound('visualization', 'dd7caf20-9efd-11e7-acb3-3dab96693fab', spaceId); - }; - - const expectHiddenTypeNotFound = createExpectNotFound( - 'hiddentype', - 'hiddentype_1', - DEFAULT_SPACE_ID - ); - - const createExpectRbacForbidden = (types: string[]) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_update ${types.join()}`, - }); - }; - - const expectDoesntExistRbacForbidden = createExpectRbacForbidden(['globaltype', 'visualization']); +const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake +const NEW_ATTRIBUTE_VAL = `Updated attribute value ${Date.now()}`; - const expectNotSpaceAwareRbacForbidden = createExpectRbacForbidden(['globaltype']); +const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); +export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); - const expectHiddenTypeRbacForbidden = createExpectRbacForbidden(['globaltype', 'hiddentype']); - const expectHiddenTypeRbacForbiddenWithGlobalAllowed = createExpectRbacForbidden(['hiddentype']); - - const expectSpaceAwareRbacForbidden = createExpectRbacForbidden(['globaltype', 'visualization']); - - const expectNotSpaceAwareResults = (resp: { [key: string]: any }) => { - const [, savedObject] = resp.body.saved_objects; - // loose uuid validation - expect(savedObject) - .to.have.property('id') - .match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(savedObject) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(savedObject).to.eql({ - id: savedObject.id, - type: 'globaltype', - updated_at: savedObject.updated_at, - version: savedObject.version, - attributes: { - name: 'My second favorite', - }, - }); +export function bulkUpdateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectForbidden = expectResponses.forbidden('bulk_update'); + const expectResponseBody = ( + testCases: BulkUpdateTestCase | BulkUpdateTestCase[], + statusCode: 200 | 403 + ): ExpectResponseBody => async (response: Record) => { + const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; + if (statusCode === 403) { + const types = testCaseArray.map(x => x.type); + await expectForbidden(types)(response); + } else { + // permitted + const savedObjects = response.body.saved_objects; + expect(savedObjects).length(testCaseArray.length); + for (let i = 0; i < savedObjects.length; i++) { + const object = savedObjects[i]; + const testCase = testCaseArray[i]; + await expectResponses.permitted(object, testCase); + if (!testCase.failure) { + expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + } + } + } }; - - const expectSpaceAwareResults = (resp: { [key: string]: any }) => { - const [, savedObject] = resp.body.saved_objects; - // loose uuid validation ignoring prefix - expect(savedObject) - .to.have.property('id') - .match(/[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(savedObject) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(savedObject).to.eql({ - id: savedObject.id, - type: 'visualization', - updated_at: savedObject.updated_at, - version: savedObject.version, - attributes: { - title: 'My second favorite vis', + const createTestDefinitions = ( + testCases: BulkUpdateTestCase | BulkUpdateTestCase[], + forbidden: boolean, + options?: { + singleRequest?: boolean; + responseBodyOverride?: ExpectResponseBody; + } + ): BulkUpdateTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + if (!options?.singleRequest) { + // if we are testing cases that should result in a forbidden response, we can do each case individually + // this ensures that multiple test cases of a single type will each result in a forbidden error + return cases.map(x => ({ + title: getTestTitle(x, responseStatusCode), + request: [createRequest(x)], + responseStatusCode, + responseBody: options?.responseBodyOverride || expectResponseBody(x, responseStatusCode), + })); + } + // batch into a single request to save time during test execution + return [ + { + title: getTestTitle(cases, responseStatusCode), + request: cases.map(x => createRequest(x)), + responseStatusCode, + responseBody: + options?.responseBodyOverride || expectResponseBody(cases, responseStatusCode), }, - }); + ]; }; - const makeBulkUpdateTest = (describeFn: DescribeFn) => ( + const makeBulkUpdateTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: BulkUpdateTestDefinition + definition: BulkUpdateTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, otherSpaceId, tests } = definition; - - // We add this type into all bulk updates - // to ensure that having additional items in the bulk - // update doesn't change the expected outcome overall - let updateCount = 0; - const generateNonSpaceAwareGlobalSavedObject = () => ({ - type: 'globaltype', - id: `8121a00-8efd-21e7-1cb3-34ab966434445`, - attributes: { - name: `Update #${++updateCount}`, - }, - }); + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.spaceAware.statusCode} for a space-aware doc`, async () => { - await supertest - .put(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_update`) - .auth(user.username, user.password) - .send([ - generateNonSpaceAwareGlobalSavedObject(), - { - type: 'visualization', - id: `${getIdPrefix(otherSpaceId || spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - attributes: { - title: 'My second favorite vis', - }, - }, - generateNonSpaceAwareGlobalSavedObject(), - ]) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response); - }); - - it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware doc`, async () => { - await supertest - .put(`${getUrlPrefix(otherSpaceId || spaceId)}/api/saved_objects/_bulk_update`) - .auth(user.username, user.password) - .send([ - generateNonSpaceAwareGlobalSavedObject(), - { - type: 'globaltype', - id: `8121a00-8efd-21e7-1cb3-34ab966434445`, - attributes: { - name: 'My second favorite', - }, - }, - generateNonSpaceAwareGlobalSavedObject(), - ]) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response); - }); - it(`should return ${tests.hiddenType.statusCode} for hiddentype doc`, async () => { - await supertest - .put(`${getUrlPrefix(otherSpaceId || spaceId)}/api/saved_objects/_bulk_update`) - .auth(user.username, user.password) - .send([ - generateNonSpaceAwareGlobalSavedObject(), - { - type: 'hiddentype', - id: 'hiddentype_1', - attributes: { - name: 'My favorite hidden type', - }, - }, - generateNonSpaceAwareGlobalSavedObject(), - ]) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); - }); + const attrs = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; - describe('unknown id', () => { - it(`should return ${tests.doesntExist.statusCode}`, async () => { + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request.map(x => ({ ...x, ...attrs })); await supertest .put(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_update`) - .auth(user.username, user.password) - .send([ - generateNonSpaceAwareGlobalSavedObject(), - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}not an id`, - attributes: { - title: 'My second favorite vis', - }, - }, - generateNonSpaceAwareGlobalSavedObject(), - ]) - .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response); + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); + } }); }; - const bulkUpdateTest = makeBulkUpdateTest(describe); + const addTests = makeBulkUpdateTest(describe); // @ts-ignore - bulkUpdateTest.only = makeBulkUpdateTest(describe.only); + addTests.only = makeBulkUpdateTest(describe.only); return { - createExpectDoesntExistNotFound, - createExpectSpaceAwareNotFound, - expectSpaceNotFound: expectHiddenTypeNotFound, - expectDoesntExistRbacForbidden, - expectNotSpaceAwareRbacForbidden, - expectNotSpaceAwareResults, - expectSpaceAwareRbacForbidden, - expectSpaceAwareResults, - expectHiddenTypeRbacForbidden, - expectHiddenTypeRbacForbiddenWithGlobalAllowed, - bulkUpdateTest, + addTests, + createTestDefinitions, + expectForbidden, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/create.ts b/x-pack/test/saved_object_api_integration/common/suites/create.ts index 29960c513d40f..f657756be92cd 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/create.ts @@ -3,206 +3,117 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface CreateTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface CreateCustomTest extends CreateTest { - type: string; - description: string; - requestBody: any; -} - -interface CreateTests { - spaceAware: CreateTest; - notSpaceAware: CreateTest; - hiddenType: CreateTest; - custom?: CreateCustomTest; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface CreateTestDefinition extends TestDefinition { + request: { type: string; id: string }; + overwrite: boolean; } - -interface CreateTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - tests: CreateTests; +export type CreateTestSuite = TestSuite; +export interface CreateTestCase extends TestCase { + failure?: 400 | 403 | 409; } -const spaceAwareType = 'visualization'; -const notSpaceAwareType = 'globaltype'; +const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake +const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; + +// ID intentionally left blank on NEW_SINGLE_NAMESPACE_OBJ to ensure we can create saved objects without specifying the ID +// we could create six separate test cases to test every permutation, but there's no real value in doing so +const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: '' }); +const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); +const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); +export const TEST_CASES = Object.freeze({ + ...CASES, + NEW_SINGLE_NAMESPACE_OBJ, + NEW_MULTI_NAMESPACE_OBJ, + NEW_NAMESPACE_AGNOSTIC_OBJ, +}); export function createTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest) { - const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to create ${type}`, - }); - }; - - const expectBadRequestForHiddenType = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - message: "Unsupported saved object type: 'hiddentype': Bad Request", - statusCode: 400, - error: 'Bad Request', - }); - }; - - const createExpectSpaceAwareResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: { - [key: string]: any; - }) => { - expect(resp.body) - .to.have.property('id') - .match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - migrationVersion: resp.body.migrationVersion, - type: spaceAwareType, - updated_at: resp.body.updated_at, - version: resp.body.version, - attributes: { - title: 'My favorite vis', - }, - references: [], - }); - - const expectedSpacePrefix = spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}:`; - - // query ES directory to ensure namespace was or wasn't specified - const { _source } = await es.get({ - id: `${expectedSpacePrefix}${spaceAwareType}:${resp.body.id}`, - index: '.kibana', - }); - - const { namespace: actualNamespace } = _source; - - if (spaceId === DEFAULT_SPACE_ID) { - expect(actualNamespace).to.eql(undefined); + const expectForbidden = expectResponses.forbidden('create'); + const expectResponseBody = ( + testCase: CreateTestCase, + spaceId = SPACES.DEFAULT.spaceId + ): ExpectResponseBody => async (response: Record) => { + if (testCase.failure === 403) { + await expectForbidden(testCase.type)(response); } else { - expect(actualNamespace).to.eql(spaceId); + // permitted + const object = response.body; + await expectResponses.permitted(object, testCase); + if (!testCase.failure) { + expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + await expectResponses.successCreated(es, spaceId, object.type, object.id); + } } }; - - const expectNotSpaceAwareRbacForbidden = createExpectRbacForbidden(notSpaceAwareType); - - const expectNotSpaceAwareResults = async (resp: { [key: string]: any }) => { - expect(resp.body) - .to.have.property('id') - .match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - type: notSpaceAwareType, - updated_at: resp.body.updated_at, - version: resp.body.version, - attributes: { - name: `Can't be contained to a space`, - }, - references: [], - }); - - // query ES directory to ensure namespace wasn't specified - const { _source } = await es.get({ - id: `${notSpaceAwareType}:${resp.body.id}`, - index: '.kibana', - }); - - const { namespace: actualNamespace } = _source; - - expect(actualNamespace).to.eql(undefined); + const createTestDefinitions = ( + testCases: CreateTestCase | CreateTestCase[], + forbidden: boolean, + overwrite: boolean, + options?: { + spaceId?: string; + responseBodyOverride?: ExpectResponseBody; + } + ): CreateTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x, options?.spaceId), + overwrite, + })); }; - const expectSpaceAwareRbacForbidden = createExpectRbacForbidden(spaceAwareType); - - const expectHiddenTypeRbacForbidden = createExpectRbacForbidden('hiddentype'); - - const makeCreateTest = (describeFn: DescribeFn) => ( + const makeCreateTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: CreateTestDefinition + definition: CreateTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; + describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.spaceAware.statusCode} for a space-aware type`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/${spaceAwareType}`) - .auth(user.username, user.password) - .send({ - attributes: { - title: 'My favorite vis', - }, - }) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response); - }); - - it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware type`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/${notSpaceAwareType}`) - .auth(user.username, user.password) - .send({ - attributes: { - name: `Can't be contained to a space`, - }, - }) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response); - }); - - it(`should return ${tests.hiddenType.statusCode} for the hiddentype`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/hiddentype`) - .auth(user.username, user.password) - .send({ - attributes: { - name: `Can't be created via the Saved Objects API`, - }, - }) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); - }); - if (tests.custom) { - it(tests.custom.description, async () => { + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const { type, id } = test.request; + const path = `${type}${id ? `/${id}` : ''}`; + const requestBody = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; + const query = test.overwrite ? '?overwrite=true' : ''; await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/${tests.custom!.type}`) - .auth(user.username, user.password) - .send(tests.custom!.requestBody) - .expect(tests.custom!.statusCode) - .then(tests.custom!.response); + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/${path}${query}`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); }); } }); }; - const createTest = makeCreateTest(describe); + const addTests = makeCreateTest(describe); // @ts-ignore - createTest.only = makeCreateTest(describe.only); + addTests.only = makeCreateTest(describe.only); return { - createExpectSpaceAwareResults, - createTest, - expectNotSpaceAwareRbacForbidden, - expectNotSpaceAwareResults, - expectSpaceAwareRbacForbidden, - expectBadRequestForHiddenType, - expectHiddenTypeRbacForbidden, + addTests, + createTestDefinitions, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/delete.ts b/x-pack/test/saved_object_api_integration/common/suites/delete.ts index d96ae5446d732..2222aa0c97267 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/delete.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/delete.ts @@ -4,147 +4,97 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface DeleteTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; +import expect from '@kbn/expect/expect.js'; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface DeleteTestDefinition extends TestDefinition { + request: { type: string; id: string }; } - -interface DeleteTests { - spaceAware: DeleteTest; - notSpaceAware: DeleteTest; - hiddenType: DeleteTest; - invalidId: DeleteTest; +export type DeleteTestSuite = TestSuite; +export interface DeleteTestCase extends TestCase { + failure?: 403 | 404; } -interface DeleteTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - otherSpaceId?: string; - tests: DeleteTests; -} +const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); +export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); export function deleteTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectNotFound = (spaceId: string, type: string, id: string) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: `Saved object [${type}/${getIdPrefix(spaceId)}${id}] not found`, - }); - }; - - const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to delete ${type}`, - }); - }; - - const expectGenericNotFound = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: `Not Found`, - }); - }; - - const createExpectSpaceAwareNotFound = (spaceId: string = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - createExpectNotFound(spaceId, 'dashboard', 'be3733a0-9efe-11e7-acb3-3dab96693fab')(resp); - }; - - const createExpectUnknownDocNotFound = (spaceId: string = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - createExpectNotFound(spaceId, 'dashboard', `not-a-real-id`)(resp); + const expectForbidden = expectResponses.forbidden('delete'); + const expectResponseBody = (testCase: DeleteTestCase): ExpectResponseBody => async ( + response: Record + ) => { + if (testCase.failure === 403) { + await expectForbidden(testCase.type)(response); + } else { + // permitted + const object = response.body; + if (testCase.failure) { + await expectResponses.permitted(object, testCase); + } else { + // the success response for `delete` is an empty object + expect(object).to.eql({}); + } + } }; - - const expectEmpty = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({}); + const createTestDefinitions = ( + testCases: DeleteTestCase | DeleteTestCase[], + forbidden: boolean, + options?: { + spaceId?: string; + responseBodyOverride?: ExpectResponseBody; + } + ): DeleteTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); }; - const expectRbacInvalidIdForbidden = createExpectRbacForbidden('dashboard'); - - const expectRbacNotSpaceAwareForbidden = createExpectRbacForbidden('globaltype'); - - const expectRbacSpaceAwareForbidden = createExpectRbacForbidden('dashboard'); - - const expectRbacHiddenTypeForbidden = createExpectRbacForbidden('hiddentype'); - - const makeDeleteTest = (describeFn: DescribeFn) => ( + const makeDeleteTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: DeleteTestDefinition + definition: DeleteTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, otherSpaceId, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.spaceAware.statusCode} when deleting a space-aware doc`, async () => - await supertest - .delete( - `${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${getIdPrefix( - otherSpaceId || spaceId - )}be3733a0-9efe-11e7-acb3-3dab96693fab` - ) - .auth(user.username, user.password) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response)); - - it(`should return ${tests.notSpaceAware.statusCode} when deleting a non-space-aware doc`, async () => - await supertest - .delete( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/globaltype/8121a00-8efd-21e7-1cb3-34ab966434445` - ) - .auth(user.username, user.password) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response)); - - it(`should return ${tests.hiddenType.statusCode} when deleting a hiddentype doc`, async () => - await supertest - .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/hiddentype/hiddentype_1`) - .auth(user.username, user.password) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response)); - - it(`should return ${tests.invalidId.statusCode} when deleting an unknown doc`, async () => - await supertest - .delete( - `${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${getIdPrefix( - otherSpaceId || spaceId - )}not-a-real-id` - ) - .auth(user.username, user.password) - .expect(tests.invalidId.statusCode) - .then(tests.invalidId.response)); + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const { type, id } = test.request; + await supertest + .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/${type}/${id}`) + .auth(user?.username, user?.password) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } }); }; - const deleteTest = makeDeleteTest(describe); + const addTests = makeDeleteTest(describe); // @ts-ignore - deleteTest.only = makeDeleteTest(describe.only); + addTests.only = makeDeleteTest(describe.only); return { - expectGenericNotFound, - createExpectSpaceAwareNotFound, - createExpectUnknownDocNotFound, - deleteTest, - expectEmpty, - expectRbacInvalidIdForbidden, - expectRbacNotSpaceAwareForbidden, - expectRbacSpaceAwareForbidden, - expectRbacHiddenTypeForbidden, + addTests, + createTestDefinitions, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts index e6853096962ec..ddd43d42410ae 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/export.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts @@ -5,164 +5,191 @@ */ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { expectResponses, getUrlPrefix } from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; -interface ExportTest { - statusCode: number; - description: string; - response: (resp: { [key: string]: any }) => void; -} +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; -interface ExportTests { - spaceAwareType: ExportTest; - hiddenType: ExportTest; - noTypeOrObjects: ExportTest; +export interface ExportTestDefinition extends TestDefinition { + request: ReturnType; } - -interface ExportTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - tests: ExportTests; +export type ExportTestSuite = TestSuite; +export interface ExportTestCase { + title: string; + type: string; + id?: string; + successResult?: TestCase | TestCase[]; + failure?: 400 | 403; } +export const getTestCases = (spaceId?: string) => ({ + singleNamespaceObject: { + title: 'single-namespace object', + ...(spaceId === SPACE_1_ID + ? CASES.SINGLE_NAMESPACE_SPACE_1 + : spaceId === SPACE_2_ID + ? CASES.SINGLE_NAMESPACE_SPACE_2 + : CASES.SINGLE_NAMESPACE_DEFAULT_SPACE), + } as ExportTestCase, + singleNamespaceType: { + // this test explicitly ensures that single-namespace objects from other spaces are not returned + title: 'single-namespace type', + type: 'isolatedtype', + successResult: + spaceId === SPACE_1_ID + ? CASES.SINGLE_NAMESPACE_SPACE_1 + : spaceId === SPACE_2_ID + ? CASES.SINGLE_NAMESPACE_SPACE_2 + : CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + } as ExportTestCase, + multiNamespaceObject: { + title: 'multi-namespace object', + ...(spaceId === SPACE_1_ID + ? CASES.MULTI_NAMESPACE_ONLY_SPACE_1 + : spaceId === SPACE_2_ID + ? CASES.MULTI_NAMESPACE_ONLY_SPACE_2 + : CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1), + failure: 400, // multi-namespace types cannot be exported yet + } as ExportTestCase, + multiNamespaceType: { + title: 'multi-namespace type', + type: 'sharedtype', + // successResult: + // spaceId === SPACE_1_ID + // ? [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, CASES.MULTI_NAMESPACE_ONLY_SPACE_1] + // : spaceId === SPACE_2_ID + // ? CASES.MULTI_NAMESPACE_ONLY_SPACE_2 + // : CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + failure: 400, // multi-namespace types cannot be exported yet + } as ExportTestCase, + namespaceAgnosticObject: { + title: 'namespace-agnostic object', + ...CASES.NAMESPACE_AGNOSTIC, + } as ExportTestCase, + namespaceAgnosticType: { + title: 'namespace-agnostic type', + type: 'globaltype', + successResult: CASES.NAMESPACE_AGNOSTIC, + } as ExportTestCase, + hiddenObject: { title: 'hidden object', ...CASES.HIDDEN, failure: 400 } as ExportTestCase, + hiddenType: { title: 'hidden type', type: 'hiddentype', failure: 400 } as ExportTestCase, +}); +export const createRequest = ({ type, id }: ExportTestCase) => + id ? { objects: [{ type, id }] } : { type }; +const getTestTitle = ({ failure, title }: ExportTestCase) => { + let description = 'success'; + if (failure === 400) { + description = 'bad request'; + } else if (failure === 403) { + description = 'forbidden'; + } + return `${description} ["${title}"]`; +}; + export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => { - // In export only, the API uses "bulk_get" or "find" depending on the parameters it receives. - // The best that could be done here is to have an if statement to ensure at least one of the - // two errors has been thrown. - if (resp.body.message.indexOf(`bulk_get`) !== -1) { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_get ${type}`, + const expectForbiddenBulkGet = expectResponses.forbidden('bulk_get'); + const expectForbiddenFind = expectResponses.forbidden('find'); + const expectResponseBody = (testCase: ExportTestCase): ExpectResponseBody => async ( + response: Record + ) => { + const { type, id, successResult = { type, id }, failure } = testCase; + if (failure === 403) { + // In export only, the API uses "bulk_get" or "find" depending on the parameters it receives. + // The best that could be done here is to have an if statement to ensure at least one of the + // two errors has been thrown. + if (id) { + await expectForbiddenBulkGet(type)(response); + } else { + await expectForbiddenFind(type)(response); + } + } else if (failure === 400) { + // 400 + expect(response.body.error).to.eql('Bad Request'); + expect(response.body.statusCode).to.eql(failure); + if (id) { + expect(response.body.message).to.eql( + `Trying to export object(s) with non-exportable types: ${type}:${id}` + ); + } else { + expect(response.body.message).to.eql(`Trying to export non-exportable type(s): ${type}`); + } + } else { + // 2xx + expect(response.body).not.to.have.property('error'); + const ndjson = response.text.split('\n'); + const savedObjectsArray = Array.isArray(successResult) ? successResult : [successResult]; + expect(ndjson.length).to.eql(savedObjectsArray.length + 1); + for (let i = 0; i < savedObjectsArray.length; i++) { + const object = JSON.parse(ndjson[i]); + const { type: expectedType, id: expectedId } = savedObjectsArray[i]; + expect(object.type).to.eql(expectedType); + expect(object.id).to.eql(expectedId); + expect(object.updated_at).to.match(/^[\d-]{10}T[\d:\.]{12}Z$/); + // don't test attributes, version, or references + } + const exportDetails = JSON.parse(ndjson[ndjson.length - 1]); + expect(exportDetails).to.eql({ + exportedCount: ndjson.length - 1, + missingRefCount: 0, + missingReferences: [], }); - return; } - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to find ${type}`, - }); }; - - const expectTypeOrObjectsRequired = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: '[request body]: expected a plain object value, but found [null] instead.', - }); - }; - - const expectInvalidTypeSpecified = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: `Trying to export object(s) with non-exportable types: hiddentype:hiddentype_1`, - }); - }; - - const createExpectVisualizationResults = (spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - const response = JSON.parse(resp.text); - expect(response).to.eql({ - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - version: response.version, - attributes: response.attributes, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, - }, - ], - migrationVersion: response.migrationVersion, - updated_at: '2017-09-21T18:51:23.794Z', - }); + const createTestDefinitions = ( + testCases: ExportTestCase | ExportTestCase[], + forbidden: boolean, + options?: { + responseBodyOverride?: ExpectResponseBody; + } + ): ExportTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); }; - const makeExportTest = (describeFn: DescribeFn) => ( + const makeExportTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: ExportTestDefinition + definition: ExportTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + const { user, spaceId = DEFAULT_SPACE_ID, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`space aware type should return ${tests.spaceAwareType.statusCode} with ${tests.spaceAwareType.description} when querying by type`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`) - .send({ - type: 'visualization', - excludeExportDetails: true, - }) - .auth(user.username, user.password) - .expect(tests.spaceAwareType.statusCode) - .then(tests.spaceAwareType.response); - }); - - it(`space aware type should return ${tests.spaceAwareType.statusCode} with ${tests.spaceAwareType.description} when querying by objects`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`) - .send({ - objects: [ - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - }, - ], - excludeExportDetails: true, - }) - .auth(user.username, user.password) - .expect(tests.spaceAwareType.statusCode) - .then(tests.spaceAwareType.response); - }); - - describe('hidden type', () => { - it(`should return ${tests.hiddenType.statusCode} with ${tests.hiddenType.description}`, async () => { + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { await supertest .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`) - .send({ - objects: [ - { - type: 'hiddentype', - id: `hiddentype_1`, - }, - ], - excludeExportDetails: true, - }) - .auth(user.username, user.password) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); + .auth(user?.username, user?.password) + .send(test.request) + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); - - describe('no type or objects', () => { - it(`should return ${tests.noTypeOrObjects.statusCode} with ${tests.noTypeOrObjects.description}`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`) - .auth(user.username, user.password) - .expect(tests.noTypeOrObjects.statusCode) - .then(tests.noTypeOrObjects.response); - }); - }); + } }); }; - const exportTest = makeExportTest(describe); + const addTests = makeExportTest(describe); // @ts-ignore - exportTest.only = makeExportTest(describe.only); + addTests.only = makeExportTest(describe.only); return { - createExpectRbacForbidden, - expectTypeOrObjectsRequired, - expectInvalidTypeSpecified, - createExpectVisualizationResults, - exportTest, + addTests, + createTestDefinitions, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts index 5479960634ccb..75d6653365fdf 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/find.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -3,271 +3,190 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface FindTest { - statusCode: number; - description: string; - response: (resp: { [key: string]: any }) => void; +import querystring from 'querystring'; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { expectResponses, getUrlPrefix } from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +export interface FindTestDefinition extends TestDefinition { + request: { query: string }; } - -interface FindTests { - spaceAwareType: FindTest; - notSpaceAwareType: FindTest; - unknownType: FindTest; - pageBeyondTotal: FindTest; - unknownSearchField: FindTest; - hiddenType: FindTest; - noType: FindTest; - filterWithNotSpaceAwareType: FindTest; - filterWithHiddenType: FindTest; - filterWithUnknownType: FindTest; - filterWithNoType: FindTest; - filterWithUnAllowedType: FindTest; +export type FindTestSuite = TestSuite; +export interface FindTestCase { + title: string; + query: string; + successResult?: { + savedObjects?: TestCase | TestCase[]; + page?: number; + perPage?: number; + total?: number; + }; + failure?: 400 | 403; } -interface FindTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - tests: FindTests; -} +export const getTestCases = (spaceId?: string) => ({ + singleNamespaceType: { + title: 'find single-namespace type', + query: 'type=isolatedtype&fields=title', + successResult: { + savedObjects: + spaceId === SPACE_1_ID + ? CASES.SINGLE_NAMESPACE_SPACE_1 + : spaceId === SPACE_2_ID + ? CASES.SINGLE_NAMESPACE_SPACE_2 + : CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + }, + } as FindTestCase, + multiNamespaceType: { + title: 'find multi-namespace type', + query: 'type=sharedtype&fields=title', + successResult: { + savedObjects: + spaceId === SPACE_1_ID + ? [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, CASES.MULTI_NAMESPACE_ONLY_SPACE_1] + : spaceId === SPACE_2_ID + ? CASES.MULTI_NAMESPACE_ONLY_SPACE_2 + : CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + }, + } as FindTestCase, + namespaceAgnosticType: { + title: 'find namespace-agnostic type', + query: 'type=globaltype&fields=title', + successResult: { savedObjects: CASES.NAMESPACE_AGNOSTIC }, + } as FindTestCase, + hiddenType: { title: 'find hidden type', query: 'type=hiddentype&fields=name' } as FindTestCase, + unknownType: { title: 'find unknown type', query: 'type=wigwags' } as FindTestCase, + pageBeyondTotal: { + title: 'find page beyond total', + query: 'type=isolatedtype&page=100&per_page=100', + successResult: { page: 100, perPage: 100, total: 1, savedObjects: [] }, + } as FindTestCase, + unknownSearchField: { + title: 'find unknown search field', + query: 'type=url&search_fields=a', + } as FindTestCase, + filterWithNamespaceAgnosticType: { + title: 'filter with namespace-agnostic type', + query: 'type=globaltype&filter=globaltype.attributes.title:*global*', + successResult: { savedObjects: CASES.NAMESPACE_AGNOSTIC }, + } as FindTestCase, + filterWithHiddenType: { + title: 'filter with hidden type', + query: `type=hiddentype&fields=name&filter=hiddentype.attributes.title:'hello'`, + } as FindTestCase, + filterWithUnknownType: { + title: 'filter with unknown type', + query: `type=wigwags&filter=wigwags.attributes.title:'unknown'`, + } as FindTestCase, + filterWithDisallowedType: { + title: 'filter with disallowed type', + query: `type=globaltype&filter=dashboard.title:'Requests'`, + failure: 400, + } as FindTestCase, +}); +export const createRequest = ({ query }: FindTestCase) => ({ query }); +const getTestTitle = ({ failure, title }: FindTestCase) => { + let description = 'success'; + if (failure === 400) { + description = 'bad request'; + } else if (failure === 403) { + description = 'forbidden'; + } + return `${description} ["${title}"]`; +}; export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectEmpty = (page: number, perPage: number, total: number) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - page, - per_page: perPage, - total, - saved_objects: [], - }); - }; - - const createExpectRbacForbidden = (type?: string) => (resp: { [key: string]: any }) => { - const message = type ? `Unable to find ${type}` : `Not authorized to find saved_object`; - - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message, - }); - }; - - const expectNotSpaceAwareResults = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - page: 1, - per_page: 20, - total: 1, - saved_objects: [ - { - type: 'globaltype', - id: `8121a00-8efd-21e7-1cb3-34ab966434445`, - version: resp.body.saved_objects[0].version, - attributes: { - name: 'My favorite global object', - }, - references: [], - updated_at: '2017-09-21T18:59:16.270Z', - }, - ], - }); - }; - - const expectFilterWrongTypeError = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Bad Request', - message: 'This type dashboard is not allowed: Bad Request', - statusCode: 400, - }); - }; - - const expectTypeRequired = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Bad Request', - message: '[request query.type]: expected at least one defined value but got [undefined]', - statusCode: 400, - }); + const expectForbidden = expectResponses.forbidden('find'); + const expectResponseBody = (testCase: FindTestCase): ExpectResponseBody => async ( + response: Record + ) => { + const { failure, successResult = {}, query } = testCase; + const parsedQuery = querystring.parse(query); + if (failure === 403) { + const type = parsedQuery.type; + await expectForbidden(type)(response); + } else if (failure === 400) { + const type = (parsedQuery.filter as string).split('.')[0]; + expect(response.body.error).to.eql('Bad Request'); + expect(response.body.statusCode).to.eql(failure); + expect(response.body.message).to.eql(`This type ${type} is not allowed: Bad Request`); + } else { + // 2xx + expect(response.body).not.to.have.property('error'); + const { page = 1, perPage = 20, total, savedObjects = [] } = successResult; + const savedObjectsArray = Array.isArray(savedObjects) ? savedObjects : [savedObjects]; + expect(response.body.page).to.eql(page); + expect(response.body.per_page).to.eql(perPage); + expect(response.body.total).to.eql(total || savedObjectsArray.length); + for (let i = 0; i < savedObjectsArray.length; i++) { + const object = response.body.saved_objects[i]; + const { type: expectedType, id: expectedId } = savedObjectsArray[i]; + expect(object.type).to.eql(expectedType); + expect(object.id).to.eql(expectedId); + expect(object.updated_at).to.match(/^[\d-]{10}T[\d:\.]{12}Z$/); + // don't test attributes, version, or references + } + } }; - - const createExpectVisualizationResults = (spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - page: 1, - per_page: 20, - total: 1, - saved_objects: [ - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - version: resp.body.saved_objects[0].version, - attributes: { - title: 'Count of requests', - }, - migrationVersion: resp.body.saved_objects[0].migrationVersion, - references: [ - { - id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - }, - ], - updated_at: '2017-09-21T18:51:23.794Z', - }, - ], - }); + const createTestDefinitions = ( + testCases: FindTestCase | FindTestCase[], + forbidden: boolean, + options?: { + responseBodyOverride?: ExpectResponseBody; + } + ): FindTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); }; - const makeFindTest = (describeFn: DescribeFn) => ( + const makeFindTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: FindTestDefinition + definition: FindTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + const { user, spaceId = DEFAULT_SPACE_ID, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`space aware type should return ${tests.spaceAwareType.statusCode} with ${tests.spaceAwareType.description}`, async () => - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=visualization&fields=title`) - .auth(user.username, user.password) - .expect(tests.spaceAwareType.statusCode) - .then(tests.spaceAwareType.response)); - - it(`not space aware type should return ${tests.notSpaceAwareType.statusCode} with ${tests.notSpaceAwareType.description}`, async () => - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=globaltype&fields=name`) - .auth(user.username, user.password) - .expect(tests.notSpaceAwareType.statusCode) - .then(tests.notSpaceAwareType.response)); - - it(`finding a hiddentype should return ${tests.hiddenType.statusCode} with ${tests.hiddenType.description}`, async () => - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=hiddentype&fields=name`) - .auth(user.username, user.password) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response)); - - describe('unknown type', () => { - it(`should return ${tests.unknownType.statusCode} with ${tests.unknownType.description}`, async () => - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=wigwags`) - .auth(user.username, user.password) - .expect(tests.unknownType.statusCode) - .then(tests.unknownType.response)); - }); - - describe('page beyond total', () => { - it(`should return ${tests.pageBeyondTotal.statusCode} with ${tests.pageBeyondTotal.description}`, async () => - await supertest - .get( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/_find?type=visualization&page=100&per_page=100` - ) - .auth(user.username, user.password) - .expect(tests.pageBeyondTotal.statusCode) - .then(tests.pageBeyondTotal.response)); - }); - - describe('unknown search field', () => { - it(`should return ${tests.unknownSearchField.statusCode} with ${tests.unknownSearchField.description}`, async () => - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=url&search_fields=a`) - .auth(user.username, user.password) - .expect(tests.unknownSearchField.statusCode) - .then(tests.unknownSearchField.response)); - }); - - describe('no type', () => { - it(`should return ${tests.noType.statusCode} with ${tests.noType.description}`, async () => - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find`) - .auth(user.username, user.password) - .expect(tests.noType.statusCode) - .then(tests.noType.response)); - }); - - describe('filter', () => { - it(`by wrong type should return ${tests.filterWithUnAllowedType.statusCode} with ${tests.filterWithUnAllowedType.description}`, async () => - await supertest - .get( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/_find?type=globaltype&filter=dashboard.title:'Requests'` - ) - .auth(user.username, user.password) - .expect(tests.filterWithUnAllowedType.statusCode) - .then(tests.filterWithUnAllowedType.response)); - - it(`not space aware type should return ${tests.filterWithNotSpaceAwareType.statusCode} with ${tests.filterWithNotSpaceAwareType.description}`, async () => + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const query = test.request.query ? `?${test.request.query}` : ''; await supertest - .get( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/_find?type=globaltype&filter=globaltype.attributes.name:*global*` - ) - .auth(user.username, user.password) - .expect(tests.filterWithNotSpaceAwareType.statusCode) - .then(tests.filterWithNotSpaceAwareType.response)); - - it(`finding a hiddentype should return ${tests.filterWithHiddenType.statusCode} with ${tests.filterWithHiddenType.description}`, async () => - await supertest - .get( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/_find?type=hiddentype&fields=name&filter=hiddentype.attributes.name:'hello'` - ) - .auth(user.username, user.password) - .expect(tests.filterWithHiddenType.statusCode) - .then(tests.filterWithHiddenType.response)); - - describe('unknown type', () => { - it(`should return ${tests.filterWithUnknownType.statusCode} with ${tests.filterWithUnknownType.description}`, async () => - await supertest - .get( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/_find?type=wigwags&filter=wigwags.attributes.title:'unknown'` - ) - .auth(user.username, user.password) - .expect(tests.filterWithUnknownType.statusCode) - .then(tests.filterWithUnknownType.response)); - }); - - describe('no type', () => { - it(`should return ${tests.filterWithNoType.statusCode} with ${tests.filterWithNoType.description}`, async () => - await supertest - .get( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/_find?filter=global.attributes.name:*global*` - ) - .auth(user.username, user.password) - .expect(tests.filterWithNoType.statusCode) - .then(tests.filterWithNoType.response)); + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find${query}`) + .auth(user?.username, user?.password) + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); + } }); }; - const findTest = makeFindTest(describe); + const addTests = makeFindTest(describe); // @ts-ignore - findTest.only = makeFindTest(describe.only); + addTests.only = makeFindTest(describe.only); return { - createExpectEmpty, - createExpectRbacForbidden, - createExpectVisualizationResults, - expectFilterWrongTypeError, - expectNotSpaceAwareResults, - expectTypeRequired, - findTest, + addTests, + createTestDefinitions, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/get.ts b/x-pack/test/saved_object_api_integration/common/suites/get.ts index c98209ca1e105..d8fa4d91276d7 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/get.ts @@ -3,193 +3,89 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface GetTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface GetTests { - spaceAware: GetTest; - notSpaceAware: GetTest; - hiddenType: GetTest; - doesntExist: GetTest; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface GetTestDefinition extends TestDefinition { + request: { type: string; id: string }; } +export type GetTestSuite = TestSuite; +export type GetTestCase = TestCase; -interface GetTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - otherSpaceId?: string; - tests: GetTests; -} - -const spaceAwareId = 'dd7caf20-9efd-11e7-acb3-3dab96693fab'; -const notSpaceAwareId = '8121a00-8efd-21e7-1cb3-34ab966434445'; -const doesntExistId = 'foobar'; +const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); +export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectDoesntExistNotFound = (spaceId = DEFAULT_SPACE_ID) => { - return createExpectNotFound('visualization', doesntExistId, spaceId); - }; - - const createExpectNotFound = (type: string, id: string, spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - error: 'Not Found', - message: `Saved object [${type}/${getIdPrefix(spaceId)}${id}] not found`, - statusCode: 404, - }); - }; - - const expectHiddenTypeNotFound = createExpectNotFound( - 'hiddentype', - 'hiddentype_1', - DEFAULT_SPACE_ID - ); - - const createExpectNotSpaceAwareRbacForbidden = () => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Forbidden', - message: `Unable to get globaltype`, - statusCode: 403, - }); - }; - - const createExpectNotSpaceAwareResults = (spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - id: `${notSpaceAwareId}`, - type: 'globaltype', - updated_at: '2017-09-21T18:59:16.270Z', - version: resp.body.version, - attributes: { - name: 'My favorite global object', - }, - references: [], - }); - }; - - const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Forbidden', - message: `Unable to get ${type}`, - statusCode: 403, - }); + const expectForbidden = expectResponses.forbidden('get'); + const expectResponseBody = (testCase: GetTestCase): ExpectResponseBody => async ( + response: Record + ) => { + if (testCase.failure === 403) { + await expectForbidden(testCase.type)(response); + } else { + // permitted + const object = response.body; + await expectResponses.permitted(object, testCase); + } }; - - const createExpectSpaceAwareNotFound = (spaceId = DEFAULT_SPACE_ID) => { - return createExpectNotFound('visualization', spaceAwareId, spaceId); + const createTestDefinitions = ( + testCases: GetTestCase | GetTestCase[], + forbidden: boolean, + options?: { + spaceId?: string; + responseBodyOverride?: ExpectResponseBody; + } + ): GetTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); }; - const expectSpaceAwareRbacForbidden = createExpectRbacForbidden('visualization'); - const expectNotSpaceAwareRbacForbidden = createExpectRbacForbidden('globaltype'); - const expectHiddenTypeRbacForbidden = createExpectRbacForbidden('hiddentype'); - const expectDoesntExistRbacForbidden = createExpectRbacForbidden('visualization'); - - const createExpectSpaceAwareResults = (spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - type: 'visualization', - migrationVersion: resp.body.migrationVersion, - updated_at: '2017-09-21T18:51:23.794Z', - version: resp.body.version, - attributes: { - title: 'Count of requests', - description: '', - version: 1, - // cheat for some of the more complex attributes - visState: resp.body.attributes.visState, - uiStateJSON: resp.body.attributes.uiStateJSON, - kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, - }, - ], - }); - }; - - const makeGetTest = (describeFn: DescribeFn) => ( + const makeGetTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: GetTestDefinition + definition: GetTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, otherSpaceId, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.spaceAware.statusCode} when getting a space aware doc`, async () => { - await supertest - .get( - `${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix( - otherSpaceId || spaceId - )}${spaceAwareId}` - ) - .auth(user.username, user.password) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response); - }); - - it(`should return ${tests.notSpaceAware.statusCode} when getting a non-space-aware doc`, async () => { - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/globaltype/${notSpaceAwareId}`) - .auth(user.username, user.password) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response); - }); - - it(`should return ${tests.hiddenType.statusCode} when getting a hiddentype doc`, async () => { - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/hiddentype/hiddentype_1`) - .auth(user.username, user.password) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); - }); - - describe('document does not exist', () => { - it(`should return ${tests.doesntExist.statusCode}`, async () => { + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const { type, id } = test.request; await supertest - .get( - `${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix( - otherSpaceId || spaceId - )}${doesntExistId}` - ) - .auth(user.username, user.password) - .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response); + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/${type}/${id}`) + .auth(user?.username, user?.password) + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); + } }); }; - const getTest = makeGetTest(describe); + const addTests = makeGetTest(describe); // @ts-ignore - getTest.only = makeGetTest(describe.only); + addTests.only = makeGetTest(describe.only); return { - createExpectDoesntExistNotFound, - createExpectNotSpaceAwareRbacForbidden, - createExpectNotSpaceAwareResults, - createExpectSpaceAwareNotFound, - createExpectSpaceAwareResults, - expectHiddenTypeNotFound, - expectSpaceAwareRbacForbidden, - expectNotSpaceAwareRbacForbidden, - expectDoesntExistRbacForbidden, - expectHiddenTypeRbacForbidden, - getTest, + addTests, + createTestDefinitions, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index f6723c912f82e..2f631221c6955 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -6,195 +6,152 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface ImportTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface ImportTestDefinition extends TestDefinition { + request: Array<{ type: string; id: string }>; } - -interface ImportTests { - default: ImportTest; - hiddenType: ImportTest; - unknownType: ImportTest; +export type ImportTestSuite = TestSuite; +export interface ImportTestCase extends TestCase { + failure?: 400 | 409; // only used for permitted response case } -interface ImportTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - tests: ImportTests; -} +const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake +const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; -const createImportData = (spaceId: string) => [ - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - attributes: { - title: 'A great new dashboard', - }, - }, - { - type: 'globaltype', - id: '05976c65-1145-4858-bbf0-d225cc78a06e', - attributes: { - name: 'A new globaltype object', - }, - }, -]; +const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); +const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); +const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); +export const TEST_CASES = Object.freeze({ + ...CASES, + NEW_SINGLE_NAMESPACE_OBJ, + NEW_MULTI_NAMESPACE_OBJ, + NEW_NAMESPACE_AGNOSTIC_OBJ, +}); export function importTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest) { - const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - success: true, - successCount: 2, - }); - }; - - const expectResultsWithUnsupportedHiddenType = async (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - success: false, - successCount: 2, - errors: [ - { - error: { - type: 'unsupported_type', - }, - id: '1', - type: 'hiddentype', - }, - ], - }); + const expectForbidden = expectResponses.forbidden('bulk_create'); + const expectResponseBody = ( + testCases: ImportTestCase | ImportTestCase[], + statusCode: 200 | 403, + spaceId = SPACES.DEFAULT.spaceId + ): ExpectResponseBody => async (response: Record) => { + const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; + if (statusCode === 403) { + const types = testCaseArray.map(x => x.type); + await expectForbidden(types)(response); + } else { + // permitted + const { success, successCount, errors } = response.body; + const expectedSuccesses = testCaseArray.filter(x => !x.failure); + const expectedFailures = testCaseArray.filter(x => x.failure); + expect(success).to.eql(expectedFailures.length === 0); + expect(successCount).to.eql(expectedSuccesses.length); + if (expectedFailures.length) { + expect(errors).to.have.length(expectedFailures.length); + } else { + expect(response.body).not.to.have.property('errors'); + } + for (let i = 0; i < expectedSuccesses.length; i++) { + const { type, id } = expectedSuccesses[i]; + const { _source } = await expectResponses.successCreated(es, spaceId, type, id); + expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + } + for (let i = 0; i < expectedFailures.length; i++) { + const { type, id, failure } = expectedFailures[i]; + // we don't know the order of the returned errors; search for each one + const object = (errors as Array>).find( + x => x.type === type && x.id === id + ); + expect(object).not.to.be(undefined); + if (failure === 400) { + expect(object!.error).to.eql({ type: 'unsupported_type' }); + } else { + // 409 + expect(object!.error).to.eql({ type: 'conflict' }); + } + } + } }; - - const expectUnknownTypeUnsupported = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - success: false, - successCount: 2, - errors: [ - { - id: '1', - type: 'wigwags', - title: 'Wigwags title', - error: { - type: 'unsupported_type', - }, - }, - ], - }); + const createTestDefinitions = ( + testCases: ImportTestCase | ImportTestCase[], + forbidden: boolean, + options?: { + spaceId?: string; + singleRequest?: boolean; + responseBodyOverride?: ExpectResponseBody; + } + ): ImportTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + if (!options?.singleRequest) { + // if we are testing cases that should result in a forbidden response, we can do each case individually + // this ensures that multiple test cases of a single type will each result in a forbidden error + return cases.map(x => ({ + title: getTestTitle(x, responseStatusCode), + request: [createRequest(x)], + responseStatusCode, + responseBody: + options?.responseBodyOverride || + expectResponseBody(x, responseStatusCode, options?.spaceId), + })); + } + // batch into a single request to save time during test execution + return [ + { + title: getTestTitle(cases, responseStatusCode), + request: cases.map(x => createRequest(x)), + responseStatusCode, + responseBody: + options?.responseBodyOverride || + expectResponseBody(cases, responseStatusCode, options?.spaceId), + }, + ]; }; - const expectHiddenTypeUnsupported = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - success: false, - successCount: 2, - errors: [ - { - id: '1', - type: 'hiddentype', - error: { - type: 'unsupported_type', - }, - }, - ], - }); - }; - - const expectRbacForbidden = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_create dashboard,globaltype`, - }); - }; - - const makeImportTest = (describeFn: DescribeFn) => ( + const makeImportTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: ImportTestDefinition + definition: ImportTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.default.statusCode}`, async () => { - const data = createImportData(spaceId); - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`) - .auth(user.username, user.password) - .attach( - 'file', - Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'), - 'export.ndjson' - ) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); - - describe('hiddentype', () => { - it(`should return ${tests.hiddenType.statusCode}`, async () => { - const data = createImportData(spaceId); - data.push({ - type: 'hiddentype', - id: '1', - attributes: { - name: 'My Hidden Type', - }, - }); - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`) - .query({ overwrite: true }) - .auth(user.username, user.password) - .attach( - 'file', - Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'), - 'export.ndjson' - ) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); - }); - }); + const attrs = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; - describe('unknown type', () => { - it(`should return ${tests.unknownType.statusCode}`, async () => { - const data = createImportData(spaceId); - data.push({ - type: 'wigwags', - id: '1', - attributes: { - title: 'Wigwags title', - }, - }); + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request + .map(obj => JSON.stringify({ ...obj, ...attrs })) + .join('\n'); await supertest .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`) - .query({ overwrite: true }) - .auth(user.username, user.password) - .attach( - 'file', - Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'), - 'export.ndjson' - ) - .expect(tests.unknownType.statusCode) - .then(tests.unknownType.response); + .auth(user?.username, user?.password) + .attach('file', Buffer.from(requestBody, 'utf8'), 'export.ndjson') + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); + } }); }; - const importTest = makeImportTest(describe); + const addTests = makeImportTest(describe); // @ts-ignore - importTest.only = makeImportTest(describe.only); + addTests.only = makeImportTest(describe.only); return { - importTest, - createExpectResults, - expectResultsWithUnsupportedHiddenType, - expectRbacForbidden, - expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, + addTests, + createTestDefinitions, + expectForbidden, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index 1b538b9b1b65d..47c4babc5fcf9 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -6,219 +6,165 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; -interface ResolveImportErrorsTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; +export interface ResolveImportErrorsTestDefinition extends TestDefinition { + request: Array<{ type: string; id: string }>; + overwrite: boolean; } - -interface ResolveImportErrorsTests { - default: ResolveImportErrorsTest; - hiddenType: ResolveImportErrorsTest; - unknownType: ResolveImportErrorsTest; +export type ResolveImportErrorsTestSuite = TestSuite; +export interface ResolveImportErrorsTestCase extends TestCase { + failure?: 400 | 409; // only used for permitted response case } -interface ResolveImportErrorsTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - tests: ResolveImportErrorsTests; -} +const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake +const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; -const createImportData = (spaceId: string) => [ - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - attributes: { - title: 'A great new dashboard', - }, - }, - { - type: 'globaltype', - id: '05976c65-1145-4858-bbf0-d225cc78a06e', - attributes: { - name: 'A new globaltype object', - }, - }, -]; +const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); +const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); +const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); +export const TEST_CASES = Object.freeze({ + ...CASES, + NEW_SINGLE_NAMESPACE_OBJ, + NEW_MULTI_NAMESPACE_OBJ, + NEW_NAMESPACE_AGNOSTIC_OBJ, +}); export function resolveImportErrorsTestSuiteFactory( es: any, esArchiver: any, supertest: SuperTest ) { - const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - success: true, - successCount: 1, - }); - }; - - const expectUnknownTypeUnsupported = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - success: false, - successCount: 1, - errors: [ - { - id: '1', - type: 'wigwags', - title: 'Wigwags title', - error: { - type: 'unsupported_type', - }, - }, - ], - }); - }; - - const expectHiddenTypeUnsupported = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - success: false, - successCount: 1, - errors: [ - { - id: '1', - type: 'hiddentype', - error: { - type: 'unsupported_type', - }, - }, - ], - }); + const expectForbidden = expectResponses.forbidden('bulk_create'); + const expectResponseBody = ( + testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[], + statusCode: 200 | 403, + spaceId = SPACES.DEFAULT.spaceId + ): ExpectResponseBody => async (response: Record) => { + const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; + if (statusCode === 403) { + const types = testCaseArray.map(x => x.type); + await expectForbidden(types)(response); + } else { + // permitted + const { success, successCount, errors } = response.body; + const expectedSuccesses = testCaseArray.filter(x => !x.failure); + const expectedFailures = testCaseArray.filter(x => x.failure); + expect(success).to.eql(expectedFailures.length === 0); + expect(successCount).to.eql(expectedSuccesses.length); + if (expectedFailures.length) { + expect(errors).to.have.length(expectedFailures.length); + } else { + expect(response.body).not.to.have.property('errors'); + } + for (let i = 0; i < expectedSuccesses.length; i++) { + const { type, id } = expectedSuccesses[i]; + const { _source } = await expectResponses.successCreated(es, spaceId, type, id); + expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + } + for (let i = 0; i < expectedFailures.length; i++) { + const { type, id, failure } = expectedFailures[i]; + // we don't know the order of the returned errors; search for each one + const object = (errors as Array>).find( + x => x.type === type && x.id === id + ); + expect(object).not.to.be(undefined); + if (failure === 400) { + expect(object!.error).to.eql({ type: 'unsupported_type' }); + } else { + // 409 + expect(object!.error).to.eql({ type: 'conflict' }); + } + } + } }; - - const expectRbacForbidden = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_create dashboard`, - }); + const createTestDefinitions = ( + testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[], + forbidden: boolean, + overwrite: boolean, + options?: { + spaceId?: string; + singleRequest?: boolean; + responseBodyOverride?: ExpectResponseBody; + } + ): ResolveImportErrorsTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + if (!options?.singleRequest) { + // if we are testing cases that should result in a forbidden response, we can do each case individually + // this ensures that multiple test cases of a single type will each result in a forbidden error + return cases.map(x => ({ + title: getTestTitle(x, responseStatusCode), + request: [createRequest(x)], + responseStatusCode, + responseBody: + options?.responseBodyOverride || + expectResponseBody(x, responseStatusCode, options?.spaceId), + overwrite, + })); + } + // batch into a single request to save time during test execution + return [ + { + title: getTestTitle(cases, responseStatusCode), + request: cases.map(x => createRequest(x)), + responseStatusCode, + responseBody: + options?.responseBodyOverride || + expectResponseBody(cases, responseStatusCode, options?.spaceId), + overwrite, + }, + ]; }; - const makeResolveImportErrorsTest = (describeFn: DescribeFn) => ( + const makeResolveImportErrorsTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: ResolveImportErrorsTestDefinition + definition: ResolveImportErrorsTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.default.statusCode}`, async () => { - const data = createImportData(spaceId); - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`) - .auth(user.username, user.password) - .field( - 'retries', - JSON.stringify([ - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - overwrite: true, - }, - ]) - ) - .attach( - 'file', - Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'), - 'export.ndjson' - ) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); + const attrs = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; - describe('unknown type', () => { - it(`should return ${tests.unknownType.statusCode}`, async () => { - const data = createImportData(spaceId); - data.push({ - type: 'wigwags', - id: '1', - attributes: { - title: 'Wigwags title', - }, - }); - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`) - .auth(user.username, user.password) - .field( - 'retries', - JSON.stringify([ - { - type: 'wigwags', - id: '1', - overwrite: true, - }, - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - overwrite: true, - }, - ]) - ) - .attach( - 'file', - Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'), - 'export.ndjson' - ) - .expect(tests.unknownType.statusCode) - .then(tests.unknownType.response); - }); - }); - describe('hidden type', () => { - it(`should return ${tests.hiddenType.statusCode}`, async () => { - const data = createImportData(spaceId); - data.push({ - type: 'hiddentype', - id: '1', - attributes: { - name: 'My Hidden Type', - }, - }); + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const retryAttrs = test.overwrite ? { overwrite: true } : {}; + const retries = JSON.stringify( + test.request.map(({ type, id }) => ({ type, id, ...retryAttrs })) + ); + const requestBody = test.request + .map(obj => JSON.stringify({ ...obj, ...attrs })) + .join('\n'); await supertest .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`) - .auth(user.username, user.password) - .field( - 'retries', - JSON.stringify([ - { - type: 'hiddentype', - id: '1', - overwrite: true, - }, - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - overwrite: true, - }, - ]) - ) - .attach( - 'file', - Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'), - 'export.ndjson' - ) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); + .auth(user?.username, user?.password) + .field('retries', retries) + .attach('file', Buffer.from(requestBody, 'utf8'), 'export.ndjson') + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); + } }); }; - const resolveImportErrorsTest = makeResolveImportErrorsTest(describe); + const addTests = makeResolveImportErrorsTest(describe); // @ts-ignore - resolveImportErrorsTest.only = makeResolveImportErrorsTest(describe.only); + addTests.only = makeResolveImportErrorsTest(describe.only); return { - resolveImportErrorsTest, - createExpectResults, - expectRbacForbidden, - expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, + addTests, + createTestDefinitions, + expectForbidden, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/update.ts b/x-pack/test/saved_object_api_integration/common/suites/update.ts index d6b7602c0114a..587e8cf320a4f 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/update.ts @@ -6,205 +6,97 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface UpdateTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface UpdateTests { - spaceAware: UpdateTest; - notSpaceAware: UpdateTest; - hiddenType: UpdateTest; - doesntExist: UpdateTest; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface UpdateTestDefinition extends TestDefinition { + request: { type: string; id: string }; } - -interface UpdateTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - otherSpaceId?: string; - tests: UpdateTests; +export type UpdateTestSuite = TestSuite; +export interface UpdateTestCase extends TestCase { + failure?: 403 | 404; } -export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectNotFound = (type: string, id: string, spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).eql({ - statusCode: 404, - error: 'Not Found', - message: `Saved object [${type}/${getIdPrefix(spaceId)}${id}] not found`, - }); - }; - - const createExpectDoesntExistNotFound = (spaceId?: string) => { - return createExpectNotFound('visualization', 'not an id', spaceId); - }; - - const createExpectSpaceAwareNotFound = (spaceId?: string) => { - return createExpectNotFound('visualization', 'dd7caf20-9efd-11e7-acb3-3dab96693fab', spaceId); - }; - - const expectHiddenTypeNotFound = createExpectNotFound( - 'hiddentype', - 'hiddentype_1', - DEFAULT_SPACE_ID - ); - - const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to update ${type}`, - }); - }; +const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake +const NEW_ATTRIBUTE_VAL = `Updated attribute value ${Date.now()}`; - const expectDoesntExistRbacForbidden = createExpectRbacForbidden('visualization'); +const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); +export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); - const expectNotSpaceAwareRbacForbidden = createExpectRbacForbidden('globaltype'); - - const expectHiddenTypeRbacForbidden = createExpectRbacForbidden('hiddentype'); - - const expectNotSpaceAwareResults = (resp: { [key: string]: any }) => { - // loose uuid validation - expect(resp.body) - .to.have.property('id') - .match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'globaltype', - updated_at: resp.body.updated_at, - version: resp.body.version, - attributes: { - name: 'My second favorite', - }, - }); +export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectForbidden = expectResponses.forbidden('update'); + const expectResponseBody = (testCase: UpdateTestCase): ExpectResponseBody => async ( + response: Record + ) => { + if (testCase.failure === 403) { + await expectForbidden(testCase.type)(response); + } else { + // permitted + const object = response.body; + await expectResponses.permitted(object, testCase); + if (!testCase.failure) { + expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + } + } }; - - const expectSpaceAwareRbacForbidden = createExpectRbacForbidden('visualization'); - - const expectSpaceAwareResults = (resp: { [key: string]: any }) => { - // loose uuid validation ignoring prefix - expect(resp.body) - .to.have.property('id') - .match(/[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'visualization', - updated_at: resp.body.updated_at, - version: resp.body.version, - attributes: { - title: 'My second favorite vis', - }, - }); + const createTestDefinitions = ( + testCases: UpdateTestCase | UpdateTestCase[], + forbidden: boolean, + options?: { + responseBodyOverride?: ExpectResponseBody; + } + ): UpdateTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); }; - const makeUpdateTest = (describeFn: DescribeFn) => ( + const makeUpdateTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: UpdateTestDefinition + definition: UpdateTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, otherSpaceId, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.spaceAware.statusCode} for a space-aware doc`, async () => { - await supertest - .put( - `${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix( - otherSpaceId || spaceId - )}dd7caf20-9efd-11e7-acb3-3dab96693fab` - ) - .auth(user.username, user.password) - .send({ - attributes: { - title: 'My second favorite vis', - }, - }) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response); - }); - - it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware doc`, async () => { - await supertest - .put( - `${getUrlPrefix( - otherSpaceId || spaceId - )}/api/saved_objects/globaltype/8121a00-8efd-21e7-1cb3-34ab966434445` - ) - .auth(user.username, user.password) - .send({ - attributes: { - name: 'My second favorite', - }, - }) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response); - }); - - it(`should return ${tests.hiddenType.statusCode} for hiddentype doc`, async () => { - await supertest - .put(`${getUrlPrefix(otherSpaceId || spaceId)}/api/saved_objects/hiddentype/hiddentype_1`) - .auth(user.username, user.password) - .send({ - attributes: { - name: 'My favorite hidden type', - }, - }) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); - }); - describe('unknown id', () => { - it(`should return ${tests.doesntExist.statusCode}`, async () => { + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const { type, id } = test.request; + const requestBody = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; await supertest - .put( - `${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix( - spaceId - )}not an id` - ) - .auth(user.username, user.password) - .send({ - attributes: { - title: 'My second favorite vis', - }, - }) - .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response); + .put(`${getUrlPrefix(spaceId)}/api/saved_objects/${type}/${id}`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); + } }); }; - const updateTest = makeUpdateTest(describe); + const addTests = makeUpdateTest(describe); // @ts-ignore - updateTest.only = makeUpdateTest(describe.only); + addTests.only = makeUpdateTest(describe.only); return { - createExpectDoesntExistNotFound, - createExpectSpaceAwareNotFound, - expectSpaceNotFound: expectHiddenTypeNotFound, - expectDoesntExistRbacForbidden, - expectNotSpaceAwareRbacForbidden, - expectNotSpaceAwareResults, - expectSpaceAwareRbacForbidden, - expectSpaceAwareResults, - expectHiddenTypeRbacForbidden, - updateTest, + addTests, + createTestDefinitions, }; } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts index 7768665f3b941..70d3afbfc9af3 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts @@ -4,206 +4,104 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkCreateTestSuiteFactory } from '../../common/suites/bulk_create'; +import { + bulkCreateTestSuiteFactory, + TEST_CASES as CASES, + BulkCreateTestDefinition, +} from '../../common/suites/bulk_create'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean, spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - bulkCreateTest, - createExpectResults, - createExpectRbacForbidden, - expectBadRequestForHiddenType, - expectedForbiddenTypesWithHiddenType, - } = bulkCreateTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = bulkCreateTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = (overwrite: boolean, spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite, spaceId); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true, overwrite, { spaceId }), + authorized: [ + createTestDefinitions(normalTypes, false, overwrite, { spaceId, singleRequest: true }), + createTestDefinitions(hiddenType, true, overwrite, { spaceId }), + createTestDefinitions(allTypes, true, overwrite, { + spaceId, + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, overwrite, { + spaceId, + singleRequest: true, + }), + }; + }; describe('_bulk_create', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - bulkCreateTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingSpace: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - }, - }); - - bulkCreateTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkCreateTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkCreateTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkCreateTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); + getTestScenarios([false, true]).securityAndSpaces.forEach( + ({ spaceId, users, modifier: overwrite }) => { + const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; + const { unauthorized, authorized, superuser } = createTests(overwrite!, spaceId); + const _addTests = (user: TestUser, tests: BulkCreateTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - bulkCreateTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - }); + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.allAtSpace].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); + } + ); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts index ec5bce1707569..09ea867bff371 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts @@ -4,205 +4,91 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkGetTestSuiteFactory } from '../../common/suites/bulk_get'; +import { + bulkGetTestSuiteFactory, + TEST_CASES as CASES, + BulkGetTestDefinition, +} from '../../common/suites/bulk_get'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const { - bulkGetTest, - createExpectResults, - createExpectRbacForbidden, - expectBadRequestForHiddenType, - expectedForbiddenTypesWithHiddenType, - } = bulkGetTestSuiteFactory(esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = bulkGetTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false, { singleRequest: true }), + createTestDefinitions(hiddenType, true), + createTestDefinitions(allTypes, true, { + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), + }; + }; describe('_bulk_get', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - bulkGetTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkGetTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - }, - }); - - bulkGetTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkGetTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: BulkGetTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - bulkGetTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, + [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach(user => { + _addTests(user, unauthorized); }); - - bulkGetTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, + [ + users.dualAll, + users.dualRead, + users.allGlobally, + users.readGlobally, + users.allAtSpace, + users.readAtSpace, + ].forEach(user => { + _addTests(user, authorized); }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts index 06240647b37a8..987209653b347 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts @@ -4,290 +4,91 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkUpdateTestSuiteFactory } from '../../common/suites/bulk_update'; +import { + bulkUpdateTestSuiteFactory, + TEST_CASES as CASES, + BulkUpdateTestDefinition, +} from '../../common/suites/bulk_update'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('bulkUpdate', () => { - const { - createExpectDoesntExistNotFound, - expectDoesntExistRbacForbidden, - expectNotSpaceAwareResults, - expectNotSpaceAwareRbacForbidden, - expectSpaceAwareRbacForbidden, - expectSpaceAwareResults, - expectSpaceNotFound, - expectHiddenTypeRbacForbidden, - expectHiddenTypeRbacForbiddenWithGlobalAllowed, - bulkUpdateTest, - } = bulkUpdateTestSuiteFactory(esArchiver, supertest); - - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - bulkUpdateTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 200, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - bulkUpdateTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbiddenWithGlobalAllowed, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions, expectForbidden } = bulkUpdateTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false, { singleRequest: true }), + createTestDefinitions(hiddenType, true), + createTestDefinitions(allTypes, true, { + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), + }; + }; - bulkUpdateTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); + describe('_bulk_update', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: BulkUpdateTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - bulkUpdateTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbiddenWithGlobalAllowed, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); }); - - bulkUpdateTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbiddenWithGlobalAllowed, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - bulkUpdateTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, + [users.dualAll, users.allGlobally, users.allAtSpace].forEach(user => { + _addTests(user, authorized); }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts index e4adaa580c1db..7278504b8f0e8 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts @@ -4,248 +4,91 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createTestSuiteFactory } from '../../common/suites/create'; +import { + createTestSuiteFactory, + TEST_CASES as CASES, + CreateTestDefinition, +} from '../../common/suites/create'; -export default function({ getService }: FtrProviderContext) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('legacyEs'); - const esArchiver = getService('esArchiver'); - - const { - createTest, - createExpectSpaceAwareResults, - expectNotSpaceAwareResults, - expectNotSpaceAwareRbacForbidden, - expectSpaceAwareRbacForbidden, - expectBadRequestForHiddenType, - expectHiddenTypeRbacForbidden, - } = createTestSuiteFactory(es, esArchiver, supertestWithoutAuth); - - describe('create', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - createTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 400, - response: expectBadRequestForHiddenType, - }, - }, - }); +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; - createTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); +const createTestCases = (overwrite: boolean, spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; - createTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + const es = getService('legacyEs'); - createTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); + const { addTests, createTestDefinitions } = createTestSuiteFactory(es, esArchiver, supertest); + const createTests = (overwrite: boolean, spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite, spaceId); + return { + unauthorized: createTestDefinitions(allTypes, true, overwrite, { spaceId }), + authorized: [ + createTestDefinitions(normalTypes, false, overwrite, { spaceId }), + createTestDefinitions(hiddenType, true, overwrite, { spaceId }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, overwrite, { spaceId }), + }; + }; - createTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); + describe('_create', () => { + getTestScenarios([false, true]).securityAndSpaces.forEach( + ({ spaceId, users, modifier: overwrite }) => { + const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; + const { unauthorized, authorized, superuser } = createTests(overwrite!, spaceId); + const _addTests = (user: TestUser, tests: CreateTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - createTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - }); + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.allAtSpace].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); + } + ); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts index bfd2112428db4..995b8fc2422d9 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts @@ -4,288 +4,83 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { deleteTestSuiteFactory } from '../../common/suites/delete'; +import { + deleteTestSuiteFactory, + TEST_CASES as CASES, + DeleteTestDefinition, +} from '../../common/suites/delete'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('delete', () => { - const { - createExpectUnknownDocNotFound, - deleteTest, - expectEmpty, - expectRbacSpaceAwareForbidden, - expectRbacNotSpaceAwareForbidden, - expectRbacInvalidIdForbidden, - expectGenericNotFound, - expectRbacHiddenTypeForbidden, - } = deleteTestSuiteFactory(esArchiver, supertest); - - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - deleteTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 404, - response: expectGenericNotFound, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(scenario.spaceId), - }, - }, - }); - - deleteTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(scenario.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = deleteTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + return { + unauthorized: createTestDefinitions(allTypes, true, { spaceId }), + authorized: [ + createTestDefinitions(normalTypes, false, { spaceId }), + createTestDefinitions(hiddenType, true, { spaceId }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, { spaceId }), + }; + }; - deleteTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); + describe('_delete', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: DeleteTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - deleteTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(scenario.spaceId), - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); }); - - deleteTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(scenario.spaceId), - }, - }, - }); - - deleteTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, + [users.dualAll, users.allGlobally, users.allAtSpace].forEach(user => { + _addTests(user, authorized); }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts index b64c3ed87c35d..6f2426e55c6a6 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts @@ -4,274 +4,70 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; -import { SPACES } from '../../common/lib/spaces'; +import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { exportTestSuiteFactory } from '../../common/suites/export'; +import { + exportTestSuiteFactory, + getTestCases, + ExportTestDefinition, +} from '../../common/suites/export'; + +const createTestCases = (spaceId: string) => { + const cases = getTestCases(spaceId); + const exportableTypes = [ + cases.singleNamespaceObject, + cases.singleNamespaceType, + cases.namespaceAgnosticObject, + cases.namespaceAgnosticType, + ]; + const nonExportableTypes = [ + cases.multiNamespaceObject, + cases.multiNamespaceType, + cases.hiddenObject, + cases.hiddenType, + ]; + const allTypes = exportableTypes.concat(nonExportableTypes); + return { exportableTypes, nonExportableTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('export', () => { - const { - createExpectRbacForbidden, - expectTypeOrObjectsRequired, - createExpectVisualizationResults, - expectInvalidTypeSpecified, - exportTest, - } = exportTestSuiteFactory(esArchiver, supertest); + const { addTests, createTestDefinitions } = exportTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { exportableTypes, nonExportableTypes, allTypes } = createTestCases(spaceId); + return { + unauthorized: [ + createTestDefinitions(exportableTypes, true), + createTestDefinitions(nonExportableTypes, false), + ].flat(), + authorized: createTestDefinitions(allTypes, false), + }; + }; - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - exportTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); + describe('_export', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized } = createTests(spaceId); + const _addTests = (user: TestUser, tests: ExportTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - exportTest(`superuser with the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, + [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach(user => { + _addTests(user, unauthorized); }); - - exportTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`rbac user with all at the other space within ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, + [ + users.dualAll, + users.dualRead, + users.allGlobally, + users.readGlobally, + users.allAtSpace, + users.readAtSpace, + users.superuser, + ].forEach(user => { + _addTests(user, authorized); }); }); }); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts index 366b8b44585cd..7c16c01d203c0 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts @@ -4,727 +4,71 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; -import { SPACES } from '../../common/lib/spaces'; +import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { findTestSuiteFactory } from '../../common/suites/find'; +import { findTestSuiteFactory, getTestCases, FindTestDefinition } from '../../common/suites/find'; + +const createTestCases = (spaceId: string) => { + const cases = getTestCases(spaceId); + const normalTypes = [ + cases.singleNamespaceType, + cases.multiNamespaceType, + cases.namespaceAgnosticType, + cases.pageBeyondTotal, + cases.unknownSearchField, + cases.filterWithNamespaceAgnosticType, + cases.filterWithDisallowedType, + ]; + const hiddenAndUnknownTypes = [ + cases.hiddenType, + cases.unknownType, + cases.filterWithHiddenType, + cases.filterWithUnknownType, + ]; + const allTypes = normalTypes.concat(hiddenAndUnknownTypes); + return { normalTypes, hiddenAndUnknownTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('find', () => { - const { - createExpectEmpty, - createExpectRbacForbidden, - createExpectVisualizationResults, - expectFilterWrongTypeError, - expectNotSpaceAwareResults, - expectTypeRequired, - findTest, - } = findTestSuiteFactory(esArchiver, supertest); + const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenAndUnknownTypes, allTypes } = createTestCases(spaceId); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenAndUnknownTypes, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - findTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and find url message', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); + describe('_find', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: FindTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - findTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithUnknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, + [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach(user => { + _addTests(user, unauthorized); }); - - findTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and find url message', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); - - findTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and find url message', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, + [ + users.dualAll, + users.dualRead, + users.allGlobally, + users.readGlobally, + users.allAtSpace, + users.readAtSpace, + ].forEach(user => { + _addTests(user, authorized); }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/get.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/get.ts index 9667abcc5e57a..9e3203e147493 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/get.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/get.ts @@ -4,289 +4,84 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { getTestSuiteFactory } from '../../common/suites/get'; +import { + getTestSuiteFactory, + TEST_CASES as CASES, + GetTestDefinition, +} from '../../common/suites/get'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const { - createExpectDoesntExistNotFound, - createExpectSpaceAwareResults, - createExpectNotSpaceAwareResults, - expectSpaceAwareRbacForbidden, - expectNotSpaceAwareRbacForbidden, - expectDoesntExistRbacForbidden, - expectHiddenTypeRbacForbidden, - expectHiddenTypeNotFound, - getTest, - } = getTestSuiteFactory(esArchiver, supertest); - - describe('get', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - getTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - getTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 404, - response: expectHiddenTypeNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - getTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - getTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = getTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenType, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - getTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); + describe('_get', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: GetTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - getTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, + [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach(user => { + _addTests(user, unauthorized); }); - - getTest(`rbac user with read globall within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - getTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - getTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - getTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, + [ + users.dualAll, + users.dualRead, + users.allGlobally, + users.readGlobally, + users.allAtSpace, + users.readAtSpace, + ].forEach(user => { + _addTests(user, authorized); }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index 58859c292ce35..10c7f61dce5cc 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -4,245 +4,92 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { importTestSuiteFactory } from '../../common/suites/import'; +import { + importTestSuiteFactory, + TEST_CASES as CASES, + ImportTestDefinition, +} from '../../common/suites/import'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const importableTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(spaceId === DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(spaceId === SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const nonImportableTypes = [ + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + ]; + const allTypes = importableTypes.concat(nonImportableTypes); + return { importableTypes, nonImportableTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - importTest, - createExpectResults, - expectRbacForbidden, - expectUnknownTypeUnsupported: expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, - } = importTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = importTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = (spaceId: string) => { + const { importableTypes, nonImportableTypes, allTypes } = createTestCases(spaceId); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: [ + createTestDefinitions(importableTypes, true, { spaceId }), + createTestDefinitions(nonImportableTypes, false, { spaceId, singleRequest: true }), + createTestDefinitions(allTypes, true, { + spaceId, + singleRequest: true, + responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + }), + ].flat(), + authorized: createTestDefinitions(allTypes, false, { spaceId, singleRequest: true }), + }; + }; describe('_import', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - importTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized } = createTests(spaceId); + const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - importTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); }); - - importTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, + [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach(user => { + _addTests(user, authorized); }); }); }); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts index bb42c5422ece5..46d7ab6425989 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -20,6 +20,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./bulk_create')); loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./bulk_update')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./export')); @@ -28,6 +29,5 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./import')); loadTestFile(require.resolve('./resolve_import_errors')); loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./bulk_update')); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts index 6c91fe6310170..8e8fe874b4317 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts @@ -4,258 +4,99 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { resolveImportErrorsTestSuiteFactory } from '../../common/suites/resolve_import_errors'; +import { + resolveImportErrorsTestSuiteFactory, + TEST_CASES as CASES, + ResolveImportErrorsTestDefinition, +} from '../../common/suites/resolve_import_errors'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean, spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const importableTypes = [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const nonImportableTypes = [ + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + ]; + const allTypes = importableTypes.concat(nonImportableTypes); + return { importableTypes, nonImportableTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - resolveImportErrorsTest, - createExpectResults, - expectRbacForbidden, - expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, - } = resolveImportErrorsTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = resolveImportErrorsTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = (overwrite: boolean, spaceId: string) => { + const { importableTypes, nonImportableTypes, allTypes } = createTestCases(overwrite, spaceId); + const singleRequest = true; + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: [ + createTestDefinitions(importableTypes, true, overwrite, { spaceId }), + createTestDefinitions(nonImportableTypes, false, overwrite, { spaceId, singleRequest }), + createTestDefinitions(allTypes, true, overwrite, { + spaceId, + singleRequest, + responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + }), + ].flat(), + authorized: createTestDefinitions(allTypes, false, overwrite, { spaceId, singleRequest }), + }; + }; describe('_resolve_import_errors', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - resolveImportErrorsTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest( - `dual-privileges readonly user within the ${scenario.spaceId} space`, - { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - } - ); - - resolveImportErrorsTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest( - `rbac user with all at the space within the ${scenario.spaceId} space`, - { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - } - ); - - resolveImportErrorsTest( - `rbac user with read at the space within the ${scenario.spaceId} space`, - { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - } - ); + getTestScenarios([false, true]).securityAndSpaces.forEach( + ({ spaceId, users, modifier: overwrite }) => { + const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; + const { unauthorized, authorized } = createTests(overwrite!, spaceId); + const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - resolveImportErrorsTest( - `rbac user with all at other space within the ${scenario.spaceId} space`, - { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - } - ); - }); + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach(user => { + _addTests(user, authorized); + }); + } + ); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/update.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/update.ts index 8eb06e41e2a41..21f354d2a8e76 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/update.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/update.ts @@ -4,289 +4,83 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { updateTestSuiteFactory } from '../../common/suites/update'; +import { + updateTestSuiteFactory, + TEST_CASES as CASES, + UpdateTestDefinition, +} from '../../common/suites/update'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('update', () => { - const { - createExpectDoesntExistNotFound, - expectDoesntExistRbacForbidden, - expectNotSpaceAwareResults, - expectNotSpaceAwareRbacForbidden, - expectSpaceAwareRbacForbidden, - expectSpaceAwareResults, - expectSpaceNotFound, - expectHiddenTypeRbacForbidden, - updateTest, - } = updateTestSuiteFactory(esArchiver, supertest); - - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - updateTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 404, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - updateTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = updateTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenType, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - updateTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); + describe('_update', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: UpdateTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - updateTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); }); - - updateTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - updateTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, + [users.dualAll, users.allGlobally, users.allAtSpace].forEach(user => { + _addTests(user, authorized); }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts index 943a22c4399c7..5b3397c7909ae 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts @@ -4,176 +4,88 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkCreateTestSuiteFactory } from '../../common/suites/bulk_create'; +import { + bulkCreateTestSuiteFactory, + TEST_CASES as CASES, + BulkCreateTestDefinition, +} from '../../common/suites/bulk_create'; + +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, + CASES.SINGLE_NAMESPACE_SPACE_1, + CASES.SINGLE_NAMESPACE_SPACE_2, + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409() }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - bulkCreateTest, - createExpectResults, - createExpectRbacForbidden, - expectBadRequestForHiddenType, - expectedForbiddenTypesWithHiddenType: expectedForbiddenTypesWithHiddenType, - } = bulkCreateTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = bulkCreateTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = (overwrite: boolean) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true, overwrite), + authorized: [ + createTestDefinitions(normalTypes, false, overwrite, { singleRequest: true }), + createTestDefinitions(hiddenType, true, overwrite), + createTestDefinitions(allTypes, true, overwrite, { + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, overwrite, { singleRequest: true }), + }; + }; describe('_bulk_create', () => { - bulkCreateTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingSpace: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - }, - }); - - bulkCreateTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkCreateTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkCreateTest(`rbac readonly user`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); + getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const { unauthorized, authorized, superuser } = createTests(overwrite!); + const _addTests = (user: TestUser, tests: BulkCreateTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, tests }); + }; - bulkCreateTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts index fde98694fe575..69494ed254669 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts @@ -4,175 +4,81 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkGetTestSuiteFactory } from '../../common/suites/bulk_get'; +import { + bulkGetTestSuiteFactory, + TEST_CASES as CASES, + BulkGetTestDefinition, +} from '../../common/suites/bulk_get'; + +const { fail400, fail404 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, + CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const { - bulkGetTest, - createExpectResults, - createExpectRbacForbidden, - expectedForbiddenTypesWithHiddenType, - expectBadRequestForHiddenType, - } = bulkGetTestSuiteFactory(esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = bulkGetTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = () => { + const { normalTypes, hiddenType, allTypes } = createTestCases(); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false, { singleRequest: true }), + createTestDefinitions(hiddenType, true), + createTestDefinitions(allTypes, true, { + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), + }; + }; describe('_bulk_get', () => { - bulkGetTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkGetTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingHiddenType: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - }, - }); - - bulkGetTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkGetTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkGetTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkGetTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: BulkGetTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - bulkGetTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, + [ + users.noAccess, + users.legacyAll, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.dualRead, users.allGlobally, users.readGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts index 6f4635f17cf8c..fb169f4c6fb86 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts @@ -4,268 +4,83 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkUpdateTestSuiteFactory } from '../../common/suites/bulk_update'; +import { + bulkUpdateTestSuiteFactory, + TEST_CASES as CASES, + BulkUpdateTestDefinition, +} from '../../common/suites/bulk_update'; + +const { fail404 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, + CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('bulkUpdate', () => { - const { - createExpectDoesntExistNotFound, - expectDoesntExistRbacForbidden, - expectNotSpaceAwareResults, - expectNotSpaceAwareRbacForbidden, - expectSpaceAwareRbacForbidden, - expectSpaceAwareResults, - expectSpaceNotFound, - expectHiddenTypeRbacForbidden, - expectHiddenTypeRbacForbiddenWithGlobalAllowed, - bulkUpdateTest, - } = bulkUpdateTestSuiteFactory(esArchiver, supertest); - - bulkUpdateTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 200, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(), - }, - }, - }); + const { addTests, createTestDefinitions, expectForbidden } = bulkUpdateTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = () => { + const { normalTypes, hiddenType, allTypes } = createTestCases(); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false, { singleRequest: true }), + createTestDefinitions(hiddenType, true), + createTestDefinitions(allTypes, true, { + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), + }; + }; - bulkUpdateTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbiddenWithGlobalAllowed, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - bulkUpdateTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbiddenWithGlobalAllowed, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - bulkUpdateTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); + describe('_bulk_update', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: BulkUpdateTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - bulkUpdateTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts index 60a9fa0a86aa6..dc8e564e42477 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts @@ -4,222 +4,79 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createTestSuiteFactory } from '../../common/suites/create'; +import { + createTestSuiteFactory, + TEST_CASES as CASES, + CreateTestDefinition, +} from '../../common/suites/create'; -export default function({ getService }: FtrProviderContext) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('legacyEs'); - const esArchiver = getService('esArchiver'); - - const { - createTest, - createExpectSpaceAwareResults, - expectNotSpaceAwareResults, - expectNotSpaceAwareRbacForbidden, - expectSpaceAwareRbacForbidden, - expectBadRequestForHiddenType, - expectHiddenTypeRbacForbidden, - } = createTestSuiteFactory(es, esArchiver, supertestWithoutAuth); - - describe('create', () => { - createTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); +const { fail400, fail409 } = testCaseFailures; - createTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 400, - response: expectBadRequestForHiddenType, - }, - }, - }); - - createTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); +const createTestCases = (overwrite: boolean) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, + CASES.SINGLE_NAMESPACE_SPACE_1, + CASES.SINGLE_NAMESPACE_SPACE_2, + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409() }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; - createTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + const es = getService('legacyEs'); - createTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); + const { addTests, createTestDefinitions } = createTestSuiteFactory(es, esArchiver, supertest); + const createTests = (overwrite: boolean) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite); + return { + unauthorized: createTestDefinitions(allTypes, true, overwrite), + authorized: [ + createTestDefinitions(normalTypes, false, overwrite), + createTestDefinitions(hiddenType, true, overwrite), + ].flat(), + superuser: createTestDefinitions(allTypes, false, overwrite), + }; + }; - createTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); + describe('_create', () => { + getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const { unauthorized, authorized, superuser } = createTests(overwrite!); + const _addTests = (user: TestUser, tests: CreateTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, tests }); + }; - createTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts b/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts index f775b5a365d6b..05939197be352 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts @@ -4,266 +4,75 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { deleteTestSuiteFactory } from '../../common/suites/delete'; +import { + deleteTestSuiteFactory, + TEST_CASES as CASES, + DeleteTestDefinition, +} from '../../common/suites/delete'; + +const { fail404 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, + CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('delete', () => { - const { - createExpectUnknownDocNotFound, - deleteTest, - expectEmpty, - expectRbacSpaceAwareForbidden, - expectRbacNotSpaceAwareForbidden, - expectRbacInvalidIdForbidden, - expectRbacHiddenTypeForbidden: expectRbacSpaceTypeForbidden, - expectGenericNotFound, - } = deleteTestSuiteFactory(esArchiver, supertest); - - deleteTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 404, - response: expectGenericNotFound, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(), - }, - }, - }); + const { addTests, createTestDefinitions } = deleteTestSuiteFactory(esArchiver, supertest); + const createTests = () => { + const { normalTypes, hiddenType, allTypes } = createTestCases(); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenType, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - deleteTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(), - }, - }, - }); - - deleteTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(), - }, - }, - }); - - deleteTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); + describe('_delete', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: DeleteTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - deleteTest(`rbac user with readonly at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts index 2a2c3a9b90b08..0fae45a1897a7 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts @@ -4,252 +4,75 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { exportTestSuiteFactory } from '../../common/suites/export'; +import { + exportTestSuiteFactory, + getTestCases, + ExportTestDefinition, +} from '../../common/suites/export'; + +const createTestCases = () => { + const cases = getTestCases(); + const exportableTypes = [ + cases.singleNamespaceObject, + cases.singleNamespaceType, + cases.namespaceAgnosticObject, + cases.namespaceAgnosticType, + ]; + const nonExportableTypes = [ + cases.multiNamespaceObject, + cases.multiNamespaceType, + cases.hiddenObject, + cases.hiddenType, + ]; + const allTypes = exportableTypes.concat(nonExportableTypes); + return { exportableTypes, nonExportableTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('export', () => { - const { - createExpectRbacForbidden, - expectTypeOrObjectsRequired, - createExpectVisualizationResults, - expectInvalidTypeSpecified, - exportTest, - } = exportTestSuiteFactory(esArchiver, supertest); - - exportTest('user with no access', { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('superuser', { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('legacy user', { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('dual-privileges user', { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('dual-privileges readonly user', { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('rbac user with all globally', { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('rbac user with read globally', { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); + const { addTests, createTestDefinitions } = exportTestSuiteFactory(esArchiver, supertest); + const createTests = () => { + const { exportableTypes, nonExportableTypes, allTypes } = createTestCases(); + return { + unauthorized: [ + createTestDefinitions(exportableTypes, true), + createTestDefinitions(nonExportableTypes, false), + ].flat(), + authorized: createTestDefinitions(allTypes, false), + }; + }; - exportTest('rbac user with all at default space', { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('rbac user with read at default space', { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('rbac user with all at space_1', { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); + describe('_export', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized } = createTests(); + const _addTests = (user: TestUser, tests: ExportTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - exportTest('rbac user with read at space_1', { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [ + users.dualAll, + users.dualRead, + users.allGlobally, + users.readGlobally, + users.superuser, + ].forEach(user => { + _addTests(user, authorized); + }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts index 64d85a199e7bc..97513783b94b9 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts @@ -4,749 +4,70 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { findTestSuiteFactory } from '../../common/suites/find'; +import { findTestSuiteFactory, getTestCases, FindTestDefinition } from '../../common/suites/find'; + +const createTestCases = () => { + const cases = getTestCases(); + const normalTypes = [ + cases.singleNamespaceType, + cases.multiNamespaceType, + cases.namespaceAgnosticType, + cases.pageBeyondTotal, + cases.unknownSearchField, + cases.filterWithNamespaceAgnosticType, + cases.filterWithDisallowedType, + ]; + const hiddenAndUnknownTypes = [ + cases.hiddenType, + cases.unknownType, + cases.filterWithHiddenType, + cases.filterWithUnknownType, + ]; + const allTypes = normalTypes.concat(hiddenAndUnknownTypes); + return { normalTypes, hiddenAndUnknownTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('find', () => { - const { - createExpectEmpty, - createExpectRbacForbidden, - createExpectVisualizationResults, - expectFilterWrongTypeError, - expectNotSpaceAwareResults, - expectTypeRequired, - findTest, - } = findTestSuiteFactory(esArchiver, supertest); - - findTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden login and find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and unknown search field', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden login and find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); - - findTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithUnknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden login and find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and unknown search field', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden login and find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); - - findTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); + const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); + const createTests = () => { + const { normalTypes, hiddenAndUnknownTypes, allTypes } = createTestCases(); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenAndUnknownTypes, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - findTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and unknown search field', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); - - findTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and unknown search field', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); - - findTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and unknown search field', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); + describe('_find', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: FindTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - findTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and unknown search field', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, + [ + users.noAccess, + users.legacyAll, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.dualRead, users.allGlobally, users.readGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/get.ts b/x-pack/test/saved_object_api_integration/security_only/apis/get.ts index 2a31463fce8b2..7cd50fe4cea61 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/get.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/get.ts @@ -4,267 +4,73 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { getTestSuiteFactory } from '../../common/suites/get'; +import { + getTestSuiteFactory, + TEST_CASES as CASES, + GetTestDefinition, +} from '../../common/suites/get'; + +const { fail404 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, + CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const { - createExpectDoesntExistNotFound, - createExpectSpaceAwareResults, - createExpectNotSpaceAwareResults, - expectSpaceAwareRbacForbidden, - expectNotSpaceAwareRbacForbidden, - expectDoesntExistRbacForbidden, - expectHiddenTypeRbacForbidden, - expectHiddenTypeNotFound, - getTest, - } = getTestSuiteFactory(esArchiver, supertest); - - describe('get', () => { - getTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - getTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(), - }, - hiddenType: { - statusCode: 404, - response: expectHiddenTypeNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); + const { addTests, createTestDefinitions } = getTestSuiteFactory(esArchiver, supertest); + const createTests = () => { + const { normalTypes, hiddenType, allTypes } = createTestCases(); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenType, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - getTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - getTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - getTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - getTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - getTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - getTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - getTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - getTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); + describe('_get', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: GetTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - getTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.dualRead, users.allGlobally, users.readGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index 770410dcfed81..5a6e530b02939 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -4,223 +4,87 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { importTestSuiteFactory } from '../../common/suites/import'; +import { + importTestSuiteFactory, + TEST_CASES as CASES, + ImportTestDefinition, +} from '../../common/suites/import'; + +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const importableTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409() }, + CASES.SINGLE_NAMESPACE_SPACE_1, + CASES.SINGLE_NAMESPACE_SPACE_2, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const nonImportableTypes = [ + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + ]; + const allTypes = importableTypes.concat(nonImportableTypes); + return { importableTypes, nonImportableTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - importTest, - createExpectResults, - expectRbacForbidden, - expectUnknownTypeUnsupported, - expectResultsWithUnsupportedHiddenType, - } = importTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = importTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = () => { + const { importableTypes, nonImportableTypes, allTypes } = createTestCases(); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: [ + createTestDefinitions(importableTypes, true), + createTestDefinitions(nonImportableTypes, false, { singleRequest: true }), + createTestDefinitions(allTypes, true, { + singleRequest: true, + responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + }), + ].flat(), + authorized: createTestDefinitions(allTypes, false, { singleRequest: true }), + }; + }; describe('_import', () => { - importTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - hiddenType: { - // import filters out the space type, so the remaining objects will import successfully - statusCode: 200, - response: expectResultsWithUnsupportedHiddenType, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - hiddenType: { - // import filters out the space type, so the remaining objects will import successfully - statusCode: 200, - response: expectResultsWithUnsupportedHiddenType, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - hiddenType: { - // import filters out the space type, so the remaining objects will import successfully - statusCode: 200, - response: expectResultsWithUnsupportedHiddenType, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest(`rbac readonly user`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized } = createTests(); + const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - importTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.superuser].forEach(user => { + _addTests(user, authorized); + }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts index bb637a9bc4c90..f581e18ff17af 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts @@ -20,6 +20,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./bulk_create')); loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./bulk_update')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./export')); @@ -28,6 +29,5 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./import')); loadTestFile(require.resolve('./resolve_import_errors')); loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./bulk_update')); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts index 59d50c16c259a..f945d2b64c432 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts @@ -4,221 +4,88 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { resolveImportErrorsTestSuiteFactory } from '../../common/suites/resolve_import_errors'; +import { + resolveImportErrorsTestSuiteFactory, + TEST_CASES as CASES, + ResolveImportErrorsTestDefinition, +} from '../../common/suites/resolve_import_errors'; + +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const importableTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, + CASES.SINGLE_NAMESPACE_SPACE_1, + CASES.SINGLE_NAMESPACE_SPACE_2, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const nonImportableTypes = [ + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + ]; + const allTypes = importableTypes.concat(nonImportableTypes); + return { importableTypes, nonImportableTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - resolveImportErrorsTest, - createExpectResults, - - expectRbacForbidden, - expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, - } = resolveImportErrorsTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = resolveImportErrorsTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = (overwrite: boolean) => { + const { importableTypes, nonImportableTypes, allTypes } = createTestCases(overwrite); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: [ + createTestDefinitions(importableTypes, true, overwrite), + createTestDefinitions(nonImportableTypes, false, overwrite, { singleRequest: true }), + createTestDefinitions(allTypes, true, overwrite, { + singleRequest: true, + responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + }), + ].flat(), + authorized: createTestDefinitions(allTypes, false, overwrite, { singleRequest: true }), + }; + }; describe('_resolve_import_errors', () => { - resolveImportErrorsTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest(`rbac readonly user`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); + getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const { unauthorized, authorized } = createTests(overwrite!); + const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, tests }); + }; - resolveImportErrorsTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.superuser].forEach(user => { + _addTests(user, authorized); + }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/update.ts b/x-pack/test/saved_object_api_integration/security_only/apis/update.ts index 8564296bdf558..e1e3a5f8a7dc7 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/update.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/update.ts @@ -4,267 +4,75 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { updateTestSuiteFactory } from '../../common/suites/update'; +import { + updateTestSuiteFactory, + TEST_CASES as CASES, + UpdateTestDefinition, +} from '../../common/suites/update'; + +const { fail404 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, + CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('update', () => { - const { - createExpectDoesntExistNotFound, - expectDoesntExistRbacForbidden, - expectNotSpaceAwareResults, - expectNotSpaceAwareRbacForbidden, - expectSpaceAwareRbacForbidden, - expectSpaceAwareResults, - expectSpaceNotFound, - expectHiddenTypeRbacForbidden, - updateTest, - } = updateTestSuiteFactory(esArchiver, supertest); - - updateTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 404, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); + const { addTests, createTestDefinitions } = updateTestSuiteFactory(esArchiver, supertest); + const createTests = () => { + const { normalTypes, hiddenType, allTypes } = createTestCases(); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenType, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - updateTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - updateTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - updateTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); + describe('_update', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: UpdateTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - updateTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts index 690e358b744d5..70d74822a8b0f 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts @@ -6,83 +6,72 @@ import expect from '@kbn/expect'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkCreateTestSuiteFactory } from '../../common/suites/bulk_create'; +import { bulkCreateTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/bulk_create'; -const expectNamespaceSpecifiedBadRequest = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Bad Request', - message: '[request body.0.namespace]: definition for this key is missing', - statusCode: 400, - }); -}; +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean, spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - bulkCreateTest, - createExpectResults, - expectBadRequestForHiddenType, - } = bulkCreateTestSuiteFactory(es, esArchiver, supertest); - - describe('_bulk_create', () => { - bulkCreateTest('in the current space (space_1)', { - ...SPACES.SPACE_1, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.SPACE_1.spaceId), - }, - includingSpace: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - custom: { - description: 'when a namespace is specified on the saved object', - requestBody: [ - { - type: 'visualization', - namespace: 'space_1', - attributes: { - title: 'something', - }, - }, - ], - statusCode: 400, - response: expectNamespaceSpecifiedBadRequest, + const { addTests, createTestDefinitions } = bulkCreateTestSuiteFactory(es, esArchiver, supertest); + const createTests = (overwrite: boolean, spaceId: string) => { + const testCases = createTestCases(overwrite, spaceId); + return createTestDefinitions(testCases, false, overwrite, { + spaceId, + singleRequest: true, + }).concat( + ['namespace', 'namespaces'].map(key => ({ + title: `(bad request) when ${key} is specified on the saved object`, + request: [{ type: 'isolatedtype', id: 'some-id', [key]: 'any-value' }] as any, + responseStatusCode: 400, + responseBody: async (response: Record) => { + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: `[request body.0.${key}]: definition for this key is missing`, + }); }, - }, - }); + overwrite, + })) + ); + }; - bulkCreateTest('in the default space', { - ...SPACES.DEFAULT, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.DEFAULT.spaceId), - }, - includingSpace: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - custom: { - description: 'when a namespace is specified on the saved object', - requestBody: [ - { - type: 'visualization', - namespace: 'space_1', - attributes: { - title: 'something', - }, - }, - ], - statusCode: 400, - response: expectNamespaceSpecifiedBadRequest, - }, - }, + describe('_bulk_create', () => { + getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const tests = createTests(overwrite!, spaceId); + addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts index bed29f8eb39a1..ad10719750585 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts @@ -5,48 +5,48 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkGetTestSuiteFactory } from '../../common/suites/bulk_get'; +import { bulkGetTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/bulk_get'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const { - bulkGetTest, - createExpectResults, - createExpectNotFoundResults, - expectBadRequestForHiddenType, - } = bulkGetTestSuiteFactory(esArchiver, supertest); + const { addTests, createTestDefinitions } = bulkGetTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false, { singleRequest: true }); + }; describe('_bulk_get', () => { - bulkGetTest(`objects within the current space (space_1)`, { - ...SPACES.SPACE_1, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.SPACE_1.spaceId), - }, - includingHiddenType: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - }, - }); - - bulkGetTest(`objects within another space`, { - ...SPACES.SPACE_1, - otherSpaceId: SPACES.SPACE_2.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectNotFoundResults(SPACES.SPACE_2.spaceId), - }, - includingHiddenType: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - }, + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts index 0681908a765ce..be6ce9e30c56e 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts @@ -5,88 +5,48 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkUpdateTestSuiteFactory } from '../../common/suites/bulk_update'; +import { bulkUpdateTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/bulk_update'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.HIDDEN, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('bulkUpdate', () => { - const { - createExpectSpaceAwareNotFound, - expectSpaceAwareResults, - createExpectDoesntExistNotFound, - expectNotSpaceAwareResults, - expectSpaceNotFound, - bulkUpdateTest, - } = bulkUpdateTestSuiteFactory(esArchiver, supertest); - - bulkUpdateTest(`in the default space`, { - spaceId: SPACES.DEFAULT.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 200, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(SPACES.DEFAULT.spaceId), - }, - }, - }); - - bulkUpdateTest('in the current space (space_1)', { - spaceId: SPACES.SPACE_1.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 200, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(SPACES.SPACE_1.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = bulkUpdateTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false, { singleRequest: true }); + }; - bulkUpdateTest('objects that exist in another space (space_1)', { - spaceId: SPACES.DEFAULT.spaceId, - otherSpaceId: SPACES.SPACE_1.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareNotFound(SPACES.SPACE_1.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 200, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(), - }, - }, + describe('_bulk_update', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts index 3bd4019649363..d0c6a21e73971 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts @@ -4,90 +4,56 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createTestSuiteFactory } from '../../common/suites/create'; +import { createTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/create'; -const expectNamespaceSpecifiedBadRequest = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Bad Request', - message: '[request body.namespace]: definition for this key is missing', - statusCode: 400, - }); -}; +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean, spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, +]; export default function({ getService }: FtrProviderContext) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('legacyEs'); + const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); + const es = getService('legacyEs'); - const { - createTest, - createExpectSpaceAwareResults, - expectNotSpaceAwareResults, - expectBadRequestForHiddenType, - } = createTestSuiteFactory(es, esArchiver, supertestWithoutAuth); - - describe('create', () => { - createTest('in the current space (space_1)', { - ...SPACES.SPACE_1, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(SPACES.SPACE_1.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 400, - response: expectBadRequestForHiddenType, - }, - custom: { - description: 'when a namespace is specified on the saved object', - type: 'visualization', - requestBody: { - namespace: 'space_1', - attributes: { - title: 'something', - }, - }, - statusCode: 400, - response: expectNamespaceSpecifiedBadRequest, - }, - }, - }); + const { addTests, createTestDefinitions } = createTestSuiteFactory(es, esArchiver, supertest); + const createTests = (overwrite: boolean, spaceId: string) => { + const testCases = createTestCases(overwrite, spaceId); + return createTestDefinitions(testCases, false, overwrite, { spaceId }); + }; - createTest('in the default space', { - ...SPACES.DEFAULT, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(SPACES.DEFAULT.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 400, - response: expectBadRequestForHiddenType, - }, - custom: { - description: 'when a namespace is specified on the saved object', - type: 'visualization', - requestBody: { - namespace: 'space_1', - attributes: { - title: 'something', - }, - }, - statusCode: 400, - response: expectNamespaceSpecifiedBadRequest, - }, - }, + describe('_create', () => { + getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const tests = createTests(overwrite!, spaceId); + addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts index 437b3f95024c6..bb48e06d93a09 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts @@ -5,87 +5,48 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { deleteTestSuiteFactory } from '../../common/suites/delete'; +import { deleteTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/delete'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.HIDDEN, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('delete', () => { - const { - createExpectSpaceAwareNotFound, - createExpectUnknownDocNotFound, - deleteTest, - expectEmpty, - expectGenericNotFound, - } = deleteTestSuiteFactory(esArchiver, supertest); - - deleteTest(`in the default space`, { - ...SPACES.DEFAULT, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 404, - response: expectGenericNotFound, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(SPACES.DEFAULT.spaceId), - }, - }, - }); - - deleteTest(`in the current space (space_1)`, { - ...SPACES.SPACE_1, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 404, - response: expectGenericNotFound, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(SPACES.SPACE_1.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = deleteTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false, { spaceId }); + }; - deleteTest(`in another space (space_2)`, { - spaceId: SPACES.SPACE_1.spaceId, - otherSpaceId: SPACES.SPACE_2.spaceId, - tests: { - spaceAware: { - statusCode: 404, - response: createExpectSpaceAwareNotFound(SPACES.SPACE_2.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 404, - response: expectGenericNotFound, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(SPACES.SPACE_2.spaceId), - }, - }, + describe('_delete', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/export.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/export.ts index f4be02c05ac88..25d4fbfae990b 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/export.ts @@ -4,62 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SPACES } from '../../common/lib/spaces'; +import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { exportTestSuiteFactory } from '../../common/suites/export'; +import { exportTestSuiteFactory, getTestCases } from '../../common/suites/export'; + +const createTestCases = (spaceId: string) => { + const cases = getTestCases(spaceId); + return Object.values(cases); +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const { - expectTypeOrObjectsRequired, - createExpectVisualizationResults, - expectInvalidTypeSpecified, - exportTest, - } = exportTestSuiteFactory(esArchiver, supertest); - - describe('export', () => { - exportTest('objects only within the current space (space_1)', { - ...SPACES.SPACE_1, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(SPACES.SPACE_1.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); + const { addTests, createTestDefinitions } = exportTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; - exportTest('objects only within the current space (default)', { - ...SPACES.DEFAULT, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(SPACES.DEFAULT.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, + describe('_export', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts index a07d3edf834e9..a15f7de404db8 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts @@ -4,154 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SPACES } from '../../common/lib/spaces'; +import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { findTestSuiteFactory } from '../../common/suites/find'; +import { findTestSuiteFactory, getTestCases } from '../../common/suites/find'; + +const createTestCases = (spaceId: string) => { + const cases = getTestCases(spaceId); + return Object.values(cases); +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const { - createExpectEmpty, - createExpectVisualizationResults, - expectFilterWrongTypeError, - expectNotSpaceAwareResults, - expectTypeRequired, - findTest, - } = findTestSuiteFactory(esArchiver, supertest); - - describe('find', () => { - findTest(`objects only within the current space (space_1)`, { - ...SPACES.SPACE_1, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(SPACES.SPACE_1.spaceId), - }, - notSpaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithUnknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); + const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; - findTest(`objects only within the current space (default)`, { - ...SPACES.DEFAULT, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(SPACES.DEFAULT.spaceId), - }, - notSpaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithUnknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, + describe('_find', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/get.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/get.ts index 592bedd92b4d7..512ae968dd0dd 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/get.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/get.ts @@ -5,88 +5,48 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { getTestSuiteFactory } from '../../common/suites/get'; +import { getTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/get'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.HIDDEN, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const { - createExpectDoesntExistNotFound, - createExpectSpaceAwareNotFound, - createExpectSpaceAwareResults, - createExpectNotSpaceAwareResults, - expectHiddenTypeNotFound: expectHiddenTypeNotFound, - getTest, - } = getTestSuiteFactory(esArchiver, supertest); - - describe('get', () => { - getTest(`can access objects belonging to the current space (default)`, { - ...SPACES.DEFAULT, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(SPACES.DEFAULT.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(SPACES.DEFAULT.spaceId), - }, - hiddenType: { - statusCode: 404, - response: expectHiddenTypeNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(SPACES.DEFAULT.spaceId), - }, - }, - }); - - getTest(`can access objects belonging to the current space (space_1)`, { - ...SPACES.SPACE_1, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(SPACES.SPACE_1.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(SPACES.SPACE_1.spaceId), - }, - hiddenType: { - statusCode: 404, - response: expectHiddenTypeNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(SPACES.SPACE_1.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = getTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false, { spaceId }); + }; - getTest(`can't access space aware objects belonging to another space (space_1)`, { - spaceId: SPACES.DEFAULT.spaceId, - otherSpaceId: SPACES.SPACE_1.spaceId, - tests: { - spaceAware: { - statusCode: 404, - response: createExpectSpaceAwareNotFound(SPACES.SPACE_1.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(SPACES.SPACE_1.spaceId), - }, - hiddenType: { - statusCode: 404, - response: expectHiddenTypeNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(SPACES.SPACE_1.spaceId), - }, - }, + describe('_get', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts index c78a0e1cc2cce..5fe4b08d91b54 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts @@ -5,56 +5,48 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { importTestSuiteFactory } from '../../common/suites/import'; +import { importTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/import'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(spaceId === DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(spaceId === SPACE_2_ID) }, + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() }, + { ...CASES.HIDDEN, ...fail400() }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - importTest, - createExpectResults, - expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, - } = importTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions } = importTestSuiteFactory(es, esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false, { spaceId, singleRequest: true }); + }; describe('_import', () => { - importTest('in the current space (space_1)', { - ...SPACES.SPACE_1, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.SPACE_1.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest('in the default space', { - ...SPACES.DEFAULT, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.DEFAULT.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts index bb481e0c98bc1..c2f8339d38c97 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts @@ -12,6 +12,7 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./bulk_create')); loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./bulk_update')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./export')); @@ -20,6 +21,5 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./import')); loadTestFile(require.resolve('./resolve_import_errors')); loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./bulk_update')); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts index 22a7ab81e5530..04f9ac8414afd 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts @@ -5,56 +5,59 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { resolveImportErrorsTestSuiteFactory } from '../../common/suites/resolve_import_errors'; +import { + resolveImportErrorsTestSuiteFactory, + TEST_CASES as CASES, +} from '../../common/suites/resolve_import_errors'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean, spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - resolveImportErrorsTest, - createExpectResults, - expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, - } = resolveImportErrorsTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions } = resolveImportErrorsTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = (overwrite: boolean, spaceId: string) => { + const testCases = createTestCases(overwrite, spaceId); + return createTestDefinitions(testCases, false, overwrite, { spaceId, singleRequest: true }); + }; describe('_resolve_import_errors', () => { - resolveImportErrorsTest('in the current space (space_1)', { - ...SPACES.SPACE_1, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.SPACE_1.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest('in the default space', { - ...SPACES.DEFAULT, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.DEFAULT.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, + getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const tests = createTests(overwrite!, spaceId); + addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/update.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/update.ts index 6ffbda511d871..381861e33b68d 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/update.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/update.ts @@ -5,88 +5,48 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { updateTestSuiteFactory } from '../../common/suites/update'; +import { updateTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/update'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.HIDDEN, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('update', () => { - const { - createExpectSpaceAwareNotFound, - expectSpaceAwareResults, - createExpectDoesntExistNotFound, - expectNotSpaceAwareResults, - expectSpaceNotFound, - updateTest, - } = updateTestSuiteFactory(esArchiver, supertest); - - updateTest(`in the default space`, { - spaceId: SPACES.DEFAULT.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 404, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(SPACES.DEFAULT.spaceId), - }, - }, - }); - - updateTest('in the current space (space_1)', { - spaceId: SPACES.SPACE_1.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 404, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(SPACES.SPACE_1.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = updateTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; - updateTest('objects that exist in another space (space_1)', { - spaceId: SPACES.DEFAULT.spaceId, - otherSpaceId: SPACES.SPACE_1.spaceId, - tests: { - spaceAware: { - statusCode: 404, - response: createExpectSpaceAwareNotFound(SPACES.SPACE_1.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 404, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, + describe('_update', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/spaces_api_integration/common/config.ts b/x-pack/test/spaces_api_integration/common/config.ts index dffc6c524cc6e..19743e09f9420 100644 --- a/x-pack/test/spaces_api_integration/common/config.ts +++ b/x-pack/test/spaces_api_integration/common/config.ts @@ -67,6 +67,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) // disable anonymouse access so that we're testing both on and off in different suites '--status.allowAnonymous=false', '--server.xsrf.disableProtection=true', + `--plugin-path=${path.join(__dirname, 'fixtures', 'shared_type_plugin')}`, ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), ], }, diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 64c1be0b90071..9a8a0a1fdda14 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -376,3 +376,123 @@ "type": "_doc" } } + +{ + "type": "doc", + "value": { + "id": "sharedtype:default_space_only", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the default space" + }, + "type": "sharedtype", + "namespaces": ["default"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:space_1_only", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the space_1 space" + }, + "type": "sharedtype", + "namespaces": ["space_1"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:space_2_only", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the space_2 space" + }, + "type": "sharedtype", + "namespaces": ["space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:default_and_space_1", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the default and space_1 spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:default_and_space_2", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the default and space_2 spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:space_1_and_space_2", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the space_1 and space_2 spaces" + }, + "type": "sharedtype", + "namespaces": ["space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:all_spaces", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the default, space_1, and space_2 spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index 1440585b625b9..508de68c32f70 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -159,6 +159,9 @@ "namespace": { "type": "keyword" }, + "namespaces": { + "type": "keyword" + }, "search": { "properties": { "columns": { @@ -320,6 +323,19 @@ "type": "text" } } + }, + "sharedtype": { + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } } } }, diff --git a/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/index.js b/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/index.js new file mode 100644 index 0000000000000..91a24fb9f4f56 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/index.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import mappings from './mappings.json'; + +export default function(kibana) { + return new kibana.Plugin({ + require: ['kibana', 'elasticsearch', 'xpack_main'], + name: 'shared_type_plugin', + uiExports: { + savedObjectsManagement: {}, + savedObjectSchemas: { + sharedtype: { + multiNamespace: true, + }, + }, + mappings, + }, + + config() {}, + + init() {}, // need empty init for plugin to load + }); +} diff --git a/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/mappings.json b/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/mappings.json new file mode 100644 index 0000000000000..918958aec0d6d --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/mappings.json @@ -0,0 +1,15 @@ +{ + "sharedtype": { + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + } +} diff --git a/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/package.json b/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/package.json new file mode 100644 index 0000000000000..c52f4256c5c06 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/package.json @@ -0,0 +1,7 @@ +{ + "name": "shared_type_plugin", + "version": "0.0.0", + "kibana": { + "version": "kibana" + } +} diff --git a/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts b/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts new file mode 100644 index 0000000000000..67f5d737ba010 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES = Object.freeze({ + DEFAULT_SPACE_ONLY: Object.freeze({ + id: 'default_space_only', + existingNamespaces: ['default'], + }), + SPACE_1_ONLY: Object.freeze({ + id: 'space_1_only', + existingNamespaces: ['space_1'], + }), + SPACE_2_ONLY: Object.freeze({ + id: 'space_2_only', + existingNamespaces: ['space_2'], + }), + DEFAULT_AND_SPACE_1: Object.freeze({ + id: 'default_and_space_1', + existingNamespaces: ['default', 'space_1'], + }), + DEFAULT_AND_SPACE_2: Object.freeze({ + id: 'default_and_space_2', + existingNamespaces: ['default', 'space_2'], + }), + SPACE_1_AND_SPACE_2: Object.freeze({ + id: 'space_1_and_space_2', + existingNamespaces: ['space_1', 'space_2'], + }), + ALL_SPACES: Object.freeze({ + id: 'all_spaces', + existingNamespaces: ['default', 'space_1', 'space_2'], + }), + DOES_NOT_EXIST: Object.freeze({ + id: 'does_not_exist', + existingNamespaces: [] as string[], + }), +}); diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index 9036fcbf7a8dd..0d8728fdf622e 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import { getUrlPrefix } from '../lib/space_test_utils'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface DeleteTest { @@ -128,6 +129,25 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe ]; expect(buckets).to.eql(expectedBuckets); + + // There were seven multi-namespace objects. + // Since Space 2 was deleted, any multi-namespace objects that existed in that space + // are updated to remove it, and of those, any that don't exist in any space are deleted. + const multiNamespaceResponse = await es.search({ + index: '.kibana', + body: { query: { terms: { type: ['sharedtype'] } } }, + }); + const docs: [Record] = multiNamespaceResponse.hits.hits; + expect(docs).length(6); // just six results, since spaces_2_only got deleted + Object.values(CASES).forEach(({ id, existingNamespaces }) => { + const remainingNamespaces = existingNamespaces.filter(x => x !== 'space_2'); + const doc = docs.find(x => x._id === `sharedtype:${id}`); + if (remainingNamespaces.length > 0) { + expect(doc?._source?.namespaces).to.eql(remainingNamespaces); + } else { + expect(doc).to.be(undefined); + } + }); }; const expectNotFound = (resp: { [key: string]: any }) => { diff --git a/x-pack/test/spaces_api_integration/common/suites/share_add.ts b/x-pack/test/spaces_api_integration/common/suites/share_add.ts new file mode 100644 index 0000000000000..b9a012b606da3 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/share_add.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { SuperTest } from 'supertest'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { SPACES } from '../lib/spaces'; +import { + expectResponses, + getUrlPrefix, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { + ExpectResponseBody, + TestDefinition, + TestSuite, +} from '../../../saved_object_api_integration/common/lib/types'; + +export interface ShareAddTestDefinition extends TestDefinition { + request: { spaces: string[]; object: { type: string; id: string } }; +} +export type ShareAddTestSuite = TestSuite; +export interface ShareAddTestCase { + id: string; + namespaces: string[]; + failure?: 400 | 403 | 404; + fail400Param?: string; + fail403Param?: string; +} + +const TYPE = 'sharedtype'; +const createRequest = ({ id, namespaces }: ShareAddTestCase) => ({ + spaces: namespaces, + object: { type: TYPE, id }, +}); +const getTestTitle = ({ id, namespaces }: ShareAddTestCase) => + `{id: ${id}, namespaces: [${namespaces.join(',')}]}`; + +export function shareAddTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectResponseBody = (testCase: ShareAddTestCase): ExpectResponseBody => async ( + response: Record + ) => { + const { id, failure, fail400Param, fail403Param } = testCase; + const object = response.body; + if (failure === 403) { + await expectResponses.forbidden(fail403Param!)(TYPE)(response); + } else if (failure) { + let error: any; + if (failure === 400) { + error = SavedObjectsErrorHelpers.createBadRequestError( + `${id} already exists in the following namespace(s): ${fail400Param}` + ); + } else if (failure === 404) { + error = SavedObjectsErrorHelpers.createGenericNotFoundError(TYPE, id); + } + expect(object.error).to.eql(error.output.payload.error); + expect(object.statusCode).to.eql(error.output.payload.statusCode); + } else { + // success + expect(object).to.eql({}); + } + }; + const createTestDefinitions = ( + testCases: ShareAddTestCase | ShareAddTestCase[], + forbidden: boolean, + options?: { + responseBodyOverride?: ExpectResponseBody; + fail403Param?: string; + } + ): ShareAddTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403, fail403Param: options?.fail403Param })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 204, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); + }; + + const makeShareAddTest = (describeFn: Mocha.SuiteFunction) => ( + description: string, + definition: ShareAddTestSuite + ) => { + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request; + await supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_share_saved_object_add`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } + }); + }; + + const addTests = makeShareAddTest(describe); + // @ts-ignore + addTests.only = makeShareAddTest(describe.only); + + return { + addTests, + createTestDefinitions, + }; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/share_remove.ts b/x-pack/test/spaces_api_integration/common/suites/share_remove.ts new file mode 100644 index 0000000000000..b5fcbe5a1cf2c --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/share_remove.ts @@ -0,0 +1,116 @@ +/* + * 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 expect from '@kbn/expect'; +import { SuperTest } from 'supertest'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { SPACES } from '../lib/spaces'; +import { + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { + ExpectResponseBody, + TestDefinition, + TestSuite, +} from '../../../saved_object_api_integration/common/lib/types'; + +export interface ShareRemoveTestDefinition extends TestDefinition { + request: { spaces: string[]; object: { type: string; id: string } }; +} +export type ShareRemoveTestSuite = TestSuite; +export interface ShareRemoveTestCase { + id: string; + namespaces: string[]; + failure?: 400 | 403 | 404; + fail400Param?: string; +} + +const TYPE = 'sharedtype'; +const createRequest = ({ id, namespaces }: ShareRemoveTestCase) => ({ + spaces: namespaces, + object: { type: TYPE, id }, +}); + +export function shareRemoveTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectForbidden = expectResponses.forbidden('delete'); + const expectResponseBody = (testCase: ShareRemoveTestCase): ExpectResponseBody => async ( + response: Record + ) => { + const { id, failure, fail400Param } = testCase; + const object = response.body; + if (failure === 403) { + await expectForbidden(TYPE)(response); + } else if (failure) { + let error: any; + if (failure === 400) { + error = SavedObjectsErrorHelpers.createBadRequestError( + `${id} doesn't exist in the following namespace(s): ${fail400Param}` + ); + } else if (failure === 404) { + error = SavedObjectsErrorHelpers.createGenericNotFoundError(TYPE, id); + } + expect(object.error).to.eql(error.output.payload.error); + expect(object.statusCode).to.eql(error.output.payload.statusCode); + } else { + // success + expect(object).to.eql({}); + } + }; + const createTestDefinitions = ( + testCases: ShareRemoveTestCase | ShareRemoveTestCase[], + forbidden: boolean, + options?: { + responseBodyOverride?: ExpectResponseBody; + } + ): ShareRemoveTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle({ ...x, type: TYPE }), + responseStatusCode: x.failure ?? 204, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); + }; + + const makeShareRemoveTest = (describeFn: Mocha.SuiteFunction) => ( + description: string, + definition: ShareRemoveTestSuite + ) => { + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request; + await supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_share_saved_object_remove`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } + }); + }; + + const addTests = makeShareRemoveTest(describe); + // @ts-ignore + addTests.only = makeShareRemoveTest(describe.only); + + return { + addTests, + createTestDefinitions, + }; +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts index e918ab0b53841..8d85d95e6812f 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts @@ -25,6 +25,8 @@ export default function({ loadTestFile, getService }: TestInvoker) { loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./share_add')); + loadTestFile(require.resolve('./share_remove')); loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts new file mode 100644 index 0000000000000..c7e65ac424776 --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts @@ -0,0 +1,124 @@ +/* + * 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 { SPACES } from '../../common/lib/spaces'; +import { + testCaseFailures, + getTestScenarios, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { TestInvoker } from '../../common/lib/types'; +import { shareAddTestSuiteFactory, ShareAddTestDefinition } from '../../common/suites/share_add'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + const namespaces = [spaceId]; + return [ + // Test cases to check adding the target namespace to different saved objects + { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_2, namespaces, ...fail404(spaceId === SPACE_1_ID) }, + { ...CASES.SPACE_1_AND_SPACE_2, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + { ...CASES.ALL_SPACES, namespaces }, + { ...CASES.DOES_NOT_EXIST, namespaces, ...fail404() }, + // Test cases to check adding multiple namespaces to different saved objects that exist in one space + // These are non-exhaustive, they only check cases for adding two additional namespaces to a saved object + // More permutations are covered in the corresponding spaces_only test suite + { + ...CASES.DEFAULT_SPACE_ONLY, + namespaces: [SPACE_1_ID, SPACE_2_ID], + ...fail404(spaceId !== DEFAULT_SPACE_ID), + }, + { + ...CASES.SPACE_1_ONLY, + namespaces: [DEFAULT_SPACE_ID, SPACE_2_ID], + ...fail404(spaceId !== SPACE_1_ID), + }, + { + ...CASES.SPACE_2_ONLY, + namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID], + ...fail404(spaceId !== SPACE_2_ID), + }, + ]; +}; +const calculateSingleSpaceAuthZ = ( + testCases: ReturnType, + spaceId: string +) => { + const targetsOtherSpace = testCases.filter( + x => !x.namespaces.includes(spaceId) || x.namespaces.length > 1 + ); + const tmp = testCases.filter(x => !targetsOtherSpace.includes(x)); // doesn't target other space + const doesntExistInThisSpace = tmp.filter(x => !x.existingNamespaces.includes(spaceId)); + const existsInThisSpace = tmp.filter(x => x.existingNamespaces.includes(spaceId)); + return { targetsOtherSpace, doesntExistInThisSpace, existsInThisSpace }; +}; +// eslint-disable-next-line import/no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = shareAddTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + const thisSpace = calculateSingleSpaceAuthZ(testCases, spaceId); + const otherSpaceId = spaceId === DEFAULT_SPACE_ID ? SPACE_1_ID : DEFAULT_SPACE_ID; + const otherSpace = calculateSingleSpaceAuthZ(testCases, otherSpaceId); + return { + unauthorized: createTestDefinitions(testCases, true, { fail403Param: 'create' }), + authorizedInSpace: [ + createTestDefinitions(thisSpace.targetsOtherSpace, true, { fail403Param: 'create' }), + createTestDefinitions(thisSpace.doesntExistInThisSpace, false), + createTestDefinitions(thisSpace.existsInThisSpace, false), + ].flat(), + authorizedInOtherSpace: [ + createTestDefinitions(otherSpace.targetsOtherSpace, true, { fail403Param: 'create' }), + // If the preflight GET request fails, it will return a 404 error; users who are authorized to create saved objects in the target + // space(s) but are not authorized to update saved objects in this space will see a 403 error instead of 404. This is a safeguard to + // prevent potential information disclosure of the spaces that a given saved object may exist in. + createTestDefinitions(otherSpace.doesntExistInThisSpace, true, { fail403Param: 'update' }), + createTestDefinitions(otherSpace.existsInThisSpace, false), + ].flat(), + authorized: createTestDefinitions(testCases, false), + }; + }; + + describe('_share_saved_object_add', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` targeting the ${spaceId} space`; + const { unauthorized, authorizedInSpace, authorizedInOtherSpace, authorized } = createTests( + spaceId + ); + const _addTests = (user: TestUser, tests: ShareAddTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; + + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + ].forEach(user => { + _addTests(user, unauthorized); + }); + _addTests(users.allAtSpace, authorizedInSpace); + _addTests(users.allAtOtherSpace, authorizedInOtherSpace); + [users.dualAll, users.allGlobally, users.superuser].forEach(user => { + _addTests(user, authorized); + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts new file mode 100644 index 0000000000000..3a8d42f620a3e --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts @@ -0,0 +1,99 @@ +/* + * 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 { SPACES } from '../../common/lib/spaces'; +import { + testCaseFailures, + getTestScenarios, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { TestInvoker } from '../../common/lib/types'; +import { + shareRemoveTestSuiteFactory, + ShareRemoveTestCase, + ShareRemoveTestDefinition, +} from '../../common/suites/share_remove'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // Test cases to check removing the target namespace from different saved objects + let namespaces = [spaceId]; + const singleSpace = [ + { id: CASES.DEFAULT_SPACE_ONLY.id, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { id: CASES.SPACE_1_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, + { id: CASES.SPACE_2_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, + { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces, ...fail404(spaceId === SPACE_2_ID) }, + { id: CASES.DEFAULT_AND_SPACE_2.id, namespaces, ...fail404(spaceId === SPACE_1_ID) }, + { id: CASES.SPACE_1_AND_SPACE_2.id, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + { id: CASES.ALL_SPACES.id, namespaces }, + { id: CASES.DOES_NOT_EXIST.id, namespaces, ...fail404() }, + ] as ShareRemoveTestCase[]; + + // Test cases to check removing all three namespaces from different saved objects that exist in two spaces + // These are non-exhaustive, they only check some cases -- each object will result in a 404, either because + // it never existed in the target namespace, or it was removed in one of the test cases above + // More permutations are covered in the corresponding spaces_only test suite + namespaces = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + const multipleSpaces = [ + { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces, ...fail404() }, + { id: CASES.DEFAULT_AND_SPACE_2.id, namespaces, ...fail404() }, + { id: CASES.SPACE_1_AND_SPACE_2.id, namespaces, ...fail404() }, + ] as ShareRemoveTestCase[]; + + const allCases = singleSpace.concat(multipleSpaces); + return { singleSpace, multipleSpaces, allCases }; +}; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = shareRemoveTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { singleSpace, multipleSpaces, allCases } = createTestCases(spaceId); + return { + unauthorized: createTestDefinitions(allCases, true), + authorizedThisSpace: [ + createTestDefinitions(singleSpace, false), + createTestDefinitions(multipleSpaces, true), + ].flat(), + authorizedGlobally: createTestDefinitions(allCases, false), + }; + }; + + describe('_share_saved_object_remove', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` targeting the ${spaceId} space`; + const { unauthorized, authorizedThisSpace, authorizedGlobally } = createTests(spaceId); + const _addTests = (user: TestUser, tests: ShareRemoveTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; + + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); + }); + _addTests(users.allAtSpace, authorizedThisSpace); + [users.dualAll, users.allGlobally, users.superuser].forEach(user => { + _addTests(user, authorizedGlobally); + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts index 1182f6bdabcff..b02dc35b58b56 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts @@ -17,6 +17,8 @@ export default function spacesOnlyTestSuite({ loadTestFile }: TestInvoker) { loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./share_add')); + loadTestFile(require.resolve('./share_remove')); loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts new file mode 100644 index 0000000000000..f1e603836fa21 --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SPACES } from '../../common/lib/spaces'; +import { + testCaseFailures, + getTestScenarios, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { TestInvoker } from '../../common/lib/types'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { shareAddTestSuiteFactory } from '../../common/suites/share_add'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +/** + * Single-namespace test cases + * @param spaceId the namespace to add to each saved object + */ +const createSingleTestCases = (spaceId: string) => { + const namespaces = ['some-space-id']; + return [ + { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_2, namespaces, ...fail404(spaceId === SPACE_1_ID) }, + { ...CASES.SPACE_1_AND_SPACE_2, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + { ...CASES.ALL_SPACES, namespaces }, + { ...CASES.DOES_NOT_EXIST, namespaces, ...fail404() }, + ]; +}; +/** + * Multi-namespace test cases + * These are non-exhaustive, but they check different permutations of saved objects and spaces to add + */ +const createMultiTestCases = () => { + const allSpaces = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + let id = CASES.DEFAULT_SPACE_ONLY.id; + const one = [{ id, namespaces: allSpaces }]; + id = CASES.DEFAULT_AND_SPACE_1.id; + const two = [{ id, namespaces: allSpaces }]; + id = CASES.ALL_SPACES.id; + const three = [{ id, namespaces: allSpaces }]; + return { one, two, three }; +}; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = shareAddTestSuiteFactory(esArchiver, supertest); + const createSingleTests = (spaceId: string) => { + const testCases = createSingleTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; + const createMultiTests = () => { + const testCases = createMultiTestCases(); + return { + one: createTestDefinitions(testCases.one, false), + two: createTestDefinitions(testCases.two, false), + three: createTestDefinitions(testCases.three, false), + }; + }; + + describe('_share_saved_object_add', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createSingleTests(spaceId); + addTests(`targeting the ${spaceId} space`, { spaceId, tests }); + }); + const { one, two, three } = createMultiTests(); + addTests('for a saved object in the default space', { tests: one }); + addTests('for a saved object in the default and space_1 spaces', { tests: two }); + addTests('for a saved object in the default, space_1, and space_2 spaces', { tests: three }); + }); +} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts new file mode 100644 index 0000000000000..15be72c9f09ac --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts @@ -0,0 +1,99 @@ +/* + * 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 { SPACES } from '../../common/lib/spaces'; +import { + testCaseFailures, + getTestScenarios, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { TestInvoker } from '../../common/lib/types'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { shareRemoveTestSuiteFactory } from '../../common/suites/share_remove'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +/** + * Single-namespace test cases + * @param spaceId the namespace to remove from each saved object + */ +const createSingleTestCases = (spaceId: string) => { + const namespaces = [spaceId]; + return [ + { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_2, namespaces, ...fail404(spaceId === SPACE_1_ID) }, + { ...CASES.SPACE_1_AND_SPACE_2, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + { ...CASES.ALL_SPACES, namespaces }, + { ...CASES.DOES_NOT_EXIST, namespaces, ...fail404() }, + ]; +}; +/** + * Multi-namespace test cases + * These are non-exhaustive, but they check different permutations of saved objects and spaces to remove + */ +const createMultiTestCases = () => { + const nonExistentSpaceId = 'does_not_exist'; // space that doesn't exist + let id = CASES.DEFAULT_SPACE_ONLY.id; + const one = [ + { id, namespaces: [nonExistentSpaceId] }, + { id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID] }, + { id, namespaces: [DEFAULT_SPACE_ID], ...fail404() }, // this saved object no longer exists + ]; + id = CASES.DEFAULT_AND_SPACE_1.id; + const two = [ + { id, namespaces: [DEFAULT_SPACE_ID, nonExistentSpaceId] }, + // this saved object will not be found in the context of the current namespace ('default') + { id, namespaces: [DEFAULT_SPACE_ID], ...fail404() }, // this object's namespaces no longer contains DEFAULT_SPACE_ID + { id, namespaces: [SPACE_1_ID], ...fail404() }, // this object's namespaces does contain SPACE_1_ID + ]; + id = CASES.ALL_SPACES.id; + const three = [ + { id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, nonExistentSpaceId] }, + // this saved object will not be found in the context of the current namespace ('default') + { id, namespaces: [DEFAULT_SPACE_ID], ...fail404() }, // this object's namespaces no longer contains DEFAULT_SPACE_ID + { id, namespaces: [SPACE_1_ID], ...fail404() }, // this object's namespaces no longer contains SPACE_1_ID + { id, namespaces: [SPACE_2_ID], ...fail404() }, // this object's namespaces does contain SPACE_2_ID + ]; + return { one, two, three }; +}; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = shareRemoveTestSuiteFactory(esArchiver, supertest); + const createSingleTests = (spaceId: string) => { + const testCases = createSingleTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; + const createMultiTests = () => { + const testCases = createMultiTestCases(); + return { + one: createTestDefinitions(testCases.one, false), + two: createTestDefinitions(testCases.two, false), + three: createTestDefinitions(testCases.three, false), + }; + }; + + describe('_share_saved_object_remove', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createSingleTests(spaceId); + addTests(`targeting the ${spaceId} space`, { spaceId, tests }); + }); + const { one, two, three } = createMultiTests(); + addTests('for a saved object in the default space', { tests: one }); + addTests('for a saved object in the default and space_1 spaces', { tests: two }); + addTests('for a saved object in the default, space_1, and space_2 spaces', { tests: three }); + }); +} From 83b9417d4534f7c8d2c6daa9009bd20e1937cf16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 10 Apr 2020 08:28:06 +0100 Subject: [PATCH 79/81] [APM] Custom links submit button is off screen in IE11 (#63122) --- .../CustomLinkFlyout/DeleteButton.tsx | 2 + .../CustomLinkFlyout/FlyoutFooter.tsx | 38 ++++++++----------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx index 2b3a5cbe87992..87cb171518ea4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx @@ -8,6 +8,7 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; import React, { useState } from 'react'; +import { px, unit } from '../../../../../../style/variables'; import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; @@ -31,6 +32,7 @@ export function DeleteButton({ onDelete, customLinkId }: Props) { setIsDeleting(false); onDelete(); }} + style={{ marginRight: px(unit) }} > {i18n.translate('xpack.apm.settings.customizeUI.customLink.delete', { defaultMessage: 'Delete' diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx index cb27221309812..96505d639bcdd 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx @@ -40,29 +40,23 @@ export const FlyoutFooter = ({ )} - - - {customLinkId && ( - - - + + {customLinkId && ( + + )} + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.save', + { + defaultMessage: 'Save' + } )} - - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.flyout.save', - { - defaultMessage: 'Save' - } - )} - - - + From 34b1d0a10d529da48f35106d6b82cf96498e6677 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Fri, 10 Apr 2020 11:28:59 +0200 Subject: [PATCH 80/81] [SIEM] Updates cypress readme with documentation about the test data. (#62747) * updates test data section * Update x-pack/legacy/plugins/siem/cypress/README.md Co-Authored-By: Ryland Herrick Co-authored-by: Ryland Herrick --- x-pack/legacy/plugins/siem/cypress/README.md | 132 +++++++++++++++---- 1 file changed, 107 insertions(+), 25 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/README.md b/x-pack/legacy/plugins/siem/cypress/README.md index 41137ce6d8a9d..a031fea172be5 100644 --- a/x-pack/legacy/plugins/siem/cypress/README.md +++ b/x-pack/legacy/plugins/siem/cypress/README.md @@ -111,6 +111,112 @@ elasticsearch: hosts: ['https://:9200'] ``` +## Running (Headless) Tests on the Command Line as a Jenkins execution (The preferred way) + +To run (headless) tests as a Jenkins execution. + +1. First bootstrap kibana changes from the Kibana root directory: + +```sh +yarn kbn bootstrap +``` + +2. Launch Cypress command line test runner: + +```sh +cd x-pack/legacy/plugins/siem +yarn cypress:run-as-ci +``` + +Note that with this type of execution you don't need to have running a kibana and elasticsearch instance. This is because + the command, as it would happen in the CI, will launch the instances. The elasticsearch instance will be fed with the data + placed in: `x-pack/test/siem_cypress/es_archives` + +As in this case we want to mimic a CI execution we want to execute the tests with the same set of data, this is why +in this case does not make sense to override Cypress environment variables. + +### Test data + +As said before when running the tests as Jenkins the tests are fed with the data placed in: `x-pack/test/siem_cypress/es_archives`. + +Currently there are two different ways of feeding data: +1. By default +2. Specifying a specific set of data for a specific test + +#### By default + +When a execution of the test is going to be done an empty kibana and a set of audibteat data are loaded (empty_kibana and auditbeat). With this data usually is enough to cover most of the scenarios that we are testing. + +#### Running tests with custom data + +Sometimes the default data is not enough and we need a specific set of data in order to being able to test the desired behaviour. + +In that case in the hooks of the test use the function `esArchiverLoad` to load the set of data neeed and `esArchiverUnload` to remove the changes done in the data. + +Example: + +```typescript +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; + +describe('This are going to be a set of tests', () => { + before(() => { + esArchiverLoad('name_of_the_data_set_you_want_to_load'); + }); + + after(() => { + esArchiverUnload('name_of_the_data_set_you_want_to_unload'); + }); + + it('Going to test something awesome', () => { + hereGoesYourAwesomeTestcode + }); +}); + +``` + +Note that loading and unloading data takes a signifcant amount of time so try to minimize the use of it when possible. + +### Current sets of data + +The current sets of data can be found in: `x-pack/test/siem_cypress/es_archives` folder. + +- auditbeat + - Auditbeat data generated in Sep, 2019 with the following hosts present: + - suricata-iowa + - siem-kibana + - siem-es + - jessie +- closed_signals + - Set of data with 108 closed signals linked to "Signals test" custom rule. +- custome_rules + - Set if data with just 4 custom activated rules. +- empty_kibana + - Empty kibana board. +- prebuilt_rules_loaded + - Elastic prebuilt loaded rules and deactivated. +- signals + - Set of data with 108 opened signals linked to "Signals test" custom rule. + +### How to generate new test data + +We are using es_archiver in order to generate the data that our Cypress tests needs. + +1. Setup if possible a clean instance of kibana and elasticsearch (if not, possible please try to clean the data that you are going to generate). +2. With the kibana and elasticsearch instance up and running, create the data that you need for your test. +3. When you are sure that you have all the data you need run the following command from: `x-pack/legacy/plugins/siem` + +```sh +node ../../../../scripts/es_archiver save --dir ../../../test/siem_cypress/es_archives --config ../../../../test/functional/config.js --es-url http://:@: +``` + +Example: +```sh +node ../../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../../test/siem_cypress/es_archives --config ../../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220 +``` + +Note that the command is going to create the folder if does not exist in the directory with the imported data. + + ## Running Tests Interactively Use the Cypress interactive test runner to develop and debug specific tests @@ -210,30 +316,6 @@ cd x-pack/legacy/plugins/siem CYPRESS_baseUrl=http://localhost:5601 CYPRESS_ELASTICSEARCH_USERNAME=elastic CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:run ``` -## Running (Headless) Tests on the Command Line as a Jenkins execution - -To run (headless) tests as a Jenkins execution. - -1. First bootstrap kibana changes from the Kibana root directory: - -```sh -yarn kbn bootstrap -``` - -2. Launch Cypress command line test runner: - -```sh -cd x-pack/legacy/plugins/siem -yarn cypress:run-as-ci -``` - -Note that with this type of execution you don't need to have running a kibana and elasticsearch instance. This is because - the command, as it would happen in the CI, will launch the instances. The elasticsearch instance will be fed with the data - placed in: `x-pack/test/siem_cypress/es_archives`. - -As in this case we want to mimic a CI execution we want to execute the tests with the same set of data, this is why -in this case does not make sense to override Cypress environment variables. - ## Reporting When Cypress tests are run on the command line via `yarn cypress:run`, @@ -280,4 +362,4 @@ target/kibana-siem/cypress/videos ## Linting -Optional linting rules for Cypress and linting setup can be found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage) \ No newline at end of file +Optional linting rules for Cypress and linting setup can be found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage) From 93971dbfab74b39335115e8236741329486c4b79 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 10 Apr 2020 12:28:55 +0200 Subject: [PATCH 81/81] [Watcher] Preserve the watch active status after updates (#61999) * Preserve the watch active status after updates * Use route validation params to set isActive * Fix Jest test * Implement PR feedback - Make the isActive flag required on the save watch endpoint - Move the isActive state to base_watch and serialize from there. * Fix Jest tests Co-authored-by: Elastic Machine --- .../watch_create_json.test.ts | 3 ++ .../watch_create_threshold.test.tsx | 8 +++++ .../client_integration/watch_edit.test.ts | 2 ++ .../application/models/watch/base_watch.js | 2 ++ .../server/lib/elasticsearch_js_plugin.ts | 4 +++ .../routes/api/watch/register_save_route.ts | 29 ++++++++----------- 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts index 2096c0dd61bd8..47de4548898cf 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts @@ -108,6 +108,7 @@ describe(' create route', () => { name: watch.name, type: watch.type, isNew: true, + isActive: true, actions: [ { id: DEFAULT_LOGGING_ACTION_ID, @@ -185,6 +186,7 @@ describe(' create route', () => { id, type, isNew: true, + isActive: true, actions: [], watch: defaultWatchJson, }; @@ -246,6 +248,7 @@ describe(' create route', () => { id, type, isNew: true, + isActive: true, actions: [], watch: defaultWatchJson, }; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx index 943233d3c14ed..e5bdcbbfb82cf 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx @@ -244,6 +244,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'logging_1', @@ -314,6 +315,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'index_1', @@ -376,6 +378,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'slack_1', @@ -448,6 +451,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'email_1', @@ -540,6 +544,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'webhook_1', @@ -629,6 +634,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'jira_1', @@ -709,6 +715,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'pagerduty_1', @@ -772,6 +779,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [], index: MATCH_INDICES, timeField: WATCH_TIME_FIELD, diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts index 51285a5786b00..ced06562b1d20 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts @@ -98,6 +98,7 @@ describe('', () => { name: EDITED_WATCH_NAME, type: watch.type, isNew: false, + isActive: true, actions: [ { id: DEFAULT_LOGGING_ACTION_ID, @@ -191,6 +192,7 @@ describe('', () => { name: EDITED_WATCH_NAME, type, isNew: false, + isActive: true, actions: [], timeField, triggerIntervalSize, diff --git a/x-pack/plugins/watcher/public/application/models/watch/base_watch.js b/x-pack/plugins/watcher/public/application/models/watch/base_watch.js index 3fe4fb006d241..af2e45b35501e 100644 --- a/x-pack/plugins/watcher/public/application/models/watch/base_watch.js +++ b/x-pack/plugins/watcher/public/application/models/watch/base_watch.js @@ -32,6 +32,7 @@ export class BaseWatch { this.isSystemWatch = Boolean(get(props, 'isSystemWatch')); this.watchStatus = WatchStatus.fromUpstreamJson(get(props, 'watchStatus')); this.watchErrors = WatchErrors.fromUpstreamJson(get(props, 'watchErrors')); + this.isActive = this.watchStatus.isActive ?? true; const actions = get(props, 'actions', []); this.actions = actions.map(Action.fromUpstreamJson); @@ -115,6 +116,7 @@ export class BaseWatch { name: this.name, type: this.type, isNew: this.isNew, + isActive: this.isActive, actions: map(this.actions, action => action.upstreamJson), }; } diff --git a/x-pack/plugins/watcher/server/lib/elasticsearch_js_plugin.ts b/x-pack/plugins/watcher/server/lib/elasticsearch_js_plugin.ts index 240e93e160fe0..3cb8dfb623fac 100644 --- a/x-pack/plugins/watcher/server/lib/elasticsearch_js_plugin.ts +++ b/x-pack/plugins/watcher/server/lib/elasticsearch_js_plugin.ts @@ -174,6 +174,10 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) name: 'master_timeout', type: 'duration', }, + active: { + name: 'active', + type: 'boolean', + }, }, url: { fmt: '/_watcher/watch/<%=id%>', diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts index 61d167bb9bbcd..10ee0c4857862 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts @@ -5,7 +5,6 @@ */ import { schema } from '@kbn/config-schema'; -import { IScopedClusterClient } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { WATCH_TYPES } from '../../../../common/constants'; import { serializeJsonWatch, serializeThresholdWatch } from '../../../../common/lib/serialization'; @@ -21,23 +20,11 @@ const bodySchema = schema.object( { type: schema.string(), isNew: schema.boolean(), + isActive: schema.boolean(), }, { unknowns: 'allow' } ); -function fetchWatch(dataClient: IScopedClusterClient, watchId: string) { - return dataClient.callAsCurrentUser('watcher.getWatch', { - id: watchId, - }); -} - -function saveWatch(dataClient: IScopedClusterClient, id: string, body: any) { - return dataClient.callAsCurrentUser('watcher.putWatch', { - id, - body, - }); -} - export function registerSaveRoute(deps: RouteDependencies) { deps.router.put( { @@ -49,12 +36,16 @@ export function registerSaveRoute(deps: RouteDependencies) { }, licensePreRoutingFactory(deps, async (ctx, request, response) => { const { id } = request.params; - const { type, isNew, ...watchConfig } = request.body; + const { type, isNew, isActive, ...watchConfig } = request.body; + + const dataClient = ctx.watcher!.client; // For new watches, verify watch with the same ID doesn't already exist if (isNew) { try { - const existingWatch = await fetchWatch(ctx.watcher!.client, id); + const existingWatch = await dataClient.callAsCurrentUser('watcher.getWatch', { + id, + }); if (existingWatch.found) { return response.conflict({ body: { @@ -92,7 +83,11 @@ export function registerSaveRoute(deps: RouteDependencies) { try { // Create new watch return response.ok({ - body: await saveWatch(ctx.watcher!.client, id, serializedWatch), + body: await dataClient.callAsCurrentUser('watcher.putWatch', { + id, + active: isActive, + body: serializedWatch, + }), }); } catch (e) { // Case: Error from Elasticsearch JS client

kCbAFrnrvi1y$?L2<5~xtW^1 zQ6b)p@`naH;H&fqaL%-Qp27`tO;^2`(vh5ZorS5p69?mgNB{3Vp}ZR z*n9{XBbycrgf`#c4}d|;k#%5oT7j+7v3Q`ARQTn0t%>F<($ZH8eB{3OS7bB(?OgXr zoI(0j;(D^%GAQpdE=9wOQ(rKn@0{h{p>yjeuaYm`5s8VuP_zB?ffT{uckbDuVlUas zj)w%rpollU#YUF-ifor>!#j;$Y2R}1k^T=&;is_+PXXzNaQD_v9&G~-@MzvU+v7>k z=xmcS&0ClLRKfknBSf9KpY`27>y8e=WDNCeIs9De)64E&fZqbGU)sxq?$ZI@TOY&j z^wpVVdkUj{JMM0{Rmbr16Izp%#g)vP>)0us%3FN450CRy+iMkj)z$e{U!J8%pZgXD z{w^hbCG>z#pu!9|EO@wt?ZI~s8V@XS>-tw}h7g|&Cg+c+x2~7B$}V2KXl);TWZTpgTQP_IP+0R5aO8HBhv*V5~N)%A>&}ARYbV1tGjuKr$pn^HlIsc z8>M3+vI$htt+ntU7=8fIh`zUj8A5d*zRKTgeW5CZ`RvuKn&5#`2V7vIba3Dc8WM zlV>*N^Z3<&>W&x#9>|f8?VS|uYl+abz;%7eXY1U=vYm#OLn1h?SgNy-(bv?8=oU1~ zoBU5mV2mN^DoNqS4w6H^6T3=PtIPkVj#ec=YvVs2$V>~JETYD!+Eu+7 zgd^v#Y-j-@y^Z%LbpRoT-Pye%6<@vrL#X^N+rU=;*kJo~(fT!86UySL?V%E|gqecX zhvFEnCr!V?bdFf4}*Oh;4XnZWe|t8}BhS8~%0D@aYx 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 Q1@I#&CMQ}btn2gt0O?oeDF6Tf literal 0 HcmV?d00001 diff --git a/docs/apm/using-the-apm-ui.asciidoc b/docs/apm/using-the-apm-ui.asciidoc index b1b7ed7307986..904718999069d 100644 --- a/docs/apm/using-the-apm-ui.asciidoc +++ b/docs/apm/using-the-apm-ui.asciidoc @@ -15,6 +15,7 @@ APM is available via the navigation sidebar in {Kib}. * <> * <> * <> +* <> * <> * <> * <> @@ -37,6 +38,8 @@ include::errors.asciidoc[] include::metrics.asciidoc[] +include::apm-alerts.asciidoc[] + include::agent-configuration.asciidoc[] include::custom-links.asciidoc[] From 13fe738b2a59c5117e15aca4af1c155e00129a37 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 9 Apr 2020 08:55:29 -0700 Subject: [PATCH 59/81] [UI COPY] Fixes typo in max_shingle_size for search_as_you_type (#63071) --- .../field_parameters/max_shingle_size_parameter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx index bc1917b2da966..cec97fb925eef 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx @@ -23,7 +23,7 @@ export const MaxShingleSizeParameter = ({ defaultToggleValue }: Props) => ( })} description={i18n.translate('xpack.idxMgmt.mappingsEditor.maxShingleSizeFieldDescription', { defaultMessage: - 'The default is three shingle subfields. More subfields enables more specific queries, but increases index size.', + 'The default is three shingle subfields. More subfields enable more specific queries, but increase index size.', })} defaultToggleValue={defaultToggleValue} > From 5e1c0be501b672174b2553dbca74d4eba16295f6 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 9 Apr 2020 11:56:03 -0400 Subject: [PATCH 60/81] [Endpoint] Add link to Logs UI to the Host Details view (#62852) * Add LinktoApp to host details for logs * initial setup for testing link on details * Export interface AppContextTestRender for reference in tests * Refactor hosts tests to use AppContextTestRender * Render full details and validate link to logs * one more test to ensure we navigate to app (not full page refresh) * Fixes post master merge --- .../endpoint/components/link_to_app.tsx | 2 +- .../endpoint/mocks/app_context_render.tsx | 2 +- .../store/hosts/mock_host_result_list.ts | 13 ++- .../endpoint/view/hosts/details.tsx | 28 +++++ .../endpoint/view/hosts/index.test.tsx | 106 +++++++++++++----- 5 files changed, 119 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx index b110d32442c2c..858dac864b58a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx @@ -12,7 +12,7 @@ import { useNavigateToAppEventHandler } from '../hooks/use_navigate_to_app_event export type LinkToAppProps = EuiLinkProps & { /** the app id - normally the value of the `id` in that plugin's `kibana.json` */ appId: string; - /** Any app specic path (route) */ + /** Any app specific path (route) */ appPath?: string; appState?: any; onClick?: MouseEventHandler; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx index af34205e2310f..7cb1031ef9a09 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx @@ -18,7 +18,7 @@ type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResul /** * Mocked app root context renderer */ -interface AppContextTestRender { +export interface AppContextTestRender { store: ReturnType; history: ReturnType; coreStart: ReturnType; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts index d4c2602e34387..20aa973ffc93d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostResultList, HostStatus } from '../../../../../common/types'; +import { HostInfo, HostResultList, HostStatus } from '../../../../../common/types'; import { EndpointDocGenerator } from '../../../../../common/generate_data'; export const mockHostResultList: (options?: { @@ -40,3 +40,14 @@ export const mockHostResultList: (options?: { }; return mock; }; + +/** + * returns a mocked API response for retrieving a single host metadata + */ +export const mockHostDetailsApiResult = (): HostInfo => { + const generator = new EndpointDocGenerator('seed'); + return { + metadata: generator.generateHostMetadata(), + host_status: HostStatus.ERROR, + }; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx index 37080e8568350..90829f7ad4cbe 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx @@ -28,6 +28,7 @@ import { useHostListSelector } from './hooks'; import { urlFromQueryParams } from './url_from_query_params'; import { FormattedDateAndTime } from '../formatted_date_time'; import { uiQueryParams, detailsData, detailsError } from './../../store/hosts/selectors'; +import { LinkToApp } from '../../components/link_to_app'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -37,6 +38,7 @@ const HostIds = styled(EuiListGroupItem)` `; const HostDetails = memo(({ details }: { details: HostMetadata }) => { + const { appId, appPath, url } = useHostLogsUrl(details.host.id); const detailsResultsUpper = useMemo(() => { return [ { @@ -113,6 +115,20 @@ const HostDetails = memo(({ details }: { details: HostMetadata }) => { listItems={detailsResultsLower} data-test-subj="hostDetailsLowerList" /> + +

&%3;cZTYaU}5&3LV*?qDac`_MRlIDB- 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