From 104b1607b7ac2dc86d711876d70631029f431392 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 15 Apr 2022 14:38:58 +0100 Subject: [PATCH 01/83] fix(NA): use correct rule on yarn_install force at @kbn/pm --- packages/kbn-pm/dist/index.js | 2 +- packages/kbn-pm/src/commands/bootstrap.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 46686c2d7b7910..691495e0a7922c 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -11871,7 +11871,7 @@ const BootstrapCommand = { await time('force install dependencies', async () => { await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_9__["removeYarnIntegrityFileIfExists"])(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(kibanaProjectPath, 'node_modules')); await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_9__["runBazel"])(['clean', '--expunge']); - await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_9__["runBazel"])(['run', '@yarn//:yarn'], runOffline, { + await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_9__["runBazel"])(['run', '@nodejs//:yarn'], runOffline, { env: { SASS_BINARY_SITE: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-sass', RE2_DOWNLOAD_MIRROR: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2' diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 2dd59d499b5854..a1dd4d38cfb8a8 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -72,7 +72,7 @@ export const BootstrapCommand: ICommand = { await time('force install dependencies', async () => { await removeYarnIntegrityFileIfExists(resolve(kibanaProjectPath, 'node_modules')); await runBazel(['clean', '--expunge']); - await runBazel(['run', '@yarn//:yarn'], runOffline, { + await runBazel(['run', '@nodejs//:yarn'], runOffline, { env: { SASS_BINARY_SITE: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-sass', From 683463ea43db9fc7ba2be43c56d2d059018e90c5 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Wed, 4 May 2022 13:22:18 -0600 Subject: [PATCH 02/83] [Security Solution][Detections] Rule Execution Log Feedback and Fixes Part Deux (#130072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Addresses feedback and fixes identified in https://github.com/elastic/kibana/pull/126215 & https://github.com/elastic/kibana/pull/129003 ##### Feedback addressed includes: * Adds toast for restoring global query state after performing `view alerts for execution` action

* Updates global SuperDatePicker to daterange of execution (+/- day) for `view alerts for execution` action (and clear all other filters) * See above gif * Remove redundant `RuleExecutionStatusType` (https://github.com/elastic/kibana/pull/129003#discussion_r842924704) * Persist table state (DatePicker/StatusFilter/SortField/SortOrder/Pagination) when navigating to other tabs on the same page

* Fix duration hours bug (`7 hours (25033167ms)` as `06:417:13:000`)

* Support `disabled rule` platform error (https://github.com/elastic/kibana/pull/126215#discussion_r834364979) * Updated `getAggregateExecutionEvents` to fallback to platform status from `event.outcome` if `security_status` is empty, and also falls back to `error.message` is `security_message` is empty. This also now queries for corresponding `event.outcome` if filter is provided so that platform-only events can still be displayed when filtering.

* Verify StatusFilter issue https://github.com/elastic/kibana/pull/126215#issuecomment-1080976155 * Unable to reproduce, I believe the query updates around first querying for status may've fixed this? * Provide helpful defaults for `to`/`from` and support datemath strings again (https://github.com/elastic/kibana/pull/129003#discussion_r843091926) * Created enhancement for this here: https://github.com/elastic/kibana/issues/131095 * Adds UI Unit tests for RuleExecutionLog Table * Finalize API Integration tests for gap remediation events * Test methods developed for injecting arbitrary execution events while still working with event-log RBAC. See last [API integration test](https://github.com/elastic/kibana/blob/22cc0c8dbd2a1300675caf4c6d471d211ed44858/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_rule_execution_events.ts#L121-L166) for technique. This can further be used to inject many execution events and expand tests around pagination, sorting, filters, etc. * Fixes `gap_duration`'s of `1-499`ms showing up as `-` instead of `0` * Fixes restore filters action to restore either absolute or relative datepicker as it originally was * Resolves https://github.com/elastic/kibana/issues/130946 * Adds `min-height` to tab container * Removes scroll-pane from ExceptionsViewer to match Alerts/Execution Log --- ##### Remaining follow-ups: None! 🎉 ### Checklist Delete any items that are not applicable to this PR. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [X] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [X] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) --- .../schemas/common/rule_monitoring.ts | 6 +- .../get_rule_execution_events_schema.ts | 16 +- .../security_solution/cypress/tasks/alerts.ts | 1 + .../viewer/exceptions_viewer_items.tsx | 9 +- .../visualization_actions/index.test.tsx | 1 + .../events/last_event_time/index.test.ts | 1 + .../containers/sourcerer/index.test.tsx | 1 + .../common/hooks/use_app_toasts.mock.ts | 1 + .../common/hooks/use_app_toasts.test.ts | 3 + .../public/common/hooks/use_app_toasts.ts | 7 +- .../common/lib/kibana/kibana_react.mock.ts | 4 + .../alerts_histogram_panel/index.test.tsx | 1 + .../detection_engine/rules/__mocks__/api.ts | 2 +- .../detection_engine/rules/api.test.ts | 4 +- .../containers/detection_engine/rules/api.ts | 21 +- .../containers/detection_engine/rules/mock.ts | 168 +++++++++ .../rules/use_pre_packaged_rules.test.tsx | 1 + .../rules/use_rule_execution_events.test.tsx | 4 +- .../rules/use_rule_execution_events.tsx | 14 +- .../detection_engine.test.tsx | 1 + .../__mocks__/rule_details_context.tsx | 62 ++++ .../execution_log_table.test.tsx.snap | 140 -------- .../rule_duration_format.test.tsx.snap | 9 - .../execution_log_columns.tsx | 12 +- .../execution_log_search_bar.tsx | 11 +- .../execution_log_table.test.tsx | 95 ++--- .../execution_log_table.tsx | 243 ++++++++++--- .../rule_duration_format.test.tsx | 81 ++++- .../rule_duration_format.tsx | 45 ++- .../execution_log_table/translations.ts | 21 ++ .../detection_engine/rules/details/index.tsx | 339 +++++++++--------- .../rules/details/rule_details_context.tsx | 216 +++++++++++ .../public/network/pages/network.test.tsx | 1 + .../timelines/containers/index.test.tsx | 1 + .../routes/__mocks__/request_responses.ts | 4 +- .../get_rule_execution_events_route.test.ts | 2 - .../client_for_routes/client_interface.ts | 4 +- .../event_log/event_log_reader.ts | 12 +- .../index.test.ts | 71 +++- .../get_execution_event_aggregation/index.ts | 60 +++- .../tests/get_rule_execution_events.ts | 79 +--- .../tests/template_data/execution_events.ts | 197 ++++++++-- .../utils/index_event_log_execution_events.ts | 3 +- 43 files changed, 1330 insertions(+), 644 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/execution_log_table.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/rule_duration_format.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/rule_details_context.tsx diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts index f73e60487a4a8f..af005d1b60a8f3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts @@ -53,8 +53,6 @@ export enum RuleExecutionStatus { export const ruleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatus); -export type RuleExecutionStatusType = t.TypeOf; - export const ruleExecutionStatusOrder = PositiveInteger; export type RuleExecutionStatusOrder = t.TypeOf; @@ -130,7 +128,7 @@ export const aggregateRuleExecutionEvent = t.type({ timed_out: t.boolean, indexing_duration_ms: t.number, search_duration_ms: t.number, - gap_duration_ms: t.number, + gap_duration_s: t.number, security_status: t.string, security_message: t.string, }); @@ -140,7 +138,7 @@ export type AggregateRuleExecutionEvent = t.TypeOf( 'DefaultStatusFiltersStringArray', t.array(ruleExecutionStatus).is, - (input, context): Either => { + (input, context): Either => { if (input == null) { return t.success([]); } else if (typeof input === 'string') { - return t.array(ruleExecutionStatus).validate(input.split(','), context); + if (input === '') { + return t.success([]); + } else { + return t.array(ruleExecutionStatus).validate(input.split(','), context); + } } else { return t.array(ruleExecutionStatus).validate(input, context); } diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index 9c5780a9a31fcc..3c4d434b1ec3f3 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -65,6 +65,7 @@ export const closeAlerts = () => { export const expandFirstAlertActions = () => { cy.get(TIMELINE_CONTEXT_MENU_BTN).should('be.visible'); + cy.get(TIMELINE_CONTEXT_MENU_BTN).find('svg').should('have.class', 'euiIcon-isLoaded'); cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true }); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx index 5331b2376fd9f1..30b5b3e4d13391 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx @@ -23,11 +23,6 @@ const MyFlexItem = styled(EuiFlexItem)` } `; -const MyExceptionsContainer = styled(EuiFlexGroup)` - height: 600px; - overflow: hidden; -`; - const MyExceptionItemContainer = styled(EuiFlexGroup)` margin: ${({ theme }) => `0 ${theme.eui.euiSize} ${theme.eui.euiSize} 0`}; `; @@ -55,7 +50,7 @@ const ExceptionsViewerItemsComponent: React.FC = ({ onEditExceptionItem, disableActions, }): JSX.Element => ( - + {showEmpty || showNoResults || isInitLoading ? ( = ({ )} - + ); ExceptionsViewerItemsComponent.displayName = 'ExceptionsViewerItemsComponent'; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/index.test.tsx index b06fde164af760..162f321b33f6c0 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/index.test.tsx @@ -95,6 +95,7 @@ describe('VisualizationActions', () => { addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), }, }, http: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts index 1bd188d01cb51d..ce03d159df5a9b 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts @@ -47,6 +47,7 @@ jest.mock('../../../lib/kibana', () => ({ addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), }), })); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx index 1d02d608c0e921..a949478593466a 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx @@ -60,6 +60,7 @@ jest.mock('../../lib/kibana', () => ({ addError: jest.fn(), addSuccess: jest.fn(), addWarning: mockAddWarning, + remove: jest.fn(), }), useKibana: () => ({ services: { diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts index c0bb52b20c534a..ae3783e82cdbfa 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts @@ -11,6 +11,7 @@ const createAppToastsMock = (): jest.Mocked => ({ addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), api: { get$: jest.fn(), add: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts index 9bea6282e3ad62..359d29be7cd080 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts @@ -30,15 +30,18 @@ describe('useAppToasts', () => { let addErrorMock: jest.Mock; let addSuccessMock: jest.Mock; let addWarningMock: jest.Mock; + let removeMock: jest.Mock; beforeEach(() => { addErrorMock = jest.fn(); addSuccessMock = jest.fn(); addWarningMock = jest.fn(); + removeMock = jest.fn(); (useToasts as jest.Mock).mockImplementation(() => ({ addError: addErrorMock, addSuccess: addSuccessMock, addWarning: addWarningMock, + remove: removeMock, })); }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts index 4b3b28a54c6420..d9c3713f3a4bac 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts @@ -19,7 +19,7 @@ import { IEsError, isEsError } from '@kbn/data-plugin/public'; import { ErrorToastOptions, ToastsStart, Toast } from '@kbn/core/public'; import { useToasts } from '../lib/kibana'; -export type UseAppToasts = Pick & { +export type UseAppToasts = Pick & { api: ToastsStart; addError: (error: unknown, options: ErrorToastOptions) => Toast; }; @@ -36,6 +36,7 @@ export const useAppToasts = (): UseAppToasts => { const addError = useRef(toasts.addError.bind(toasts)).current; const addSuccess = useRef(toasts.addSuccess.bind(toasts)).current; const addWarning = useRef(toasts.addWarning.bind(toasts)).current; + const remove = useRef(toasts.remove.bind(toasts)).current; const _addError = useCallback( (error: unknown, options: ErrorToastOptions) => { @@ -46,8 +47,8 @@ export const useAppToasts = (): UseAppToasts => { ); return useMemo( - () => ({ api: toasts, addError: _addError, addSuccess, addWarning }), - [_addError, addSuccess, addWarning, toasts] + () => ({ api: toasts, addError: _addError, addSuccess, addWarning, remove }), + [_addError, addSuccess, addWarning, remove, toasts] ); }; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index 0097e31cfee8ad..09e7c1fa6e8c84 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -157,6 +157,10 @@ export const createStartServicesMock = ( theme: { theme$: themeServiceMock.createTheme$(), }, + timelines: { + getLastUpdated: jest.fn(), + getFieldBrowser: jest.fn(), + }, } as unknown as StartServices; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx index 3c82161851dd3e..539728291156fe 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx @@ -55,6 +55,7 @@ jest.mock('../../../../common/lib/kibana/kibana_react', () => { addWarning: jest.fn(), addError: jest.fn(), addSuccess: jest.fn(), + remove: jest.fn(), }, }, }, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts index d93d667f5fbca1..59f79b294d7fa6 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts @@ -78,7 +78,7 @@ export const fetchRuleExecutionEvents = async ({ duration_ms: 3866, es_search_duration_ms: 1236, execution_uuid: '88d15095-7937-462c-8f21-9763e1387cad', - gap_duration_ms: 0, + gap_duration_s: 0, indexing_duration_ms: 95, message: "rule executed: siem.queryRule:fb1fc150-a292-11ec-a2cf-c1b28b0392b0: 'Lots of Execution Events'", diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index b999040674a912..c932d0f982bb68 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -630,7 +630,7 @@ describe('Detections Rules API', () => { start: '2001-01-01T17:00:00.000Z', end: '2001-01-02T17:00:00.000Z', queryText: '', - statusFilters: '', + statusFilters: [], signal: abortCtrl.signal, }); @@ -659,7 +659,7 @@ describe('Detections Rules API', () => { start: 'now-30', end: 'now', queryText: '', - statusFilters: '', + statusFilters: [], signal: abortCtrl.signal, }); expect(response).toEqual(responseMock); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index cb78a7a116ae49..220926ebc1722d 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { camelCase } from 'lodash'; import dateMath from '@kbn/datemath'; import { HttpStart } from '@kbn/core/public'; @@ -18,7 +19,11 @@ import { DETECTION_ENGINE_RULES_PREVIEW, detectionEngineRuleExecutionEventsUrl, } from '../../../../../common/constants'; -import { BulkAction } from '../../../../../common/detection_engine/schemas/common'; +import { + AggregateRuleExecutionEvent, + BulkAction, + RuleExecutionStatus, +} from '../../../../../common/detection_engine/schemas/common'; import { FullResponseSchema, PreviewResponse, @@ -320,11 +325,11 @@ export const exportRules = async ({ * @param start Start daterange either in UTC ISO8601 or as datemath string (e.g. `2021-12-29T02:44:41.653Z` or `now-30`) * @param end End daterange either in UTC ISO8601 or as datemath string (e.g. `2021-12-29T02:44:41.653Z` or `now/w`) * @param queryText search string in querystring format (e.g. `event.duration > 1000 OR kibana.alert.rule.execution.metrics.execution_gap_duration_s > 100`) - * @param statusFilters comma separated string of `statusFilters` (e.g. `succeeded,failed,partial failure`) + * @param statusFilters RuleExecutionStatus[] array of `statusFilters` (e.g. `succeeded,failed,partial failure`) * @param page current page to fetch * @param perPage number of results to fetch per page - * @param sortField field to sort by - * @param sortOrder what order to sort by (e.g. `asc` or `desc`) + * @param sortField keyof AggregateRuleExecutionEvent field to sort by + * @param sortOrder SortOrder what order to sort by (e.g. `asc` or `desc`) * @param signal AbortSignal Optional signal for cancelling the request * * @throws An error if response is not OK @@ -345,11 +350,11 @@ export const fetchRuleExecutionEvents = async ({ start: string; end: string; queryText?: string; - statusFilters?: string; + statusFilters?: RuleExecutionStatus[]; page?: number; perPage?: number; - sortField?: string; - sortOrder?: string; + sortField?: keyof AggregateRuleExecutionEvent; + sortOrder?: SortOrder; signal?: AbortSignal; }): Promise => { const url = detectionEngineRuleExecutionEventsUrl(ruleId); @@ -361,7 +366,7 @@ export const fetchRuleExecutionEvents = async ({ start: startDate?.utc().toISOString(), end: endDate?.utc().toISOString(), query_text: queryText, - status_filters: statusFilters, + status_filters: statusFilters?.sort()?.join(','), page, per_page: perPage, sort_field: sortField, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts index 533ab6138cb09f..8c1737a4519a72 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; import { FetchRulesResponse, Rule } from './types'; export const savedRuleMock: Rule = { @@ -126,3 +127,170 @@ export const rulesMock: FetchRulesResponse = { }, ], }; + +export const ruleExecutionEventsMock: GetAggregateRuleExecutionEventsResponse = { + events: [ + { + execution_uuid: 'dc45a63c-4872-4964-a2d0-bddd8b2e634d', + timestamp: '2022-04-28T21:19:08.047Z', + duration_ms: 3, + status: 'failure', + message: 'siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: execution failed', + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 0, + schedule_delay_ms: 2169, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 0, + gap_duration_s: 0, + security_status: 'failed', + security_message: 'Rule failed to execute because rule ran after it was disabled.', + }, + { + execution_uuid: '0fde9271-05d0-4bfb-8ff8-815756d28350', + timestamp: '2022-04-28T21:19:04.973Z', + duration_ms: 1446, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 0, + schedule_delay_ms: 2089, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 2, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + { + execution_uuid: '5daaa259-ded8-4a52-853e-1e7652d325d5', + timestamp: '2022-04-28T21:19:01.976Z', + duration_ms: 1395, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 1, + schedule_delay_ms: 2637, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 3, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + { + execution_uuid: 'c7223e1c-4264-4a27-8697-0d720243fafc', + timestamp: '2022-04-28T21:18:58.431Z', + duration_ms: 1815, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 1, + schedule_delay_ms: -255429, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 3, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + { + execution_uuid: '1f6ba0c1-cc36-4f45-b919-7790b8a8d670', + timestamp: '2022-04-28T21:18:13.954Z', + duration_ms: 2055, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 0, + schedule_delay_ms: 2027, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 0, + gap_duration_s: 0, + security_status: 'partial failure', + security_message: + 'Check privileges failed to execute ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [yup] name: "Click here for hot fresh alerts!" id: "a6e61cf0-c737-11ec-9e32-e14913ffdd2d" rule id: "34946b12-88d1-49ef-82b7-9cad45972030" execution id: "1f6ba0c1-cc36-4f45-b919-7790b8a8d670" space ID: "default"', + }, + { + execution_uuid: 'b0f65d64-b229-432b-9d39-f4385a7f9368', + timestamp: '2022-04-28T21:15:43.086Z', + duration_ms: 1205, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 672, + schedule_delay_ms: 3086, + timed_out: false, + indexing_duration_ms: 140, + search_duration_ms: 684, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + { + execution_uuid: '7bfd25b9-c0d8-44b1-982c-485169466a8e', + timestamp: '2022-04-28T21:10:40.135Z', + duration_ms: 6321, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 930, + schedule_delay_ms: 1222, + timed_out: false, + indexing_duration_ms: 2103, + search_duration_ms: 946, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + ], + total: 7, +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx index 6596cefef4f089..dfeaca617ed244 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -18,6 +18,7 @@ jest.mock('../../../../common/lib/kibana', () => ({ addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), }), })); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx index 2a4efb7f69491c..5fba7dd7a2ed20 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx @@ -51,7 +51,7 @@ describe('useRuleExecutionEvents', () => { start: 'now-30', end: 'now', queryText: '', - statusFilters: '', + statusFilters: [], }), { wrapper: createReactQueryWrapper(), @@ -92,7 +92,7 @@ describe('useRuleExecutionEvents', () => { duration_ms: 3866, es_search_duration_ms: 1236, execution_uuid: '88d15095-7937-462c-8f21-9763e1387cad', - gap_duration_ms: 0, + gap_duration_s: 0, indexing_duration_ms: 95, message: "rule executed: siem.queryRule:fb1fc150-a292-11ec-a2cf-c1b28b0392b0: 'Lots of Execution Events'", diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx index f2e72858cf3920..e18d1f6c2ce5cb 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx @@ -5,22 +5,27 @@ * 2.0. */ +import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { useQuery } from 'react-query'; +import { + AggregateRuleExecutionEvent, + RuleExecutionStatus, +} from '../../../../../common/detection_engine/schemas/common'; import { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { fetchRuleExecutionEvents } from './api'; import * as i18n from './translations'; -interface UseRuleExecutionEventsArgs { +export interface UseRuleExecutionEventsArgs { ruleId: string; start: string; end: string; queryText?: string; - statusFilters?: string; + statusFilters?: RuleExecutionStatus[]; page?: number; perPage?: number; - sortField?: string; - sortOrder?: string; + sortField?: keyof AggregateRuleExecutionEvent; + sortOrder?: SortOrder; } export const useRuleExecutionEvents = ({ @@ -66,6 +71,7 @@ export const useRuleExecutionEvents = ({ }); }, { + keepPreviousData: true, onError: (e) => { addError(e, { title: i18n.RULE_EXECUTION_EVENTS_FETCH_FAILURE }); }, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index a783ac7e1e2604..1f066751c2b925 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -98,6 +98,7 @@ jest.mock('../../../common/lib/kibana', () => { addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), }), }; }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx new file mode 100644 index 00000000000000..257dc8ec512a8a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuleDetailsContextType } from '../rule_details_context'; +import React from 'react'; + +export const useRuleDetailsContextMock = { + create: (): jest.Mocked => ({ + executionLogs: { + state: { + superDatePicker: { + recentlyUsedRanges: [], + refreshInterval: 1000, + isPaused: true, + start: 'now-24h', + end: 'now', + }, + queryText: '', + statusFilters: [], + showMetricColumns: true, + pagination: { + pageIndex: 1, + pageSize: 5, + }, + sort: { + sortField: 'timestamp', + sortDirection: 'desc', + }, + }, + actions: { + setEnd: jest.fn(), + setIsPaused: jest.fn(), + setPageIndex: jest.fn(), + setPageSize: jest.fn(), + setQueryText: jest.fn(), + setRecentlyUsedRanges: jest.fn(), + setRefreshInterval: jest.fn(), + setShowMetricColumns: jest.fn(), + setSortDirection: jest.fn(), + setSortField: jest.fn(), + setStart: jest.fn(), + setStatusFilters: jest.fn(), + }, + }, + }), +}; + +export const useRuleDetailsContext = jest + .fn, []>() + .mockImplementation(useRuleDetailsContextMock.create); + +export const useRuleDetailsContextOptional = jest + .fn, []>() + .mockImplementation(useRuleDetailsContextMock.create); + +export const RulesTableContextProvider = jest + .fn() + .mockImplementation(({ children }: { children: React.ReactNode }) => <>{children}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/execution_log_table.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/execution_log_table.test.tsx.snap deleted file mode 100644 index f93cce70172dc9..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/execution_log_table.test.tsx.snap +++ /dev/null @@ -1,140 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ExecutionLogTable snapshots renders correctly against snapshot 1`] = ` - - - - - - - - - - - - - - - Showing 0 rule executions - - - - - - - - - - - , - "render": [Function], - "sortable": false, - "truncateText": false, - "width": "10%", - }, - Object { - "field": "timestamp", - "name": , - "render": [Function], - "sortable": true, - "truncateText": false, - "width": "15%", - }, - Object { - "field": "duration_ms", - "name": , - "render": [Function], - "sortable": true, - "truncateText": false, - "width": "10%", - }, - Object { - "field": "security_message", - "name": , - "render": [Function], - "sortable": false, - "truncateText": false, - "width": "35%", - }, - ] - } - items={Array []} - noItemsMessage={ - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 5, - "pageSizeOptions": Array [ - 5, - 10, - 25, - 50, - ], - "totalItemCount": 0, - } - } - responsive={true} - sorting={ - Object { - "sort": Object { - "direction": "desc", - "field": "timestamp", - }, - } - } - tableLayout="fixed" - /> - -`; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/rule_duration_format.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/rule_duration_format.test.tsx.snap deleted file mode 100644 index ddb59cf52a8903..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/rule_duration_format.test.tsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RuleDurationFormat snapshots renders correctly against snapshot 1`] = ` - - 00:00:00:000 - -`; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_columns.tsx index c48f5d6971d236..98569b701d93cb 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_columns.tsx @@ -6,9 +6,9 @@ */ import { EuiBasicTableColumn, EuiHealth, EuiLink, EuiText } from '@elastic/eui'; -import { capitalize } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n-react'; import { DocLinksStart } from '@kbn/core/public'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { capitalize } from 'lodash'; import React from 'react'; import { AggregateRuleExecutionEvent, @@ -19,9 +19,9 @@ import { FormattedDate } from '../../../../../../common/components/formatted_dat import { getStatusColor } from '../../../../../components/rules/rule_execution_status/utils'; import { PopoverTooltip } from '../../all/popover_tooltip'; import { TableHeaderTooltipCell } from '../../all/table_header_tooltip_cell'; +import { RuleDurationFormat } from './rule_duration_format'; import * as i18n from './translations'; -import { RuleDurationFormat } from './rule_duration_format'; export const EXECUTION_LOG_COLUMNS: Array> = [ { @@ -32,7 +32,7 @@ export const EXECUTION_LOG_COLUMNS: Array ), field: 'security_status', - render: (value: RuleExecutionStatus, data) => + render: (value: RuleExecutionStatus) => value ? ( {capitalize(value)} ) : ( @@ -89,7 +89,7 @@ export const GET_EXECUTION_LOG_METRICS_COLUMNS = ( docLinks: DocLinksStart ): Array> => [ { - field: 'gap_duration_ms', + field: 'gap_duration_s', name: ( ), render: (value: number) => ( - <>{value ? : getEmptyValue()} + <>{value ? : getEmptyValue()} ), sortable: true, truncateText: false, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.tsx index b8c2d82ab324e8..74804b7c6b5576 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.tsx @@ -56,20 +56,23 @@ const statusFilters = statuses.map((status) => ({ interface ExecutionLogTableSearchProps { onlyShowFilters: true; onSearch: (queryText: string) => void; - onStatusFilterChange: (statusFilters: string[]) => void; + onStatusFilterChange: (statusFilters: RuleExecutionStatus[]) => void; + defaultSelectedStatusFilters?: RuleExecutionStatus[]; } /** * SearchBar + StatusFilters component to be used with the Rule Execution Log table - * NOTE: This component is currently not shown in the UI as custom search queries + * NOTE: The SearchBar component is currently not shown in the UI as custom search queries * are not yet fully supported by the Rule Execution Log aggregation API since * certain queries could result in missing data or inclusion of wrong events. * Please see this comment for history/details: https://github.com/elastic/kibana/pull/127339/files#r825240516 */ export const ExecutionLogSearchBar = React.memo( - ({ onlyShowFilters, onSearch, onStatusFilterChange }) => { + ({ onlyShowFilters, onSearch, onStatusFilterChange, defaultSelectedStatusFilters = [] }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [selectedFilters, setSelectedFilters] = useState([]); + const [selectedFilters, setSelectedFilters] = useState( + defaultSelectedStatusFilters + ); const onSearchCallback = useCallback( (queryText: string) => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.test.tsx index 81619d01934ba5..608853f004eb94 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.test.tsx @@ -5,83 +5,21 @@ * 2.0. */ +import { ruleExecutionEventsMock } from '../../../../../containers/detection_engine/rules/mock'; +import { render, screen } from '@testing-library/react'; +import { TestProviders } from '../../../../../../common/mock'; +import { useRuleDetailsContextMock } from '../__mocks__/rule_details_context'; import React from 'react'; -import { shallow } from 'enzyme'; import { noop } from 'lodash/fp'; +import { useRuleExecutionEvents } from '../../../../../containers/detection_engine/rules'; import { useSourcererDataView } from '../../../../../../common/containers/sourcerer'; +import { useRuleDetailsContext } from '../rule_details_context'; import { ExecutionLogTable } from './execution_log_table'; -jest.mock('../../../../../containers/detection_engine/rules', () => { - const original = jest.requireActual('../../../../../containers/detection_engine/rules'); - return { - ...original, - useRuleExecutionEvents: jest.fn().mockReturnValue({ - loading: true, - setQuery: () => undefined, - data: null, - response: '', - request: '', - refetch: null, - }), - }; -}); - jest.mock('../../../../../../common/containers/sourcerer'); - -jest.mock('../../../../../../common/hooks/use_app_toasts', () => { - const original = jest.requireActual('../../../../../../common/hooks/use_app_toasts'); - - return { - ...original, - useAppToasts: () => ({ - addSuccess: jest.fn(), - addError: jest.fn(), - }), - }; -}); - -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - return { - ...original, - useDispatch: () => jest.fn(), - useSelector: () => jest.fn(), - }; -}); - -jest.mock('../../../../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../../../../common/lib/kibana'); - - return { - ...original, - useUiSetting$: jest.fn().mockReturnValue([]), - useKibana: () => ({ - services: { - data: { - query: { - filterManager: jest.fn().mockReturnValue({}), - }, - }, - docLinks: { - links: { - siem: { - troubleshootGaps: 'link', - }, - }, - }, - storage: { - get: jest.fn(), - set: jest.fn(), - }, - timelines: { - getLastUpdated: jest.fn(), - getFieldBrowser: jest.fn(), - }, - }, - }), - }; -}); +jest.mock('../../../../../containers/detection_engine/rules'); +jest.mock('../rule_details_context'); const mockUseSourcererDataView = useSourcererDataView as jest.Mock; mockUseSourcererDataView.mockReturnValue({ @@ -92,13 +30,20 @@ mockUseSourcererDataView.mockReturnValue({ loading: false, }); -// TODO: Replace snapshot test with base test cases +const mockUseRuleExecutionEvents = useRuleExecutionEvents as jest.Mock; +mockUseRuleExecutionEvents.mockReturnValue({ + data: ruleExecutionEventsMock, + isLoading: false, + isFetching: false, +}); describe('ExecutionLogTable', () => { - describe('snapshots', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + test('Shows total events returned', () => { + const ruleDetailsContext = useRuleDetailsContextMock.create(); + (useRuleDetailsContext as jest.Mock).mockReturnValue(ruleDetailsContext); + render(, { + wrapper: TestProviders, }); + expect(screen.getByTestId('executionsShowing')).toHaveTextContent('Showing 7 rule executions'); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx index 31fc446fa2bb7d..b471493a9ecd26 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import React, { useCallback, useMemo, useState } from 'react'; +import moment from 'moment'; +import React, { useCallback, useMemo, useRef } from 'react'; import { EuiTextColor, EuiFlexGroup, @@ -20,11 +20,17 @@ import { EuiSpacer, EuiSwitch, EuiBasicTable, + EuiButton, } from '@elastic/eui'; -import { buildFilter, FILTERS } from '@kbn/es-query'; +import { buildFilter, Filter, FILTERS, Query } from '@kbn/es-query'; import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; +import { mountReactNode } from '@kbn/core/public/utils'; +import { RuleDetailTabs } from '..'; import { RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY } from '../../../../../../../common/constants'; -import { AggregateRuleExecutionEvent } from '../../../../../../../common/detection_engine/schemas/common'; +import { + AggregateRuleExecutionEvent, + RuleExecutionStatus, +} from '../../../../../../../common/detection_engine/schemas/common'; import { UtilityBar, @@ -34,9 +40,23 @@ import { } from '../../../../../../common/components/utility_bar'; import { useSourcererDataView } from '../../../../../../common/containers/sourcerer'; import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts'; +import { useDeepEqualSelector } from '../../../../../../common/hooks/use_selector'; import { useKibana } from '../../../../../../common/lib/kibana'; +import { inputsSelectors } from '../../../../../../common/store'; +import { + setAbsoluteRangeDatePicker, + setFilterQuery, + setRelativeRangeDatePicker, +} from '../../../../../../common/store/inputs/actions'; +import { + AbsoluteTimeRange, + isAbsoluteTimeRange, + isRelativeTimeRange, + RelativeTimeRange, +} from '../../../../../../common/store/inputs/model'; import { SourcererScopeName } from '../../../../../../common/store/sourcerer/model'; import { useRuleExecutionEvents } from '../../../../../containers/detection_engine/rules'; +import { useRuleDetailsContext } from '../rule_details_context'; import * as i18n from './translations'; import { EXECUTION_LOG_COLUMNS, GET_EXECUTION_LOG_METRICS_COLUMNS } from './execution_log_columns'; import { ExecutionLogSearchBar } from './execution_log_search_bar'; @@ -56,6 +76,12 @@ interface ExecutionLogTableProps { selectAlertsTab: () => void; } +interface CachedGlobalQueryState { + filters: Filter[]; + query: Query; + timerange: AbsoluteTimeRange | RelativeTimeRange; +} + const ExecutionLogTableComponent: React.FC = ({ ruleId, selectAlertsTab, @@ -68,28 +94,84 @@ const ExecutionLogTableComponent: React.FC = ({ storage, timelines, } = useKibana().services; - // Datepicker state - const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([]); - const [refreshInterval, setRefreshInterval] = useState(1000); - const [isPaused, setIsPaused] = useState(true); - const [start, setStart] = useState('now-24h'); - const [end, setEnd] = useState('now'); - // Searchbar/Filter/Settings state - const [queryText, setQueryText] = useState(''); - const [statusFilters, setStatusFilters] = useState(undefined); - const [showMetricColumns, setShowMetricColumns] = useState( - storage.get(RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY) ?? false - ); + const { + [RuleDetailTabs.executionLogs]: { + state: { + superDatePicker: { recentlyUsedRanges, refreshInterval, isPaused, start, end }, + queryText, + statusFilters, + showMetricColumns, + pagination: { pageIndex, pageSize }, + sort: { sortField, sortDirection }, + }, + actions: { + setEnd, + setIsPaused, + setPageIndex, + setPageSize, + setQueryText, + setRecentlyUsedRanges, + setRefreshInterval, + setShowMetricColumns, + setSortDirection, + setSortField, + setStart, + setStatusFilters, + }, + }, + } = useRuleDetailsContext(); - // Pagination state - const [pageIndex, setPageIndex] = useState(1); - const [pageSize, setPageSize] = useState(5); - const [sortField, setSortField] = useState('timestamp'); - const [sortDirection, setSortDirection] = useState('desc'); // Index for `add filter` action and toasts for errors const { indexPattern } = useSourcererDataView(SourcererScopeName.detections); - const { addError, addSuccess } = useAppToasts(); + const { addError, addSuccess, remove } = useAppToasts(); + + // QueryString, Filters, and TimeRange state + const dispatch = useDispatch(); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const timerange = useDeepEqualSelector(inputsSelectors.globalTimeRangeSelector); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + const cachedGlobalQueryState = useRef({ filters, query, timerange }); + const successToastId = useRef(''); + + const resetGlobalQueryState = useCallback(() => { + if (isAbsoluteTimeRange(cachedGlobalQueryState.current.timerange)) { + dispatch( + setAbsoluteRangeDatePicker({ + id: 'global', + from: cachedGlobalQueryState.current.timerange.from, + to: cachedGlobalQueryState.current.timerange.to, + }) + ); + } else if (isRelativeTimeRange(cachedGlobalQueryState.current.timerange)) { + dispatch( + setRelativeRangeDatePicker({ + id: 'global', + from: cachedGlobalQueryState.current.timerange.from, + fromStr: cachedGlobalQueryState.current.timerange.fromStr, + to: cachedGlobalQueryState.current.timerange.to, + toStr: cachedGlobalQueryState.current.timerange.toStr, + }) + ); + } + + dispatch( + setFilterQuery({ + id: 'global', + query: cachedGlobalQueryState.current.query.query, + language: cachedGlobalQueryState.current.query.language, + }) + ); + // Using filterManager directly as dispatch(setSearchBarFilter()) was not replacing filters + filterManager.removeAll(); + filterManager.addFilters(cachedGlobalQueryState.current.filters); + remove(successToastId.current); + }, [dispatch, filterManager, remove]); // Table data state const { @@ -118,15 +200,18 @@ const ExecutionLogTableComponent: React.FC = ({ }, [indexPattern]); // Callbacks - const onTableChangeCallback = useCallback(({ page = {}, sort = {} }) => { - const { index, size } = page; - const { field, direction } = sort; + const onTableChangeCallback = useCallback( + ({ page = {}, sort = {} }) => { + const { index, size } = page; + const { field, direction } = sort; - setPageIndex(index + 1); - setPageSize(size); - setSortField(field); - setSortDirection(direction); - }, []); + setPageIndex(index + 1); + setPageSize(size); + setSortField(field); + setSortDirection(direction); + }, + [setPageIndex, setPageSize, setSortDirection, setSortField] + ); const onTimeChangeCallback = useCallback( (props: OnTimeChangeProps) => { @@ -141,14 +226,17 @@ const ExecutionLogTableComponent: React.FC = ({ recentlyUsedRange.length > 10 ? recentlyUsedRange.slice(0, 9) : recentlyUsedRange ); }, - [recentlyUsedRanges] + [recentlyUsedRanges, setEnd, setRecentlyUsedRanges, setStart] ); - const onRefreshChangeCallback = useCallback((props: OnRefreshChangeProps) => { - setIsPaused(props.isPaused); - // Only support auto-refresh >= 1minute -- no current ability to limit within component - setRefreshInterval(props.refreshInterval > 60000 ? props.refreshInterval : 60000); - }, []); + const onRefreshChangeCallback = useCallback( + (props: OnRefreshChangeProps) => { + setIsPaused(props.isPaused); + // Only support auto-refresh >= 1minute -- no current ability to limit within component + setRefreshInterval(props.refreshInterval > 60000 ? props.refreshInterval : 60000); + }, + [setIsPaused, setRefreshInterval] + ); const onRefreshCallback = useCallback( (props: OnRefreshProps) => { @@ -157,19 +245,26 @@ const ExecutionLogTableComponent: React.FC = ({ [refetch] ); - const onSearchCallback = useCallback((updatedQueryText: string) => { - setQueryText(updatedQueryText); - }, []); + const onSearchCallback = useCallback( + (updatedQueryText: string) => { + setQueryText(updatedQueryText); + }, + [setQueryText] + ); - const onStatusFilterChangeCallback = useCallback((updatedStatusFilters: string[]) => { - setStatusFilters( - updatedStatusFilters.length ? updatedStatusFilters.sort().join(',') : undefined - ); - }, []); + const onStatusFilterChangeCallback = useCallback( + (updatedStatusFilters: RuleExecutionStatus[]) => { + setStatusFilters(updatedStatusFilters); + }, + [setStatusFilters] + ); const onFilterByExecutionIdCallback = useCallback( (executionId: string, executionStart: string) => { if (uuidDataViewField != null) { + // Update cached global query state with current state as a rollback point + cachedGlobalQueryState.current = { filters, query, timerange }; + // Create filter & daterange constraints const filter = buildFilter( indexPattern, uuidDataViewField, @@ -179,20 +274,55 @@ const ExecutionLogTableComponent: React.FC = ({ executionId, null ); + dispatch( + setAbsoluteRangeDatePicker({ + id: 'global', + from: moment(executionStart).subtract(1, 'days').toISOString(), + to: moment(executionStart).add(1, 'days').toISOString(), + }) + ); filterManager.removeAll(); filterManager.addFilters(filter); + dispatch(setFilterQuery({ id: 'global', query: '', language: 'kuery' })); selectAlertsTab(); - addSuccess({ - title: i18n.ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_TITLE, - text: i18n.ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_DESCRIPTION, - }); + successToastId.current = addSuccess( + { + title: i18n.ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_TITLE, + text: mountReactNode( + <> +

{i18n.ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_DESCRIPTION}

+ + + + {i18n.ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_RESTORE_BUTTON} + + + + + ), + }, + // Essentially keep toast around till user dismisses via 'x' + { toastLifeTimeMs: 10 * 60 * 1000 } + ).id; } else { addError(i18n.ACTIONS_FIELD_NOT_FOUND_ERROR, { title: i18n.ACTIONS_FIELD_NOT_FOUND_ERROR_TITLE, }); } }, - [addError, addSuccess, filterManager, indexPattern, selectAlertsTab, uuidDataViewField] + [ + addError, + addSuccess, + dispatch, + filterManager, + filters, + indexPattern, + query, + resetGlobalQueryState, + selectAlertsTab, + timerange, + uuidDataViewField, + ] ); const onShowMetricColumnsCallback = useCallback( @@ -200,7 +330,7 @@ const ExecutionLogTableComponent: React.FC = ({ storage.set(RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY, showMetrics); setShowMetricColumns(showMetrics); }, - [storage] + [setShowMetricColumns, storage] ); // Memoized state @@ -223,8 +353,6 @@ const ExecutionLogTableComponent: React.FC = ({ }; }, [sortDirection, sortField]); - // TODO: Re-add actions once alert count is displayed in table and UX is finalized - // @ts-expect-error unused constant const actions = useMemo( () => [ { @@ -258,9 +386,9 @@ const ExecutionLogTableComponent: React.FC = ({ const executionLogColumns = useMemo( () => showMetricColumns - ? [...EXECUTION_LOG_COLUMNS, ...GET_EXECUTION_LOG_METRICS_COLUMNS(docLinks)] - : [...EXECUTION_LOG_COLUMNS], - [docLinks, showMetricColumns] + ? [...EXECUTION_LOG_COLUMNS, ...GET_EXECUTION_LOG_METRICS_COLUMNS(docLinks), ...actions] + : [...EXECUTION_LOG_COLUMNS, ...actions], + [actions, docLinks, showMetricColumns] ); return ( @@ -271,6 +399,7 @@ const ExecutionLogTableComponent: React.FC = ({ onSearch={onSearchCallback} onStatusFilterChange={onStatusFilterChangeCallback} onlyShowFilters={true} + defaultSelectedStatusFilters={statusFilters} /> @@ -315,7 +444,7 @@ const ExecutionLogTableComponent: React.FC = ({ - + {timelines.getLastUpdated({ showUpdating: isLoading || isFetching, updatedAt: dataUpdatedAt, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.test.tsx index 44f765f934ae86..30e7d6e8f0a2e7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.test.tsx @@ -5,17 +5,82 @@ * 2.0. */ -import { shallow } from 'enzyme'; +import { render } from '@testing-library/react'; import React from 'react'; -import { RuleDurationFormat } from './rule_duration_format'; - -// TODO: Replace snapshot test with base test cases +import { getFormattedDuration, RuleDurationFormat } from './rule_duration_format'; describe('RuleDurationFormat', () => { - describe('snapshots', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + describe('getFormattedDuration', () => { + test('if input value is 0, formatted response is also 0', () => { + const formattedDuration = getFormattedDuration(0); + expect(formattedDuration).toEqual('00:00:00:000'); + }); + + test('if input value only contains ms, formatted response also only contains ms (SSS)', () => { + const formattedDuration = getFormattedDuration(999); + expect(formattedDuration).toEqual('00:00:00:999'); + }); + + test('for milliseconds (SSS) to seconds (ss) overflow', () => { + const formattedDuration = getFormattedDuration(1000); + expect(formattedDuration).toEqual('00:00:01:000'); + }); + + test('for seconds (ss) to minutes (mm) overflow', () => { + const formattedDuration = getFormattedDuration(60000 + 1); + expect(formattedDuration).toEqual('00:01:00:001'); + }); + + test('for minutes (mm) to hours (hh) overflow', () => { + const formattedDuration = getFormattedDuration(60000 * 60 + 1); + expect(formattedDuration).toEqual('01:00:00:001'); + }); + + test('for hours (hh) to days (ddd) overflow', () => { + const formattedDuration = getFormattedDuration(60000 * 60 * 24 + 1); + expect(formattedDuration).toEqual('001:00:00:00:001'); + }); + + test('for overflow with all units up to hours (hh)', () => { + const formattedDuration = getFormattedDuration(25033167); + expect(formattedDuration).toEqual('06:57:13:167'); + }); + + test('for overflow with all units up to hours (ddd)', () => { + const formattedDuration = getFormattedDuration(2503316723); + expect(formattedDuration).toEqual('028:23:21:56:723'); + }); + + test('for overflow greater than a year', () => { + const formattedDuration = getFormattedDuration((60000 * 60 * 24 + 1) * 365); + expect(formattedDuration).toEqual('> 1 Year'); + }); + + test('for max overflow', () => { + const formattedDuration = getFormattedDuration(Number.MAX_SAFE_INTEGER); + expect(formattedDuration).toEqual('> 1 Year'); + }); + }); + + describe('RuleDurationFormatComponent', () => { + test('renders correctly with duration and no additional props', () => { + const { getByTestId } = render(); + expect(getByTestId('rule-duration-format-value')).toHaveTextContent('00:00:01:000'); + }); + + test('renders correctly with duration and isSeconds=true', () => { + const { getByTestId } = render(); + expect(getByTestId('rule-duration-format-value')).toHaveTextContent('00:00:01:000'); + }); + + test('renders correctly with allowZero=true', () => { + const { getByTestId } = render(); + expect(getByTestId('rule-duration-format-value')).toHaveTextContent('N/A'); + }); + + test('renders correctly with max overflow', () => { + const { getByTestId } = render(); + expect(getByTestId('rule-duration-format-value')).toHaveTextContent('> 1 Year'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.tsx index cdbc19ce16c3b8..c9f8e1d2734d8e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.tsx @@ -5,48 +5,59 @@ * 2.0. */ -import numeral from '@elastic/numeral'; import moment from 'moment'; import React, { useMemo } from 'react'; +import * as i18n from './translations'; + interface Props { duration: number; - isMillis?: boolean; + isSeconds?: boolean; allowZero?: boolean; } -export function getFormattedDuration(value: number) { +export const getFormattedDuration = (value: number) => { if (!value) { return '00:00:00:000'; } const duration = moment.duration(value); - const hours = Math.floor(duration.asHours()).toString().padStart(2, '0'); - const minutes = Math.floor(duration.asMinutes()).toString().padStart(2, '0'); + const days = Math.floor(duration.asDays()).toString().padStart(3, '0'); + const hours = Math.floor(duration.asHours() % 24) + .toString() + .padStart(2, '0'); + const minutes = Math.floor(duration.asMinutes() % 60) + .toString() + .padStart(2, '0'); const seconds = duration.seconds().toString().padStart(2, '0'); const ms = duration.milliseconds().toString().padStart(3, '0'); - return `${hours}:${minutes}:${seconds}:${ms}`; -} -export function getFormattedMilliseconds(value: number) { - const formatted = numeral(value).format('0,0'); - return `${formatted} ms`; -} + if (Math.floor(duration.asDays()) > 0) { + if (Math.floor(duration.asDays()) >= 365) { + return i18n.GREATER_THAN_YEAR; + } else { + return `${days}:${hours}:${minutes}:${seconds}:${ms}`; + } + } else { + return `${hours}:${minutes}:${seconds}:${ms}`; + } +}; /** - * Formats duration as (hh:mm:ss:SSS) - * @param props duration default as nanos, set isMillis:true to pass in ms + * Formats duration as (hh:mm:ss:SSS) by default, overflowing to include days + * as (ddd:hh:mm:ss:SSS) if necessary, and then finally to `> 1 Year` + * @param props duration as millis, set isSeconds:true to pass in seconds * @constructor */ const RuleDurationFormatComponent = (props: Props) => { - const { duration, isMillis = false, allowZero = true } = props; + const { duration, isSeconds = false, allowZero = true } = props; const formattedDuration = useMemo(() => { // Durations can be buggy and return negative if (allowZero && duration >= 0) { - return getFormattedDuration(isMillis ? duration * 1000 : duration); + return getFormattedDuration(isSeconds ? duration * 1000 : duration); } - return 'N/A'; - }, [allowZero, duration, isMillis]); + return i18n.DURATION_NOT_AVAILABLE; + }, [allowZero, duration, isSeconds]); return {formattedDuration}; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/translations.ts index d5d8d566649076..b161ae3662e0e3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/translations.ts @@ -181,6 +181,13 @@ export const ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_DESCRIPTION = i18n.transla } ); +export const ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_RESTORE_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.actionSearchFiltersUpdatedRestoreButtonTitle', + { + defaultMessage: 'Restore previous filters', + } +); + export const ACTIONS_FIELD_NOT_FOUND_ERROR_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.actionFieldNotFoundErrorTitle', { @@ -194,3 +201,17 @@ export const ACTIONS_FIELD_NOT_FOUND_ERROR = i18n.translate( defaultMessage: "Cannot find field 'kibana.alert.rule.execution.uuid' in alerts index.", } ); + +export const DURATION_NOT_AVAILABLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.durationNotAvailableDescription', + { + defaultMessage: 'N/A', + } +); + +export const GREATER_THAN_YEAR = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.durationGreaterThanYearDescription', + { + defaultMessage: '> 1 Year', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 8550d03eca2aa8..cc8872b901f441 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -108,6 +108,7 @@ import { import * as detectionI18n from '../../translations'; import * as ruleI18n from '../translations'; import { ExecutionLogTable } from './execution_log_table/execution_log_table'; +import { RuleDetailsContextProvider } from './rule_details_context'; import * as i18n from './translations'; import { NeedAdminForUpdateRulesCallOut } from '../../../../components/callouts/need_admin_for_update_callout'; import { MissingPrivilegesCallOut } from '../../../../components/callouts/missing_privileges_callout'; @@ -131,7 +132,14 @@ const StyledFullHeightContainer = styled.div` flex: 1 1 auto; `; -enum RuleDetailTabs { +/** + * Sets min-height on tab container to minimize page hop when switching to tabs with less content + */ +const StyledMinHeightTabContainer = styled.div` + min-height: 800px; +`; + +export enum RuleDetailTabs { alerts = 'alerts', executionLogs = 'executionLogs', exceptions = 'exceptions', @@ -639,181 +647,186 @@ const RuleDetailsPageComponent: React.FC = ({ indexPattern={indexPattern} /> - - - - - - - {ruleStatusI18n.STATUS} - {':'} - - {ruleStatusInfo} - - - } - title={title} - badgeOptions={badgeOptions} - > - - - - - - {i18n.ENABLE_RULE} + + + + + + + {ruleStatusI18n.STATUS} + {':'} + + {ruleStatusInfo} - - - - - - {editRule} - - - - - - - - {ruleError} - {getLegacyUrlConflictCallout} - - - - - - - - - - - {defineRuleData != null && ( - + } + title={title} + badgeOptions={badgeOptions} + > + + + + + - )} - + {i18n.ENABLE_RULE} + + - - - - {scheduleRuleData != null && ( - + + {editRule} + + - )} - + + - - - - {tabs} - - - {ruleDetailTab === RuleDetailTabs.alerts && hasIndexRead && ( - <> - - - + {ruleError} + {getLegacyUrlConflictCallout} + + + + - - {updatedAt && - timelinesUi.getLastUpdated({ - updatedAt: updatedAt || Date.now(), - showUpdating, - })} + + + + + + {defineRuleData != null && ( + + )} + + + + + + {scheduleRuleData != null && ( + + )} + + + - - - - - - {ruleId != null && ( - + {tabs} + + + + {ruleDetailTab === RuleDetailTabs.alerts && hasIndexRead && ( + <> + + + + + + {updatedAt && + timelinesUi.getLastUpdated({ + updatedAt: updatedAt || Date.now(), + showUpdating, + })} + + + + + + + + {ruleId != null && ( + + )} + + )} + {ruleDetailTab === RuleDetailTabs.exceptions && ( + )} - - )} - {ruleDetailTab === RuleDetailTabs.exceptions && ( - - )} - {ruleDetailTab === RuleDetailTabs.executionLogs && ( - - )} - + {ruleDetailTab === RuleDetailTabs.executionLogs && ( + + )} + + + diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/rule_details_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/rule_details_context.tsx new file mode 100644 index 00000000000000..13b17d0493d43e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/rule_details_context.tsx @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; +import React, { createContext, useContext, useMemo, useState } from 'react'; +import { RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY } from '../../../../../../common/constants'; +import { + AggregateRuleExecutionEvent, + RuleExecutionStatus, +} from '../../../../../../common/detection_engine/schemas/common'; +import { invariant } from '../../../../../../common/utils/invariant'; +import { useKibana } from '../../../../../common/lib/kibana'; +import { RuleDetailTabs } from '.'; + +export interface ExecutionLogTableState { + /** + * State of the SuperDatePicker component + */ + superDatePicker: { + /** + * DateRanges to display as recently used + */ + recentlyUsedRanges: DurationRange[]; + /** + * Interval to auto-refresh at + */ + refreshInterval: number; + /** + * State of auto-refresh + */ + isPaused: boolean; + /** + * Start datetime + */ + start: string; + /** + * End datetime + */ + end: string; + }; + /** + * SearchBar query + */ + queryText: string; + /** + * Selected Filters by Execution Status(es) + */ + statusFilters: RuleExecutionStatus[]; + /** + * Whether or not to show additional metric columnbs + */ + showMetricColumns: boolean; + /** + * Currently selected page and number of rows per page + */ + pagination: { + pageIndex: number; + pageSize: number; + }; + sort: { + sortField: keyof AggregateRuleExecutionEvent; + sortDirection: SortOrder; + }; +} + +// @ts-expect-error unused constant +const DEFAULT_STATE: ExecutionLogTableState = { + superDatePicker: { + recentlyUsedRanges: [], + refreshInterval: 1000, + isPaused: true, + start: 'now-24hr', + end: 'now', + }, + queryText: '', + statusFilters: [], + showMetricColumns: false, + pagination: { + pageIndex: 1, + pageSize: 5, + }, + sort: { + sortField: 'timestamp', + sortDirection: 'desc', + }, +}; + +export interface ExecutionLogTableActions { + setRecentlyUsedRanges: React.Dispatch>; + setRefreshInterval: React.Dispatch>; + setIsPaused: React.Dispatch>; + setStart: React.Dispatch>; + setEnd: React.Dispatch>; + setQueryText: React.Dispatch>; + setStatusFilters: React.Dispatch>; + setShowMetricColumns: React.Dispatch>; + setPageIndex: React.Dispatch>; + setPageSize: React.Dispatch>; + setSortField: React.Dispatch>; + setSortDirection: React.Dispatch>; +} + +export interface RuleDetailsContextType { + // TODO: Add section for RuleDetailTabs.exceptions and store query/pagination/etc. + // TODO: Let's discuss how to integration with ExceptionsViewerComponent state mgmt + [RuleDetailTabs.executionLogs]: { + state: ExecutionLogTableState; + actions: ExecutionLogTableActions; + }; +} + +const RuleDetailsContext = createContext(null); + +interface RuleDetailsContextProviderProps { + children: React.ReactNode; +} + +export const RuleDetailsContextProvider = ({ children }: RuleDetailsContextProviderProps) => { + const { storage } = useKibana().services; + + // Execution Log Table tab + // // SuperDatePicker State + const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([]); + const [refreshInterval, setRefreshInterval] = useState(1000); + const [isPaused, setIsPaused] = useState(true); + const [start, setStart] = useState('now-24h'); + const [end, setEnd] = useState('now'); + // Searchbar/Filter/Settings state + const [queryText, setQueryText] = useState(''); + const [statusFilters, setStatusFilters] = useState([]); + const [showMetricColumns, setShowMetricColumns] = useState( + storage.get(RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY) ?? false + ); + // Pagination state + const [pageIndex, setPageIndex] = useState(1); + const [pageSize, setPageSize] = useState(5); + const [sortField, setSortField] = useState('timestamp'); + const [sortDirection, setSortDirection] = useState('desc'); + // // End Execution Log Table tab + + const providerValue = useMemo( + () => ({ + [RuleDetailTabs.executionLogs]: { + state: { + superDatePicker: { + recentlyUsedRanges, + refreshInterval, + isPaused, + start, + end, + }, + queryText, + statusFilters, + showMetricColumns, + pagination: { + pageIndex, + pageSize, + }, + sort: { + sortField, + sortDirection, + }, + }, + actions: { + setEnd, + setIsPaused, + setPageIndex, + setPageSize, + setQueryText, + setRecentlyUsedRanges, + setRefreshInterval, + setShowMetricColumns, + setSortDirection, + setSortField, + setStart, + setStatusFilters, + }, + }, + }), + [ + end, + isPaused, + pageIndex, + pageSize, + queryText, + recentlyUsedRanges, + refreshInterval, + showMetricColumns, + sortDirection, + sortField, + start, + statusFilters, + ] + ); + + return ( + {children} + ); +}; + +export const useRuleDetailsContext = (): RuleDetailsContextType => { + const ruleDetailsContext = useContext(RuleDetailsContext); + invariant( + ruleDetailsContext, + 'useRuleDetailsContext should be used inside RuleDetailsContextProvider' + ); + return ruleDetailsContext; +}; + +export const useRuleDetailsContextOptional = (): RuleDetailsContextType | null => + useContext(RuleDetailsContext); diff --git a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx index a52c4e15608932..32ba03abe500b6 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx @@ -106,6 +106,7 @@ jest.mock('../../common/lib/kibana', () => { addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), }), }; }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx index dd032016088b69..ee4af121dd0543 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -35,6 +35,7 @@ jest.mock('../../common/lib/kibana', () => ({ addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), }), useKibana: jest.fn().mockReturnValue({ services: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index e7f539d31ec0cf..9f4670e3c252a5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -577,7 +577,7 @@ export const getAggregateExecutionEvents = (): GetAggregateRuleExecutionEventsRe timed_out: false, indexing_duration_ms: 7, search_duration_ms: 551, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -600,7 +600,7 @@ export const getAggregateExecutionEvents = (): GetAggregateRuleExecutionEventsRe timed_out: false, indexing_duration_ms: 0, search_duration_ms: 0, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'partial failure', security_message: 'Check privileges failed to execute ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [broken-index] name: "This Rule Makes Alerts, Actions, AND Moar!" id: "f78f3550-a186-11ec-89a1-0bce95157aba" rule id: "b64b4540-d035-4826-a1e7-f505bf4b9653" execution id: "254d8400-9dc7-43c5-ad4b-227273d1a44b" space ID: "default"', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts index 37b1f413286743..c59a0e4dfe1760 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts @@ -12,8 +12,6 @@ import { } from '../__mocks__/request_responses'; import { getRuleExecutionEventsRoute } from './get_rule_execution_events_route'; -// TODO: Add additional tests for param validation - describe('getRuleExecutionEventsRoute', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client_interface.ts index 8d0cae91f19879..3f353732abbc5f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client_interface.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client_interface.ts @@ -9,7 +9,7 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ExecutionLogTableSortColumns, RuleExecutionEvent, - RuleExecutionStatusType, + RuleExecutionStatus, RuleExecutionSummary, } from '../../../../../common/detection_engine/schemas/common'; import { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; @@ -19,7 +19,7 @@ export interface GetAggregateExecutionEventsArgs { start: string; end: string; queryText: string; - statusFilters: RuleExecutionStatusType[]; + statusFilters: RuleExecutionStatus[]; page: number; perPage: number; sortField: ExecutionLogTableSortColumns; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_reader.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_reader.ts index 78e8e62702d47a..20cc2f0e78c77b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_reader.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_reader.ts @@ -6,8 +6,8 @@ */ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; import { IEventLogClient } from '@kbn/event-log-plugin/server'; +import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; import { RuleExecutionEvent, @@ -18,13 +18,14 @@ import { invariant } from '../../../../../common/utils/invariant'; import { withSecuritySpan } from '../../../../utils/with_security_span'; import { GetAggregateExecutionEventsArgs } from '../client_for_routes/client_interface'; import { - RULE_SAVED_OBJECT_TYPE, RULE_EXECUTION_LOG_PROVIDER, + RULE_SAVED_OBJECT_TYPE, RuleExecutionLogAction, } from './constants'; import { formatExecutionEventResponse, getExecutionEventAggregation, + mapRuleExecutionStatusToPlatformStatus, } from './get_execution_event_aggregation'; import { EXECUTION_UUID_FIELD, @@ -62,10 +63,15 @@ export const createEventLogReader = (eventLog: IEventLogClient): IEventLogReader let totalExecutions: number | undefined; // If 0 or 3 statuses are selected we can search for all statuses and don't need this pre-filter by ID if (statusFilters.length > 0 && statusFilters.length < 3) { + const outcomes = mapRuleExecutionStatusToPlatformStatus(statusFilters); + const outcomeFilter = outcomes.length ? `OR event.outcome:(${outcomes.join(' OR ')})` : ''; const statusResults = await eventLog.aggregateEventsBySavedObjectIds(soType, soIds, { start, end, - filter: `kibana.alert.rule.execution.status:(${statusFilters.join(' OR ')})`, + // Also query for `event.outcome` to catch executions that only contain platform events + filter: `kibana.alert.rule.execution.status:(${statusFilters.join( + ' OR ' + )}) ${outcomeFilter}`, aggs: { totalExecutions: { cardinality: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts index 7cee84cd64594a..dcd592d7a70fc1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts @@ -12,6 +12,7 @@ * 2.0. */ +import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common'; import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; import { @@ -20,6 +21,8 @@ import { formatSortForTermsSort, getExecutionEventAggregation, getProviderAndActionFilter, + mapPlatformStatusToRuleExecutionStatus, + mapRuleExecutionStatusToPlatformStatus, } from '.'; describe('getExecutionEventAggregation', () => { @@ -69,7 +72,7 @@ describe('getExecutionEventAggregation', () => { sort: [{ notsortable: { order: 'asc' } }], }); }).toThrowErrorMatchingInlineSnapshot( - `"Invalid sort field \\"notsortable\\" - must be one of [timestamp,duration_ms,indexing_duration_ms,search_duration_ms,gap_duration_ms,schedule_delay_ms,num_triggered_actions]"` + `"Invalid sort field \\"notsortable\\" - must be one of [timestamp,duration_ms,indexing_duration_ms,search_duration_ms,gap_duration_s,schedule_delay_ms,num_triggered_actions]"` ); }); @@ -82,7 +85,7 @@ describe('getExecutionEventAggregation', () => { sort: [{ notsortable: { order: 'asc' } }, { timestamp: { order: 'asc' } }], }); }).toThrowErrorMatchingInlineSnapshot( - `"Invalid sort field \\"notsortable\\" - must be one of [timestamp,duration_ms,indexing_duration_ms,search_duration_ms,gap_duration_ms,schedule_delay_ms,num_triggered_actions]"` + `"Invalid sort field \\"notsortable\\" - must be one of [timestamp,duration_ms,indexing_duration_ms,search_duration_ms,gap_duration_s,schedule_delay_ms,num_triggered_actions]"` ); }); @@ -206,7 +209,7 @@ describe('getExecutionEventAggregation', () => { top_hits: { size: 1, _source: { - includes: ['event.outcome', 'message'], + includes: ['error.message', 'event.outcome', 'message'], }, }, }, @@ -647,7 +650,7 @@ describe('formatExecutionEventResponse', () => { timed_out: false, indexing_duration_ms: 7, search_duration_ms: 480, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -670,7 +673,7 @@ describe('formatExecutionEventResponse', () => { timed_out: false, indexing_duration_ms: 0, search_duration_ms: 9, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -965,7 +968,7 @@ describe('formatExecutionEventResponse', () => { timed_out: true, indexing_duration_ms: 7, search_duration_ms: 480, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -988,7 +991,7 @@ describe('formatExecutionEventResponse', () => { timed_out: false, indexing_duration_ms: 0, search_duration_ms: 9, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -1288,7 +1291,7 @@ describe('formatExecutionEventResponse', () => { timed_out: true, indexing_duration_ms: 7, search_duration_ms: 480, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -1311,7 +1314,7 @@ describe('formatExecutionEventResponse', () => { timed_out: false, indexing_duration_ms: 0, search_duration_ms: 9, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -1319,3 +1322,53 @@ describe('formatExecutionEventResponse', () => { }); }); }); + +describe('mapRuleStatusToPlatformStatus', () => { + test('should correctly translate empty array to empty array', () => { + expect(mapRuleExecutionStatusToPlatformStatus([])).toEqual([]); + }); + + test('should correctly translate RuleExecutionStatus.failed to `failure` platform status', () => { + expect(mapRuleExecutionStatusToPlatformStatus([RuleExecutionStatus.failed])).toEqual([ + 'failure', + ]); + }); + + test('should correctly translate RuleExecutionStatus.succeeded to `success` platform status', () => { + expect(mapRuleExecutionStatusToPlatformStatus([RuleExecutionStatus.succeeded])).toEqual([ + 'success', + ]); + }); + + test('should correctly translate RuleExecutionStatus.["going to run"] to empty array platform status', () => { + expect(mapRuleExecutionStatusToPlatformStatus([RuleExecutionStatus['going to run']])).toEqual( + [] + ); + }); + + test("should correctly translate multiple RuleExecutionStatus's to platform statuses", () => { + expect( + mapRuleExecutionStatusToPlatformStatus([ + RuleExecutionStatus.succeeded, + RuleExecutionStatus.failed, + RuleExecutionStatus['going to run'], + ]).sort() + ).toEqual(['failure', 'success']); + }); +}); + +describe('mapPlatformStatusToRuleExecutionStatus', () => { + test('should correctly translate `invalid` platform status to `undefined`', () => { + expect(mapPlatformStatusToRuleExecutionStatus('')).toEqual(undefined); + }); + + test('should correctly translate `failure` platform status to `RuleExecutionStatus.failed`', () => { + expect(mapPlatformStatusToRuleExecutionStatus('failure')).toEqual(RuleExecutionStatus.failed); + }); + + test('should correctly translate `success` platform status to `RuleExecutionStatus.succeeded`', () => { + expect(mapPlatformStatusToRuleExecutionStatus('success')).toEqual( + RuleExecutionStatus.succeeded + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.ts index 4cf8e0bd5f1dc6..dcf2fbfe911bd6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.ts @@ -10,7 +10,10 @@ import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { flatMap, get } from 'lodash'; import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; import { AggregateEventsBySavedObjectResult } from '@kbn/event-log-plugin/server'; -import { AggregateRuleExecutionEvent } from '../../../../../../common/detection_engine/schemas/common'; +import { + AggregateRuleExecutionEvent, + RuleExecutionStatus, +} from '../../../../../../common/detection_engine/schemas/common'; import { GetAggregateRuleExecutionEventsResponse } from '../../../../../../common/detection_engine/schemas/response'; import { ExecutionEventAggregationOptions, @@ -22,6 +25,7 @@ import { // Base ECS fields const ACTION_FIELD = 'event.action'; const DURATION_FIELD = 'event.duration'; +const ERROR_MESSAGE_FIELD = 'error.message'; const MESSAGE_FIELD = 'message'; const PROVIDER_FIELD = 'event.provider'; const OUTCOME_FIELD = 'event.outcome'; @@ -48,7 +52,7 @@ const SORT_FIELD_TO_AGG_MAPPING: Record = { duration_ms: 'ruleExecution>executionDuration', indexing_duration_ms: 'securityMetrics>indexDuration', search_duration_ms: 'securityMetrics>searchDuration', - gap_duration_ms: 'securityMetrics>gapDuration', + gap_duration_s: 'securityMetrics>gapDuration', schedule_delay_ms: 'ruleExecution>scheduleDelay', num_triggered_actions: 'ruleExecution>numTriggeredActions', // TODO: To be added in https://github.com/elastic/kibana/pull/126210 @@ -166,7 +170,7 @@ export const getExecutionEventAggregation = ({ top_hits: { size: 1, _source: { - includes: [OUTCOME_FIELD, MESSAGE_FIELD], + includes: [ERROR_MESSAGE_FIELD, OUTCOME_FIELD, MESSAGE_FIELD], }, }, }, @@ -293,11 +297,18 @@ export const formatAggExecutionEventFromBucket = ( // security fields indexing_duration_ms: bucket?.securityMetrics?.indexDuration?.value ?? 0, search_duration_ms: bucket?.securityMetrics?.searchDuration?.value ?? 0, - gap_duration_ms: bucket?.securityMetrics?.gapDuration?.value ?? 0, + gap_duration_s: bucket?.securityMetrics?.gapDuration?.value ?? 0, + // If security_status isn't available, use platform status from `event.outcome`, but translate to RuleExecutionStatus security_status: bucket?.securityStatus?.status?.hits?.hits[0]?._source?.kibana?.alert?.rule?.execution - ?.status, - security_message: bucket?.securityStatus?.message?.hits?.hits[0]?._source?.message, + ?.status ?? + mapPlatformStatusToRuleExecutionStatus( + bucket?.ruleExecution?.outcomeAndMessage?.hits?.hits[0]?._source?.event?.outcome + ), + // If security_message isn't available, use `error.message` instead for platform errors since it is more descriptive than `message` + security_message: + bucket?.securityStatus?.message?.hits?.hits[0]?._source?.message ?? + bucket?.ruleExecution?.outcomeAndMessage?.hits?.hits[0]?._source?.error?.message, }; }; @@ -353,3 +364,40 @@ export const formatSortForTermsSort = (sort: estypes.Sort) => { ) ); }; + +/** + * Maps a RuleExecutionStatus[] to string[] of associated platform statuses. Useful for querying specific platform + * events based on security status values + * @param ruleStatuses RuleExecutionStatus[] + */ +export const mapRuleExecutionStatusToPlatformStatus = ( + ruleStatuses: RuleExecutionStatus[] +): string[] => { + return flatMap(ruleStatuses, (rs) => { + switch (rs) { + case RuleExecutionStatus.failed: + return 'failure'; + case RuleExecutionStatus.succeeded: + return 'success'; + default: + return []; + } + }); +}; + +/** + * Maps a platform status string to RuleExecutionStatus + * @param platformStatus string, i.e. `failure` or `success` + */ +export const mapPlatformStatusToRuleExecutionStatus = ( + platformStatus: string +): RuleExecutionStatus | undefined => { + switch (platformStatus) { + case 'failure': + return RuleExecutionStatus.failed; + case 'success': + return RuleExecutionStatus.succeeded; + default: + return undefined; + } +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_rule_execution_events.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_rule_execution_events.ts index d3bddc8b0d46c3..9fadbd39f77047 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_rule_execution_events.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_rule_execution_events.ts @@ -52,11 +52,6 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllEventLogExecutionEvents(es, log); }); - afterEach(async () => { - await deleteAllAlerts(supertest, log); - await deleteAllEventLogExecutionEvents(es, log); - }); - it('should return an error if rule does not exist', async () => { const start = dateMath.parse('now-24h')?.utc().toISOString(); const end = dateMath.parse('now', { roundUp: true })?.utc().toISOString(); @@ -90,7 +85,7 @@ export default ({ getService }: FtrProviderContext) => { expect(response.body.events[0].search_duration_ms).to.greaterThan(0); expect(response.body.events[0].schedule_delay_ms).to.greaterThan(0); expect(response.body.events[0].indexing_duration_ms).to.greaterThan(0); - expect(response.body.events[0].gap_duration_ms).to.eql(0); + expect(response.body.events[0].gap_duration_s).to.eql(0); expect(response.body.events[0].security_status).to.eql('succeeded'); expect(response.body.events[0].security_message).to.eql('succeeded'); }); @@ -114,7 +109,7 @@ export default ({ getService }: FtrProviderContext) => { expect(response.body.events[0].search_duration_ms).to.eql(0); expect(response.body.events[0].schedule_delay_ms).to.greaterThan(0); expect(response.body.events[0].indexing_duration_ms).to.eql(0); - expect(response.body.events[0].gap_duration_ms).to.eql(0); + expect(response.body.events[0].gap_duration_s).to.eql(0); expect(response.body.events[0].security_status).to.eql('partial failure'); expect( response.body.events[0].security_message.startsWith( @@ -123,16 +118,15 @@ export default ({ getService }: FtrProviderContext) => { ).to.eql(true); }); - // TODO: Debug indexing - it.skip('should return execution events for a rule that has executed in a failure state with a gap', async () => { + it('should return execution events for a rule that has executed in a failure state with a gap', async () => { const rule = getRuleForSignalTesting(['auditbeat-*'], uuid.v4(), false); const { id } = await createRule(supertest, log, rule); const start = dateMath.parse('now')?.utc().toISOString(); const end = dateMath.parse('now+24h', { roundUp: true })?.utc().toISOString(); - // Create 5 timestamps a minute apart to use in the templated data - const dateTimes = [...Array(5).keys()].map((i) => + // Create 5 timestamps (failedGapExecution.length) a minute apart to use in the templated data + const dateTimes = [...Array(failedGapExecution.length).keys()].map((i) => moment(start) .add(i + 1, 'm') .toDate() @@ -144,6 +138,7 @@ export default ({ getService }: FtrProviderContext) => { set(e, 'event.start', dateTimes[i]); set(e, 'event.end', dateTimes[i]); set(e, 'rule.id', id); + set(e, 'kibana.saved_objects[0].id', id); return e; }); @@ -155,73 +150,19 @@ export default ({ getService }: FtrProviderContext) => { .set('kbn-xsrf', 'true') .query({ start, end }); - // console.log(JSON.stringify(response)); - expect(response.status).to.eql(200); expect(response.body.total).to.eql(1); - expect(response.body.events[0].duration_ms).to.eql(4236); + expect(response.body.events[0].duration_ms).to.eql(1545); expect(response.body.events[0].search_duration_ms).to.eql(0); - expect(response.body.events[0].schedule_delay_ms).to.greaterThan(0); + expect(response.body.events[0].schedule_delay_ms).to.eql(544808); expect(response.body.events[0].indexing_duration_ms).to.eql(0); - expect(response.body.events[0].gap_duration_ms).to.greaterThan(0); + expect(response.body.events[0].gap_duration_s).to.eql(245); expect(response.body.events[0].security_status).to.eql('failed'); expect( response.body.events[0].security_message.startsWith( - 'Check privileges failed to execute ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [no-name-index]' + '4 minutes (244689ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances.' ) ).to.eql(true); }); - - // it('should return execution events when providing a status filter', async () => { - // const rule = getRuleForSignalTesting(['auditbeat-*', 'no-name-index']); - // const { id } = await createRule(supertest, log, rule); - // await waitForRuleSuccessOrStatus(supertest, log, id, RuleExecutionStatus.failed); - // await waitForSignalsToBePresent(supertest, log, 1, [id]); - // - // const start = dateMath.parse('now-24h')?.utc().toISOString(); - // const end = dateMath.parse('now', { roundUp: true })?.utc().toISOString(); - // const response = await supertest - // .get(detectionEngineRuleExecutionEventsUrl(id)) - // .set('kbn-xsrf', 'true') - // .query({ start, end }); - // - // expect(response.status).to.eql(200); - // expect(response.body.total).to.eql(1); - // expect(response.body.events[0].duration_ms).to.greaterThan(0); - // expect(response.body.events[0].search_duration_ms).to.eql(0); - // expect(response.body.events[0].schedule_delay_ms).to.greaterThan(0); - // expect(response.body.events[0].indexing_duration_ms).to.eql(0); - // expect(response.body.events[0].gap_duration_ms).to.eql(0); - // expect(response.body.events[0].security_status).to.eql('failed'); - // expect(response.body.events[0].security_message).to.include( - // 'were not queried between this rule execution and the last execution, so signals may have been missed. ' - // ); - // }); - - // it('should return execution events when providing a status filter and sortField', async () => { - // const rule = getRuleForSignalTesting(['auditbeat-*', 'no-name-index']); - // const { id } = await createRule(supertest, log, rule); - // await waitForRuleSuccessOrStatus(supertest, log, id, RuleExecutionStatus.failed); - // await waitForSignalsToBePresent(supertest, log, 1, [id]); - // - // const start = dateMath.parse('now-24h')?.utc().toISOString(); - // const end = dateMath.parse('now', { roundUp: true })?.utc().toISOString(); - // const response = await supertest - // .get(detectionEngineRuleExecutionEventsUrl(id)) - // .set('kbn-xsrf', 'true') - // .query({ start, end }); - // - // expect(response.status).to.eql(200); - // expect(response.body.total).to.eql(1); - // expect(response.body.events[0].duration_ms).to.greaterThan(0); - // expect(response.body.events[0].search_duration_ms).to.eql(0); - // expect(response.body.events[0].schedule_delay_ms).to.greaterThan(0); - // expect(response.body.events[0].indexing_duration_ms).to.eql(0); - // expect(response.body.events[0].gap_duration_ms).to.eql(0); - // expect(response.body.events[0].security_status).to.eql('failed'); - // expect(response.body.events[0].security_message).to.include( - // 'were not queried between this rule execution and the last execution, so signals may have been missed. ' - // ); - // }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/template_data/execution_events.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/template_data/execution_events.ts index c4767bbcc5632a..4a674c52fa1edc 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/template_data/execution_events.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/template_data/execution_events.ts @@ -5,6 +5,20 @@ * 2.0. */ +/** + * When using these execution events as templates be sure to replace all the following fields with their updated values + * + * E.g. + * set(e, '@timestamp', dateTimes[i]); + * set(e, 'event.start', dateTimes[i]); + * set(e, 'event.end', dateTimes[i]); + * set(e, 'rule.id', id); + * set(e, 'kibana.saved_objects[0].id', id); + */ + +/** + * Rule executed without issue + */ export const successfulExecution = [ { '@timestamp': '2022-03-17T22:59:31.360Z', @@ -226,30 +240,24 @@ export const successfulExecution = [ }, ]; +/** + * Rule execution identified gap since last execution + */ export const failedGapExecution = [ { - '@timestamp': '2022-03-17T12:36:16.413Z', + '@timestamp': '2022-03-17T12:36:14.868Z', event: { provider: 'alerting', - action: 'execute', + action: 'execute-start', kind: 'alert', category: ['siem'], start: '2022-03-17T12:36:14.868Z', - outcome: 'success', - end: '2022-03-17T12:36:16.413Z', - duration: 1545000000, }, kibana: { alert: { rule: { execution: { uuid: '38fa2d4a-94d3-4ea3-80d6-d1284eb98357', - metrics: { - number_of_triggered_actions: 0, - number_of_searches: 6, - es_search_duration_ms: 2, - total_search_duration_ms: 15, - }, }, }, }, @@ -265,9 +273,6 @@ export const failedGapExecution = [ scheduled: '2022-03-17T12:27:10.060Z', schedule_delay: 544808000000, }, - alerting: { - status: 'ok', - }, server_uuid: '5b2de169-2785-441b-ae8c-186a1936b17d', version: '8.2.0', }, @@ -276,22 +281,21 @@ export const failedGapExecution = [ license: 'basic', category: 'siem.queryRule', ruleset: 'siem', - name: 'Lots of Execution Events', }, - message: - "rule executed: siem.queryRule:fb1fc150-a292-11ec-a2cf-c1b28b0392b0: 'Lots of Execution Events'", + message: 'rule execution start: "fb1fc150-a292-11ec-a2cf-c1b28b0392b0"', ecs: { version: '1.8.0', }, }, { - '@timestamp': '2022-03-17T12:36:15.382Z', + '@timestamp': '2022-03-17T12:36:14.888Z', event: { provider: 'securitySolution.ruleExecution', - kind: 'metric', - action: 'execution-metrics', - sequence: 1, + kind: 'event', + action: 'status-change', + sequence: 0, }, + message: '', rule: { id: 'fb1fc150-a292-11ec-a2cf-c1b28b0392b0', name: 'Lots of Execution Events', @@ -301,9 +305,8 @@ export const failedGapExecution = [ alert: { rule: { execution: { - metrics: { - execution_gap_duration_s: 245, - }, + status: 'running', + status_order: 15, uuid: '38fa2d4a-94d3-4ea3-80d6-d1284eb98357', }, }, @@ -364,14 +367,13 @@ export const failedGapExecution = [ }, }, { - '@timestamp': '2022-03-17T12:36:14.888Z', + '@timestamp': '2022-03-17T12:36:15.382Z', event: { provider: 'securitySolution.ruleExecution', - kind: 'event', - action: 'status-change', - sequence: 0, + kind: 'metric', + action: 'execution-metrics', + sequence: 1, }, - message: '', rule: { id: 'fb1fc150-a292-11ec-a2cf-c1b28b0392b0', name: 'Lots of Execution Events', @@ -381,8 +383,9 @@ export const failedGapExecution = [ alert: { rule: { execution: { - status: 'running', - status_order: 15, + metrics: { + execution_gap_duration_s: 245, + }, uuid: '38fa2d4a-94d3-4ea3-80d6-d1284eb98357', }, }, @@ -403,19 +406,28 @@ export const failedGapExecution = [ }, }, { - '@timestamp': '2022-03-17T12:36:14.868Z', + '@timestamp': '2022-03-17T12:36:16.413Z', event: { provider: 'alerting', - action: 'execute-start', + action: 'execute', kind: 'alert', category: ['siem'], start: '2022-03-17T12:36:14.868Z', + outcome: 'success', + end: '2022-03-17T12:36:16.413Z', + duration: 1545000000, }, kibana: { alert: { rule: { execution: { uuid: '38fa2d4a-94d3-4ea3-80d6-d1284eb98357', + metrics: { + number_of_triggered_actions: 0, + number_of_searches: 6, + es_search_duration_ms: 2, + total_search_duration_ms: 15, + }, }, }, }, @@ -431,6 +443,9 @@ export const failedGapExecution = [ scheduled: '2022-03-17T12:27:10.060Z', schedule_delay: 544808000000, }, + alerting: { + status: 'ok', + }, server_uuid: '5b2de169-2785-441b-ae8c-186a1936b17d', version: '8.2.0', }, @@ -439,14 +454,19 @@ export const failedGapExecution = [ license: 'basic', category: 'siem.queryRule', ruleset: 'siem', + name: 'Lots of Execution Events', }, - message: 'rule execution start: "fb1fc150-a292-11ec-a2cf-c1b28b0392b0"', + message: + "rule executed: siem.queryRule:fb1fc150-a292-11ec-a2cf-c1b28b0392b0: 'Lots of Execution Events'", ecs: { version: '1.8.0', }, }, ]; +/** + * Rule execution resulted in partial warning, e.g. missing index pattern + */ export const partialWarningExecution = [ { '@timestamp': '2022-03-16T23:28:36.012Z', @@ -628,3 +648,112 @@ export const partialWarningExecution = [ }, }, ]; + +/** + * Rule execution failed because rule is disabled (configure 1s interval/lookback then rule + * is disabled while running) + */ +export const failedRanAfterDisabled = [ + { + '@timestamp': '2022-04-21T02:00:55.400Z', + event: { + provider: 'alerting', + action: 'execute', + kind: 'alert', + category: ['siem'], + start: '2022-04-21T02:00:55.397Z', + end: '2022-04-21T02:00:55.400Z', + duration: 3000000, + reason: 'disabled', + outcome: 'failure', + }, + kibana: { + alert: { + rule: { + rule_type_id: 'siem.queryRule', + consumer: 'siem', + execution: { + uuid: '50eb8b2e-8334-4387-b77f-d47fdb7fbe2d', + }, + }, + }, + saved_objects: [ + { + rel: 'primary', + type: 'alert', + id: 'a890e240-b9fb-11ec-8598-338317271cf4', + type_id: 'siem.queryRule', + }, + ], + space_ids: ['default'], + task: { + scheduled: '2022-04-21T02:00:53.325Z', + schedule_delay: 2072000000, + }, + alerting: { + status: 'error', + }, + server_uuid: '5b2de169-2785-441b-ae8c-186a1936b17d', + version: '8.3.0', + }, + rule: { + id: 'a890e240-b9fb-11ec-8598-338317271cf4', + license: 'basic', + category: 'siem.queryRule', + ruleset: 'siem', + }, + error: { + message: 'Rule failed to execute because rule ran after it was disabled.', + }, + message: 'siem.queryRule:a890e240-b9fb-11ec-8598-338317271cf4: execution failed', + ecs: { + version: '1.8.0', + }, + }, + { + '@timestamp': '2022-04-21T02:00:55.397Z', + event: { + provider: 'alerting', + action: 'execute-start', + kind: 'alert', + category: ['siem'], + start: '2022-04-21T02:00:55.397Z', + }, + kibana: { + alert: { + rule: { + rule_type_id: 'siem.queryRule', + consumer: 'siem', + execution: { + uuid: '50eb8b2e-8334-4387-b77f-d47fdb7fbe2d', + }, + }, + }, + saved_objects: [ + { + rel: 'primary', + type: 'alert', + id: 'a890e240-b9fb-11ec-8598-338317271cf4', + type_id: 'siem.queryRule', + }, + ], + space_ids: ['default'], + task: { + scheduled: '2022-04-21T02:00:53.325Z', + schedule_delay: 2072000000, + }, + server_uuid: '5b2de169-2785-441b-ae8c-186a1936b17d', + version: '8.3.0', + }, + rule: { + id: 'a890e240-b9fb-11ec-8598-338317271cf4', + license: 'basic', + category: 'siem.queryRule', + ruleset: 'siem', + }, + message: 'rule execution start: "a890e240-b9fb-11ec-8598-338317271cf4"', + ecs: { + version: '1.8.0', + }, + }, +]; diff --git a/x-pack/test/detection_engine_api_integration/utils/index_event_log_execution_events.ts b/x-pack/test/detection_engine_api_integration/utils/index_event_log_execution_events.ts index 63f8c9f2c8e1b8..8e354607d40023 100644 --- a/x-pack/test/detection_engine_api_integration/utils/index_event_log_execution_events.ts +++ b/x-pack/test/detection_engine_api_integration/utils/index_event_log_execution_events.ts @@ -19,8 +19,9 @@ export const indexEventLogExecutionEvents = async ( log: ToolingLog, events: object[] ): Promise => { + const aliases = await es.cat.aliases({ format: 'json', name: '.kibana-event-log-*' }); const operations = events.flatMap((doc: object) => [ - { index: { _index: '.kibana-event-log-*' } }, + { index: { _index: aliases[0].index } }, doc, ]); From 490bee238bea4f9ebddcedf9bc9272be4db0d3fc Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 4 May 2022 13:43:54 -0600 Subject: [PATCH 03/83] [ML] Anomaly Detection: Adds View in Maps item to Actions menu in the anomalies table (#131284) * add link to actions menu in anomalies table * set map start time to bucket start * simplify isGeoRecord value setting * lint fix * adds query and timerange to link * substract ms so as not to go into next bucket start time * lint fix --- x-pack/plugins/ml/common/types/anomalies.ts | 5 + .../anomalies_table_columns.js | 4 +- .../components/anomalies_table/links_menu.tsx | 94 +++++++++++++++---- .../explorer_charts_container.js | 61 +----------- .../application/explorer/explorer_utils.ts | 3 + .../maps/anomaly_layer_wizard_factory.tsx | 15 +-- x-pack/plugins/ml/public/maps/util.ts | 77 +++++++++++++++ 7 files changed, 168 insertions(+), 91 deletions(-) diff --git a/x-pack/plugins/ml/common/types/anomalies.ts b/x-pack/plugins/ml/common/types/anomalies.ts index 58d5e9df130afb..9b6218e8c3f347 100644 --- a/x-pack/plugins/ml/common/types/anomalies.ts +++ b/x-pack/plugins/ml/common/types/anomalies.ts @@ -278,6 +278,11 @@ export interface AnomaliesTableRecord { * which can be plotted by the ML UI in an anomaly chart. */ isTimeSeriesViewRecord?: boolean; + + /** + * Returns true if the anomaly record represented by the table row can be shown in the maps plugin + */ + isGeoRecord?: boolean; } export type PartitionFieldsType = typeof PARTITION_FIELDS[number]; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index 9e84dccf12b285..c24e294b54a5d2 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -18,6 +18,7 @@ import { formatHumanReadableDateTime, formatHumanReadableDateTimeSeconds, } from '../../../../common/util/date_utils'; +import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { DescriptionCell } from './description_cell'; import { DetectorCell } from './detector_cell'; @@ -47,7 +48,8 @@ function showLinksMenuForItem(item, showViewSeriesLink) { canConfigureRules || (showViewSeriesLink && item.isTimeSeriesViewRecord) || item.entityName === 'mlcategory' || - item.customUrls !== undefined + item.customUrls !== undefined || + item.detector.includes(ML_JOB_AGGREGATION.LAT_LONG) ); } diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx index cb299ee9dba33c..ac67dbe35d9ec4 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx @@ -8,7 +8,9 @@ import { cloneDeep } from 'lodash'; import moment from 'moment'; import rison, { RisonValue } from 'rison-node'; +import { escapeKuery } from '@kbn/es-query'; import React, { FC, useEffect, useMemo, useState } from 'react'; +import { APP_ID as MAPS_APP_ID } from '@kbn/maps-plugin/common'; import { EuiButtonIcon, EuiContextMenuItem, @@ -20,8 +22,10 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { ES_FIELD_TYPES } from '@kbn/field-types'; +import { MAPS_APP_LOCATOR } from '@kbn/maps-plugin/public'; import { mlJobService } from '../../services/job_service'; import { getDataViewIdFromName } from '../../util/index_utils'; +import { getInitialAnomaliesLayers } from '../../../maps/util'; import { formatHumanReadableDateTimeSeconds, timeFormatter, @@ -33,7 +37,7 @@ import { getUrlForRecord, openCustomUrlWindow } from '../../util/custom_url_util import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; // @ts-ignore -import { escapeDoubleQuotes } from '../../explorer/explorer_utils'; +import { escapeDoubleQuotes, getDateFormatTz } from '../../explorer/explorer_utils'; import { isCategorizationAnomaly, isRuleSupported } from '../../../../common/util/anomaly_utils'; import { checkPermission } from '../../capabilities/check_capabilities'; import type { @@ -49,6 +53,7 @@ import type { AnomaliesTableRecord } from '../../../../common/types/anomalies'; interface LinksMenuProps { anomaly: AnomaliesTableRecord; bounds: TimeRangeBounds; + showMapsLink: boolean; showViewSeriesLink: boolean; isAggregatedData: boolean; interval: 'day' | 'hour' | 'second'; @@ -66,9 +71,39 @@ export const LinksMenuUI = (props: LinksMenuProps) => { const kibana = useMlKibana(); const { - services: { share, application }, + services: { data, share, application }, } = kibana; + const getMapsLink = async (anomaly: AnomaliesTableRecord) => { + const initialLayers = getInitialAnomaliesLayers(anomaly.jobId); + const anomalyBucketStartMoment = moment(anomaly.time).tz(getDateFormatTz()); + const anomalyBucketStart = anomalyBucketStartMoment.toISOString(); + const anomalyBucketEnd = anomalyBucketStartMoment + .add(anomaly.source.bucket_span, 'seconds') + .subtract(1, 'ms') + .toISOString(); + const timeRange = data.query.timefilter.timefilter.getTime(); + + // Set 'from' in timeRange to start bucket time for the specific anomaly + timeRange.from = anomalyBucketStart; + timeRange.to = anomalyBucketEnd; + + const locator = share.url.locators.get(MAPS_APP_LOCATOR); + const location = await locator?.getLocation({ + initialLayers, + timeRange, + ...(anomaly.entityName && anomaly.entityValue + ? { + query: { + language: SEARCH_QUERY_LANGUAGE.KUERY, + query: `${escapeKuery(anomaly.entityName)}:${escapeKuery(anomaly.entityValue)}`, + }, + } + : {}), + }); + return location; + }; + useEffect(() => { let unmounted = false; const discoverLocator = share.url.locators.get('DISCOVER_APP_LOCATOR'); @@ -561,23 +596,44 @@ export const LinksMenuUI = (props: LinksMenuProps) => { ); } - if (showViewSeriesLink === true && anomaly.isTimeSeriesViewRecord === true) { - items.push( - { - closePopover(); - viewSeries(); - }} - data-test-subj="mlAnomaliesListRowActionViewSeriesButton" - > - - - ); + if (showViewSeriesLink === true) { + if (anomaly.isTimeSeriesViewRecord) { + items.push( + { + closePopover(); + viewSeries(); + }} + data-test-subj="mlAnomaliesListRowActionViewSeriesButton" + > + + + ); + } + + if (anomaly.isGeoRecord === true) { + items.push( + { + const mapsLink = await getMapsLink(anomaly); + await application.navigateToApp(MAPS_APP_ID, { path: mapsLink?.path }); + }} + data-test-subj="mlAnomaliesListRowActionViewInMapsButton" + > + + + ); + } } if (application.capabilities.discover?.show && isCategorizationAnomalyRecord) { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index ddaaa55e7a3a46..d3dc2681cf4c51 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -16,7 +16,6 @@ import { EuiFlexItem, EuiIconTip, EuiToolTip, - htmlIdGenerator, } from '@elastic/eui'; import { @@ -36,15 +35,13 @@ import { MlTooltipComponent } from '../../components/chart_tooltip'; import { withKibana } from '@kbn/kibana-react-plugin/public'; import { useMlKibana } from '../../contexts/kibana'; import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; -import { AnomalySource } from '../../../maps/anomaly_source'; -import { CUSTOM_COLOR_RAMP } from '../../../maps/anomaly_layer_wizard_factory'; -import { LAYER_TYPE, APP_ID as MAPS_APP_ID } from '@kbn/maps-plugin/common'; +import { getInitialAnomaliesLayers } from '../../../maps/util'; +import { APP_ID as MAPS_APP_ID } from '@kbn/maps-plugin/common'; import { MAPS_APP_LOCATOR } from '@kbn/maps-plugin/public'; import { ExplorerChartsErrorCallOuts } from './explorer_charts_error_callouts'; import { addItemToRecentlyAccessed } from '../../util/recently_accessed'; import { EmbeddedMapComponentWrapper } from './explorer_chart_embedded_map'; import { useActiveCursor } from '@kbn/charts-plugin/public'; -import { ML_ANOMALY_LAYERS } from '../../../maps/util'; import { Chart, Settings } from '@elastic/charts'; import useObservable from 'react-use/lib/useObservable'; @@ -114,59 +111,7 @@ function ExplorerChartContainer({ const getMapsLink = useCallback(async () => { const { queryString, query } = getEntitiesQuery(series); - const initialLayers = []; - const typicalStyle = { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#98A2B2', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }; - - const style = { - type: 'VECTOR', - properties: { - fillColor: CUSTOM_COLOR_RAMP, - lineColor: CUSTOM_COLOR_RAMP, - }, - isTimeAware: false, - }; - - for (const layer in ML_ANOMALY_LAYERS) { - if (ML_ANOMALY_LAYERS.hasOwnProperty(layer)) { - initialLayers.push({ - id: htmlIdGenerator()(), - type: LAYER_TYPE.GEOJSON_VECTOR, - sourceDescriptor: AnomalySource.createDescriptor({ - jobId: series.jobId, - typicalActual: ML_ANOMALY_LAYERS[layer], - }), - style: ML_ANOMALY_LAYERS[layer] === ML_ANOMALY_LAYERS.TYPICAL ? typicalStyle : style, - }); - } - } + const initialLayers = getInitialAnomaliesLayers(series.jobId); const locator = share.url.locators.get(MAPS_APP_LOCATOR); const location = await locator.getLocation({ diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts index d2c7c3f1fe2d22..6aa3444c9caaff 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts @@ -19,6 +19,7 @@ import { } from '../../../common/constants/search'; import { EntityField, getEntityFieldList } from '../../../common/util/anomaly_utils'; import { extractErrorMessage } from '../../../common/util/errors'; +import { ML_JOB_AGGREGATION } from '../../../common/constants/aggregation_types'; import { isSourceDataChartableForDetector, isModelPlotChartableForDetector, @@ -495,6 +496,8 @@ export async function loadAnomaliesTableData( } anomaly.isTimeSeriesViewRecord = isChartable; + anomaly.isGeoRecord = + detector !== undefined && detector.function === ML_JOB_AGGREGATION.LAT_LONG; if (mlJobService.customUrlsByJob[jobId] !== undefined) { anomaly.customUrls = mlJobService.customUrlsByJob[jobId]; diff --git a/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx b/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx index b40a08fadd128c..6b9ee6c2e63321 100644 --- a/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx +++ b/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx @@ -11,13 +11,13 @@ import type { StartServicesAccessor } from '@kbn/core/public'; import { LocatorPublic } from '@kbn/share-plugin/common'; import { SerializableRecord } from '@kbn/utility-types'; import type { LayerWizard, RenderWizardArguments } from '@kbn/maps-plugin/public'; -import { FIELD_ORIGIN, LAYER_TYPE, STYLE_TYPE } from '@kbn/maps-plugin/common'; +import { LAYER_TYPE } from '@kbn/maps-plugin/common'; import { VectorLayerDescriptor, VectorStylePropertiesDescriptor, } from '@kbn/maps-plugin/common/descriptor_types'; -import { SEVERITY_COLOR_RAMP } from '../../common'; import { ML_APP_LOCATOR, ML_PAGES } from '../../common/constants/locator'; +import { CUSTOM_COLOR_RAMP } from './util'; import { CreateAnomalySourceEditor } from './create_anomaly_source_editor'; import { AnomalySource, AnomalySourceDescriptor } from './anomaly_source'; @@ -26,17 +26,6 @@ import type { MlPluginStart, MlStartDependencies } from '../plugin'; import type { MlApiServices } from '../application/services/ml_api_service'; export const ML_ANOMALY = 'ML_ANOMALIES'; -export const CUSTOM_COLOR_RAMP = { - type: STYLE_TYPE.DYNAMIC, - options: { - customColorRamp: SEVERITY_COLOR_RAMP, - field: { - name: 'record_score', - origin: FIELD_ORIGIN.SOURCE, - }, - useCustomColorRamp: true, - }, -}; export class AnomalyLayerWizardFactory { public readonly type = ML_ANOMALY; diff --git a/x-pack/plugins/ml/public/maps/util.ts b/x-pack/plugins/ml/public/maps/util.ts index c1cb3f9efaeade..88e9994d303a93 100644 --- a/x-pack/plugins/ml/public/maps/util.ts +++ b/x-pack/plugins/ml/public/maps/util.ts @@ -7,14 +7,19 @@ import { FeatureCollection, Feature, Geometry } from 'geojson'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { htmlIdGenerator } from '@elastic/eui'; +import { FIELD_ORIGIN, STYLE_TYPE } from '@kbn/maps-plugin/common'; import { fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@kbn/es-query'; import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; import { VectorSourceRequestMeta } from '@kbn/maps-plugin/common'; +import { LAYER_TYPE } from '@kbn/maps-plugin/common'; +import { SEVERITY_COLOR_RAMP } from '../../common'; import { formatHumanReadableDateTimeSeconds } from '../../common/util/date_utils'; import type { MlApiServices } from '../application/services/ml_api_service'; import { MLAnomalyDoc } from '../../common/types/anomalies'; import { SEARCH_QUERY_LANGUAGE } from '../../common/constants/search'; import { getIndexPattern } from '../application/explorer/reducers/explorer_reducer/get_index_pattern'; +import { AnomalySource } from './anomaly_source'; export const ML_ANOMALY_LAYERS = { TYPICAL: 'typical', @@ -22,6 +27,57 @@ export const ML_ANOMALY_LAYERS = { TYPICAL_TO_ACTUAL: 'typical to actual', } as const; +export const CUSTOM_COLOR_RAMP = { + type: STYLE_TYPE.DYNAMIC, + options: { + customColorRamp: SEVERITY_COLOR_RAMP, + field: { + name: 'record_score', + origin: FIELD_ORIGIN.SOURCE, + }, + useCustomColorRamp: true, + }, +}; + +export const ACTUAL_STYLE = { + type: 'VECTOR', + properties: { + fillColor: CUSTOM_COLOR_RAMP, + lineColor: CUSTOM_COLOR_RAMP, + }, + isTimeAware: false, +}; + +export const TYPICAL_STYLE = { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#98A2B2', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, +}; + export type MlAnomalyLayersType = typeof ML_ANOMALY_LAYERS[keyof typeof ML_ANOMALY_LAYERS]; // Must reverse coordinates here. Map expects [lon, lat] - anomalies are stored as [lat, lon] for lat_lon jobs @@ -32,6 +88,27 @@ function getCoordinates(latLonString: string): number[] { .reverse(); } +export function getInitialAnomaliesLayers(jobId: string) { + const initialLayers = []; + for (const layer in ML_ANOMALY_LAYERS) { + if (ML_ANOMALY_LAYERS.hasOwnProperty(layer)) { + initialLayers.push({ + id: htmlIdGenerator()(), + type: LAYER_TYPE.GEOJSON_VECTOR, + sourceDescriptor: AnomalySource.createDescriptor({ + jobId, + typicalActual: ML_ANOMALY_LAYERS[layer as keyof typeof ML_ANOMALY_LAYERS], + }), + style: + ML_ANOMALY_LAYERS[layer as keyof typeof ML_ANOMALY_LAYERS] === ML_ANOMALY_LAYERS.TYPICAL + ? TYPICAL_STYLE + : ACTUAL_STYLE, + }); + } + } + return initialLayers; +} + export async function getResultsForJobId( mlResultsService: MlApiServices['results'], jobId: string, From 2d896d7c27c76d5646f17ae6b5e553cad73540f7 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 4 May 2022 12:45:45 -0700 Subject: [PATCH 04/83] [DOCS] Fix broken blob attributes (#131563) --- docs/index.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 668a6edcad3db1..41321f991c1b0c 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -11,7 +11,6 @@ include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] :docker-image: {docker-repo}:{version} :es-docker-repo: docker.elastic.co/elasticsearch/elasticsearch :es-docker-image: {es-docker-repo}:{version} -:blob: {kib-repo}blob/{branch}/ :security-ref: https://www.elastic.co/community/security/ :Data-source: Data view :data-source: data view @@ -20,6 +19,8 @@ include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] include::{docs-root}/shared/attributes.asciidoc[] +:blob: {kib-repo}blob/{branch}/ + include::user/index.asciidoc[] include::accessibility.asciidoc[] From 6f0e4fab800ad3295416a37047a826af46718777 Mon Sep 17 00:00:00 2001 From: Kellen <9484709+goodroot@users.noreply.github.com> Date: Wed, 4 May 2022 13:35:14 -0700 Subject: [PATCH 05/83] Update nav-kibana-dev.docnav.json (#131568) --- nav-kibana-dev.docnav.json | 1 - 1 file changed, 1 deletion(-) diff --git a/nav-kibana-dev.docnav.json b/nav-kibana-dev.docnav.json index 43b23fd084673c..4a1fd848a928eb 100644 --- a/nav-kibana-dev.docnav.json +++ b/nav-kibana-dev.docnav.json @@ -100,7 +100,6 @@ { "id": "kibCoreSavedObjectsPluginApi" }, { "id": "kibFieldFormatsPluginApi" }, { "id": "kibDataPluginApi" }, - { "id": "kibDataAutocompletePluginApi" }, { "id": "kibDataEnhancedPluginApi" }, { "id": "kibDataViewsPluginApi" }, { "id": "kibDataQueryPluginApi" }, From 42ec15bd0b6c1b41cc4fc012190e77670c2f9d6a Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 4 May 2022 13:50:05 -0700 Subject: [PATCH 06/83] Clean up unused status (#131558) --- x-pack/plugins/fleet/common/constants/epm.ts | 1 - x-pack/plugins/fleet/common/types/models/epm.ts | 13 ++----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index 5952208007232d..07f6fa048dc427 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -59,5 +59,4 @@ export const installationStatuses = { Installing: 'installing', InstallFailed: 'install_failed', NotInstalled: 'not_installed', - InstalledBundled: 'installed_bundled', } as const; diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 5217a6232a18c1..2359b979d0a177 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -45,11 +45,7 @@ export interface DefaultPackagesInstallationError { export type InstallType = 'reinstall' | 'reupdate' | 'rollback' | 'update' | 'install' | 'unknown'; export type InstallSource = 'registry' | 'upload' | 'bundled'; -export type EpmPackageInstallStatus = - | 'installed' - | 'installing' - | 'install_failed' - | 'installed_bundled'; +export type EpmPackageInstallStatus = 'installed' | 'installing' | 'install_failed'; export type DetailViewPanelName = 'overview' | 'policies' | 'assets' | 'settings' | 'custom'; export type ServiceName = 'kibana' | 'elasticsearch'; @@ -431,8 +427,7 @@ export type Installable = | InstalledRegistry | Installing | NotInstalled - | InstallFailed - | InstalledBundled; + | InstallFailed; export type InstallStatusExcluded = T & { status: undefined; @@ -443,10 +438,6 @@ export type InstalledRegistry = T & { savedObject: SavedObject; }; -export type InstalledBundled = T & { - status: InstallationStatus['InstalledBundled']; -}; - export type Installing = T & { status: InstallationStatus['Installing']; savedObject: SavedObject; From 860261c864d8eb2d968514f39bf478e5cc0d6b35 Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Wed, 4 May 2022 14:51:24 -0600 Subject: [PATCH 07/83] Fix broken/missing APM app links and use `docLinks` service (#128326) Co-authored-by: Kaarina Tungseth Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Giorgos Bamparopoulos --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + packages/kbn-doc-links/src/types.ts | 1 + .../app/settings/custom_link/empty_prompt.tsx | 30 +++++++++++++---- .../app/settings/custom_link/index.test.tsx | 33 ++++++++++++------- .../app/settings/schema/schema_overview.tsx | 12 +++---- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 8 files changed, 51 insertions(+), 29 deletions(-) diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 14fd80c3a85529..79b41112768a64 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -47,6 +47,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { metaData: `${APM_DOCS}guide/${DOC_LINK_VERSION}/data-model-metadata.html`, overview: `${APM_DOCS}guide/${DOC_LINK_VERSION}/apm-overview.html`, tailSamplingPolicies: `${APM_DOCS}guide/${DOC_LINK_VERSION}/configure-tail-based-sampling.html`, + elasticAgent: `${APM_DOCS}guide/${DOC_LINK_VERSION}/upgrade-to-apm-integration.html`, }, canvas: { guide: `${KIBANA_DOCS}canvas.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 4a5a9fdeb95763..645aad3af2bd24 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -33,6 +33,7 @@ export interface DocLinks { readonly metaData: string; readonly overview: string; readonly tailSamplingPolicies: string; + readonly elasticAgent: string; }; readonly canvas: { readonly guide: string; diff --git a/x-pack/plugins/apm/public/components/app/settings/custom_link/empty_prompt.tsx b/x-pack/plugins/apm/public/components/app/settings/custom_link/empty_prompt.tsx index dd9cd760d70cf1..fd7a3d25871901 100644 --- a/x-pack/plugins/apm/public/components/app/settings/custom_link/empty_prompt.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/custom_link/empty_prompt.tsx @@ -5,16 +5,19 @@ * 2.0. */ -import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiLink, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; import { CreateCustomLinkButton } from './create_custom_link_button'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; export function EmptyPrompt({ onCreateCustomLinkClick, }: { onCreateCustomLinkClick: () => void; }) { + const { docLinks } = useApmPluginContext().core; return ( -

- {i18n.translate('xpack.apm.settings.customLink.emptyPromptText', { - defaultMessage: - "Let's change that! You can add custom links to the Actions context menu by the transaction details for each service. Create a helpful link to your company's support portal or open a new bug report. Learn more about it in our docs.", - })} -

+ + + {i18n.translate( + 'xpack.apm.settings.customLink.emptyPromptText.customLinkDocLinkText', + { defaultMessage: 'docs' } + )} + + ), + }} + /> + } actions={} diff --git a/x-pack/plugins/apm/public/components/app/settings/custom_link/index.test.tsx b/x-pack/plugins/apm/public/components/app/settings/custom_link/index.test.tsx index e9979746426a37..40f8f5ad1db253 100644 --- a/x-pack/plugins/apm/public/components/app/settings/custom_link/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/custom_link/index.test.tsx @@ -23,6 +23,7 @@ import { expectTextsNotInDocument, } from '../../../../utils/test_helpers'; import * as saveCustomLink from './create_edit_custom_link_flyout/save_custom_link'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; const data = { customLinks: [ @@ -73,9 +74,11 @@ describe('CustomLink', () => { it('shows when no link is available', () => { const component = render( - - - + + + + + ); expectTextsInDocument(component, ['No links found.']); @@ -360,9 +363,11 @@ describe('CustomLink', () => { }); const component = render( - - - + + + + + ); expectTextsNotInDocument(component, ['Start free 30-day trial']); @@ -375,9 +380,11 @@ describe('CustomLink', () => { const { getByTestId } = render( - - - + + + + + ); const createButton = getByTestId('createButton') as HTMLButtonElement; @@ -389,9 +396,11 @@ describe('CustomLink', () => { const { queryAllByText } = render( - - - + + + + + ); diff --git a/x-pack/plugins/apm/public/components/app/settings/schema/schema_overview.tsx b/x-pack/plugins/apm/public/components/app/settings/schema/schema_overview.tsx index 1e31eb8ccbe67c..1f17b9f63c0a07 100644 --- a/x-pack/plugins/apm/public/components/app/settings/schema/schema_overview.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/schema/schema_overview.tsx @@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiLink, EuiLoadingSpinner, EuiSpacer, EuiText, @@ -21,11 +22,11 @@ import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; import semverLt from 'semver/functions/lt'; import { PackagePolicy } from '@kbn/fleet-plugin/common/types'; -import { ElasticDocsLink } from '../../../shared/links/elastic_docs_link'; import rocketLaunchGraphic from './blog_rocket_720x420.png'; import { MigrationInProgressPanel } from './migration_in_progress_panel'; import { UpgradeAvailableCard } from './migrated/upgrade_available_card'; import { SuccessfulMigrationCard } from './migrated/successful_migration_card'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; interface Props { onSwitch: () => void; @@ -184,6 +185,7 @@ export function SchemaOverview({ } export function SchemaOverviewHeading() { + const { docLinks } = useApmPluginContext().core; return ( <> @@ -208,16 +210,12 @@ export function SchemaOverviewHeading() { ), elasticAgentDocLink: ( - + {i18n.translate( 'xpack.apm.settings.schema.descriptionText.elasticAgentDocLinkText', { defaultMessage: 'Elastic Agent' } )} - + ), }} /> diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 7ee56f53e1f89e..cf9e0c25c346dc 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -8035,7 +8035,6 @@ "xpack.apm.settings.customLink.delete": "Supprimer", "xpack.apm.settings.customLink.delete.failed": "Impossible de supprimer le lien personnalisé", "xpack.apm.settings.customLink.delete.successed": "Lien personnalisé supprimé.", - "xpack.apm.settings.customLink.emptyPromptText": "Nous allons y remédier ! Vous pouvez ajouter des liens personnalisés au menu contextuel Actions à partir des détails de transaction de chaque service. Créez un lien utile vers le portail d'assistance de votre société, ou ouvrez un nouveau rapport de bug. Pour en savoir plus, consultez notre documentation.", "xpack.apm.settings.customLink.emptyPromptTitle": "Aucun lien trouvé.", "xpack.apm.settings.customLink.flyout.action.title": "Lien", "xpack.apm.settings.customLink.flyout.close": "Fermer", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2cd3f174be4433..847603367dd1bd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8001,7 +8001,6 @@ "xpack.apm.settings.customLink.delete": "削除", "xpack.apm.settings.customLink.delete.failed": "カスタムリンクを削除できませんでした", "xpack.apm.settings.customLink.delete.successed": "カスタムリンクを削除しました。", - "xpack.apm.settings.customLink.emptyPromptText": "変更しましょう。サービスごとのトランザクションの詳細でアクションコンテキストメニューにカスタムリンクを追加できます。自社のサポートポータルへの役立つリンクを作成するか、新しい不具合レポートを発行します。詳細はドキュメントをご覧ください。", "xpack.apm.settings.customLink.emptyPromptTitle": "リンクが見つかりません。", "xpack.apm.settings.customLink.flyout.action.title": "リンク", "xpack.apm.settings.customLink.flyout.close": "閉じる", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1afb1b21737dac..aee146cced1398 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8021,7 +8021,6 @@ "xpack.apm.settings.customLink.delete": "删除", "xpack.apm.settings.customLink.delete.failed": "无法删除定制链接", "xpack.apm.settings.customLink.delete.successed": "已删除定制链接。", - "xpack.apm.settings.customLink.emptyPromptText": "让我们改动一下!可以通过每个服务的事务详情将定制链接添加到“操作”上下文菜单。创建指向公司支持门户或用于提交新错误报告的有用链接。在我们的文档中详细了解。", "xpack.apm.settings.customLink.emptyPromptTitle": "未找到链接。", "xpack.apm.settings.customLink.flyout.action.title": "链接", "xpack.apm.settings.customLink.flyout.close": "关闭", From fb453aca45afccfcec4ce3e0cd98a4fa4772a122 Mon Sep 17 00:00:00 2001 From: Melissa Burpo Date: Wed, 4 May 2022 16:13:54 -0500 Subject: [PATCH 08/83] Osquery pack attribution (#131462) * add new reference page for prebuilt packs * add link to new prebuilt pack ref page * convert list to table * add table close * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/osquery/osquery.asciidoc | 4 ++ docs/osquery/prebuilt-packs.asciidoc | 63 ++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 docs/osquery/prebuilt-packs.asciidoc diff --git a/docs/osquery/osquery.asciidoc b/docs/osquery/osquery.asciidoc index e450bcbcb7d6f2..9e384d79a4d6b4 100644 --- a/docs/osquery/osquery.asciidoc +++ b/docs/osquery/osquery.asciidoc @@ -127,6 +127,8 @@ The Osquery Manager integration includes a set of prebuilt Osquery packs that yo You can modify the scheduled agent policies for a prebuilt pack, but you cannot edit queries in the pack. To edit the queries, you must first create a copy of the pack. +For information about the prebuilt packs that are available, refer to <>. + [float] [[load-prebuilt-packs]] === Load and activate prebuilt Elastic packs @@ -310,3 +312,5 @@ https://osquery.readthedocs.io/en/stable/deployment/logging/#differential-logs[d include::manage-integration.asciidoc[] include::exported-fields-reference.asciidoc[] + +include::prebuilt-packs.asciidoc[] diff --git a/docs/osquery/prebuilt-packs.asciidoc b/docs/osquery/prebuilt-packs.asciidoc new file mode 100644 index 00000000000000..776a1937b7a698 --- /dev/null +++ b/docs/osquery/prebuilt-packs.asciidoc @@ -0,0 +1,63 @@ +[[prebuilt-packs]] +== Prebuilt packs reference + +This section lists all prebuilt packs available for Osquery Manager. +Each pack is also available as a saved object, with the name `Pack: `. + +For more information, refer to <>. + + +|=== +|Name |Description |Source |Added + +|`hardware-monitoring` +|Monitor for hardware changes. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`incident-response` +|Detect and respond to breaches. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`it-compliance` +a|Identify outdated and vulnerable software. + +Dashboard: `[Osquery Manager] Compliance pack` + +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`osquery-monitoring` +|Monitor Osquery info and performance. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`ossec-rootkit` +a|Run rootkit detection queries to monitor for compromise. + +Dashboard: `[Osquery Manager] OSSEC rootkit pack` + +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`osx-attacks` +|Identify compromised macOS systems. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`unwanted-chrome-extensions` +|Monitor for malicious Chrome extensions. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`vuln-management` +|Identify system vulnerabilities. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`windows-attacks` +|Monitor for evidence of Windows attacks. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 +|=== From 542b381fa51703fe95708dff956fdac93dd5d3dc Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 4 May 2022 15:05:58 -0700 Subject: [PATCH 09/83] [ftr] automatically determine config run order (#130983) * [ftr] automatically determine config run order * split lens config into two groups * support ftr configs always running against CI * Split detection_engine_api_integration rule exception list tests * Add configs from previous commit * [ftr] remove testMetadata and maintain a unique lifecycle instance per run * Revert "[ftr] remove testMetadata and maintain a unique lifecycle instance per run" This reverts commit d2b4fdb8249ff4d8b835f0c6c74df8d1a29ddb3c. * Split alerting_api_integration/security_and_spaces tests * Add groups to yaml * Revert "Revert "[ftr] remove testMetadata and maintain a unique lifecycle instance per run"" This reverts commit 56232eea682a4d1393d9ecef575e4906dc2862b3. * stop ES more forcefully and fix timeout * only cleanup lifecycle phases when the cleanup is totally complete * only use kill when cleaning up an esTestInstance * fix broken import * fix runOptions.alwaysUseSource implementation * fix config access * fix x-pack/ccs config * fix ml import file paths * update kibana build id * revert array.concat() change * fix baseConfig usage * fix pie chart data * split up maps tests * pull in all of group5 so that es archives are loaded correctly * add to ftr configs.yml * fix pie chart data without breaking legacy version * fix more pie_chart stuff in new vis lib * restore normal PR tasks * bump kibana-buildkite-library * remove ciGroup validation * remove the script which is no longer called from checks.sh * [CI] Auto-commit changed files from 'yarn kbn run build -i @kbn/pm' * adapt flaky test runner scripts to handle ftrConfig paths * fix types in alerting_api_integration * improve flaky config parsing and use non-local var name for passing explicit configs to ftr_configs.sh * Split xpack dashboard tests * Add configs * [flaky] remove key from ftr-config steps * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * restore cypress builds * remove ciGroups from FTR config files * fixup some docs * add temporary script to hunt for FTR config files * use config.base.js naming for clarity * use script to power ftr_configs.yml * remove usage of removed x-pack/scripts/functional_tests * fix test names in dashboard snapshots * bump kibana-buildkite-library * Try retrying only failed configs * be a little quieter about trying to get testStats from configs with testRunners defined * Remove test code * bump kibana-buildkite-library * update es_snapshot and on_merge jobs too * track duration and exit code for each config and print it at the end of the script * store results in order, rather than by key, in case there are duplicates in $config * bash is hard * fix env source and use +e rather than disabling e for whole file * bash sucks * print config summary in jest jobs too * define results in jest_parallel.sh * simplify config summary print, format times a little better * fix reference to unbound time variable, use better variable name * skip the newline between each result * finish with the nitpicking * sync changes with ftr_configs.sh * refuse to execute config files which aren't listed in the .buildkite/ftr_configs.yml * fix config.edge.js base config import paths * fix some readmes * resolve paths from ftr_configs manifest * fix readConfigFile tests * just allow __fixtures__ configs * list a few more cypress config files * install the main branch of kibana-buildkite-library * split up lens group1 * move ml data_visualizer tests to their own config * fix import paths * fix more imports * install specific commit of buildkite-pipeline-library * sort configs in ftr_configs.yml * bump kibana-buildkite-library * remove temporary script * fix env var for limiting config types * Update docs/developer/contributing/development-functional-tests.asciidoc Co-authored-by: Christiane (Tina) Heiligers * produce a JUnit report for saved objects field count * apply standard concurrency limits from flaky test runner * support customizing FTR concurrency via the env Co-authored-by: Brian Seeders Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Christiane (Tina) Heiligers --- .buildkite/ftr_configs.yml | 236 ++++++++++++++++++ .buildkite/package-lock.json | 12 +- .buildkite/package.json | 2 +- .buildkite/pipelines/es_snapshots/verify.yml | 39 +-- .buildkite/pipelines/flaky_tests/groups.json | 34 +-- .buildkite/pipelines/flaky_tests/pipeline.js | 155 ++++++------ .buildkite/pipelines/on_merge.yml | 123 +-------- .buildkite/pipelines/pull_request/base.yml | 123 +-------- .buildkite/scripts/steps/checks.sh | 1 - .../steps/checks/validate_ci_groups.sh | 9 - .../scripts/steps/functional/fleet_cypress.sh | 4 +- .../steps/functional/osquery_cypress.sh | 4 +- .../steps/functional/oss_accessibility.sh | 13 - .../scripts/steps/functional/oss_cigroup.sh | 16 -- .../scripts/steps/functional/oss_firefox.sh | 14 -- .../scripts/steps/functional/oss_misc.sh | 51 ---- .../functional/performance_playwright.sh | 10 +- .../scripts/steps/functional/response_ops.sh | 4 +- .../steps/functional/response_ops_cases.sh | 4 +- .../steps/functional/security_solution.sh | 4 +- .../steps/functional/xpack_accessibility.sh | 15 -- .../scripts/steps/functional/xpack_cigroup.sh | 20 -- .../scripts/steps/functional/xpack_firefox.sh | 17 -- .../xpack_saved_object_field_metrics.sh | 17 -- .../scripts/steps/test/api_integration.sh | 16 -- .buildkite/scripts/steps/test/ftr_configs.sh | 88 +++++++ .../scripts/steps/test/jest_parallel.sh | 29 ++- .../steps/test/pick_jest_config_run_order.sh | 9 - ..._order.js => pick_test_group_run_order.js} | 0 .../steps/test/pick_test_group_run_order.sh | 9 + .../test/{jest_env.sh => test_group_env.sh} | 1 + .../development-functional-tests.asciidoc | 7 +- .../external-plugin-functional-tests.asciidoc | 2 +- .../kbn-dev-utils/src/proc_runner/proc.ts | 14 +- .../src/proc_runner/proc_runner.ts | 48 ++-- packages/kbn-es-archiver/src/cli.ts | 2 +- packages/kbn-es/src/cluster.js | 18 ++ packages/kbn-pm/dist/index.js | 21 +- .../src/observe_lines.ts | 17 +- .../src/observe_readable.ts | 10 +- packages/kbn-test/BUILD.bazel | 2 + packages/kbn-test/README.md | 4 +- packages/kbn-test/src/es/test_es_cluster.ts | 23 +- .../src/functional_test_runner/cli.ts | 6 +- .../functional_test_runner.ts | 163 ++++++------ .../src/functional_test_runner/index.ts | 1 - .../lib/config/ftr_configs_manifest.ts | 23 ++ ..._file.test.js => read_config_file.test.ts} | 25 +- .../lib/config/read_config_file.ts | 38 ++- .../lib/config/schema.ts | 5 + .../src/functional_test_runner/lib/index.ts | 1 - .../functional_test_runner/lib/lifecycle.ts | 38 ++- .../lib/lifecycle_phase.test.ts | 12 +- .../lib/lifecycle_phase.ts | 7 + .../lib/mocha/decorate_mocha_ui.js | 32 +++ .../lib/mocha/filter_suites.test.js | 5 +- .../lib/mocha/filter_suites.ts | 2 +- .../mocha/reporter/ci_stats_ftr_reporter.ts | 15 +- .../lib/mocha/reporter/reporter.js | 2 - .../lib/mocha/validate_ci_group_tags.js | 6 +- .../lib/providers/index.ts | 2 +- .../snapshots/decorate_snapshot_ui.test.ts | 17 +- .../lib/suite_tracker.test.ts | 4 +- .../lib/test_metadata.ts | 41 --- .../functional_test_runner/public_types.ts | 9 +- .../src/functional_tests/lib/run_ftr.ts | 8 +- .../functional_tests/lib/run_kibana_server.ts | 11 +- .../kbn-test/src/functional_tests/tasks.ts | 28 +-- scripts/README.md | 8 +- scripts/functional_tests.js | 24 +- scripts/functional_tests_server.js | 2 +- test/accessibility/config.ts | 2 +- test/analytics/config.ts | 2 +- .../lib/saved_objects_test_utils.ts | 3 +- test/api_integration/config.js | 2 +- test/examples/bfetch_explorer/index.ts | 1 - test/examples/config.js | 2 +- .../data_view_field_editor_example/index.ts | 1 - test/examples/embeddables/index.ts | 1 - test/examples/expressions_explorer/index.ts | 1 - test/examples/field_formats/index.ts | 1 - test/examples/hello_world/index.ts | 1 - test/examples/partial_results/index.ts | 1 - test/examples/routing/index.ts | 1 - test/examples/state_sync/index.ts | 1 - test/examples/ui_actions/index.ts | 1 - .../bundles/config.ts} | 17 +- test/functional/apps/bundles/index.js | 2 +- test/functional/apps/console/config.ts | 18 ++ test/functional/apps/console/index.js | 2 - test/functional/apps/context/config.ts | 18 ++ test/functional/apps/context/index.ts | 2 - test/functional/apps/dashboard/README.md | 7 + .../apps/dashboard/group1/config.ts | 18 ++ .../create_and_add_embeddables.ts | 2 +- .../{ => group1}/dashboard_back_button.ts | 2 +- .../{ => group1}/dashboard_error_handling.ts | 2 +- .../{ => group1}/dashboard_options.ts | 2 +- .../{ => group1}/dashboard_query_bar.ts | 2 +- .../{ => group1}/dashboard_unsaved_listing.ts | 2 +- .../{ => group1}/dashboard_unsaved_state.ts | 2 +- .../{ => group1}/data_shared_attributes.ts | 2 +- .../{ => group1}/edit_embeddable_redirects.ts | 2 +- .../{ => group1}/edit_visualizations.js | 0 .../apps/dashboard/{ => group1}/embed_mode.ts | 2 +- .../{ => group1}/embeddable_data_grid.ts | 2 +- .../{ => group1}/embeddable_rendering.ts | 2 +- .../dashboard/{ => group1}/empty_dashboard.ts | 2 +- .../functional/apps/dashboard/group1/index.ts | 54 ++++ .../dashboard/{ => group1}/legacy_urls.ts | 2 +- .../{ => group1}/saved_search_embeddable.ts | 2 +- .../apps/dashboard/{ => group1}/share.ts | 2 +- .../{ => group1}/url_field_formatter.ts | 4 +- .../apps/dashboard/group2/config.ts | 18 ++ .../{ => group2}/dashboard_filter_bar.ts | 2 +- .../{ => group2}/dashboard_filtering.ts | 2 +- .../dashboard/{ => group2}/dashboard_grid.ts | 2 +- .../{ => group2}/dashboard_saved_query.ts | 2 +- .../{ => group2}/dashboard_snapshots.ts | 2 +- .../{ => group2}/embeddable_library.ts | 2 +- .../{ => group2}/full_screen_mode.ts | 2 +- .../functional/apps/dashboard/group2/index.ts | 43 ++++ .../{ => group2}/panel_expand_toggle.ts | 2 +- .../apps/dashboard/{ => group2}/view_edit.ts | 2 +- .../dashboard/{ => group3}/bwc_shared_urls.ts | 2 +- .../apps/dashboard/group3/config.ts | 18 ++ .../dashboard/{ => group3}/copy_panel_to.ts | 2 +- .../dashboard/{ => group3}/dashboard_state.ts | 4 +- .../{ => group3}/dashboard_time_picker.ts | 4 +- .../functional/apps/dashboard/group3/index.ts | 36 +++ .../dashboard/{ => group3}/panel_cloning.ts | 4 +- .../{ => group3}/panel_context_menu.ts | 4 +- .../dashboard/{ => group3}/panel_replacing.ts | 4 +- .../apps/dashboard/group4/config.ts | 18 ++ .../dashboard/{ => group4}/dashboard_clone.ts | 2 +- .../{ => group4}/dashboard_listing.ts | 2 +- .../dashboard/{ => group4}/dashboard_save.ts | 2 +- .../dashboard/{ => group4}/dashboard_time.ts | 2 +- .../functional/apps/dashboard/group4/index.ts | 33 +++ .../apps/dashboard/group5/config.ts | 18 ++ .../functional/apps/dashboard/group5/index.ts | 48 ++++ test/functional/apps/dashboard/index.ts | 139 ----------- .../apps/dashboard_elements/config.ts | 18 ++ .../apps/dashboard_elements/index.ts | 4 +- test/functional/apps/discover/config.ts | 18 ++ test/functional/apps/discover/index.ts | 2 - .../functional/apps/getting_started/config.ts | 18 ++ test/functional/apps/getting_started/index.ts | 2 - test/functional/apps/home/config.ts | 18 ++ test/functional/apps/home/index.js | 4 +- test/functional/apps/management/config.ts | 18 ++ test/functional/apps/management/index.ts | 47 ++-- .../apps/saved_objects_management/config.ts | 18 ++ .../apps/saved_objects_management/index.ts | 1 - test/functional/apps/status_page/config.ts | 18 ++ test/functional/apps/status_page/index.ts | 2 - test/functional/apps/visualize/README.md | 7 + .../visualize/{ => group1}/_chart_types.ts | 2 +- .../visualize/{ => group1}/_data_table.ts | 2 +- .../{ => group1}/_data_table_nontimeindex.ts | 2 +- .../_data_table_notimeindex_filters.ts | 2 +- .../{ => group1}/_embedding_chart.ts | 2 +- .../apps/visualize/group1/config.ts | 18 ++ .../functional/apps/visualize/group1/index.ts | 32 +++ .../{ => group2}/_experimental_vis.ts | 2 +- .../visualize/{ => group2}/_gauge_chart.ts | 2 +- .../visualize/{ => group2}/_heatmap_chart.ts | 2 +- .../{ => group2}/_histogram_request_start.ts | 2 +- .../apps/visualize/{ => group2}/_inspector.ts | 2 +- .../visualize/{ => group2}/_metric_chart.ts | 2 +- .../apps/visualize/group2/config.ts | 18 ++ .../functional/apps/visualize/group2/index.ts | 33 +++ .../{ => group3}/_add_to_dashboard.ts | 2 +- .../apps/visualize/{ => group3}/_lab_mode.ts | 2 +- .../{ => group3}/_linked_saved_searches.ts | 2 +- .../apps/visualize/{ => group3}/_pie_chart.ts | 22 +- .../visualize/{ => group3}/_shared_item.ts | 2 +- .../{ => group3}/_visualize_listing.ts | 2 +- .../apps/visualize/group3/config.ts | 18 ++ .../functional/apps/visualize/group3/index.ts | 33 +++ .../visualize/{ => group4}/_tsvb_chart.ts | 2 +- .../apps/visualize/group4/config.ts | 18 ++ .../functional/apps/visualize/group4/index.ts | 28 +++ .../{ => group5}/_tsvb_time_series.ts | 2 +- .../apps/visualize/group5/config.ts | 18 ++ .../functional/apps/visualize/group5/index.ts | 28 +++ .../apps/visualize/{ => group6}/_tag_cloud.ts | 2 +- .../visualize/{ => group6}/_tsvb_markdown.ts | 2 +- .../visualize/{ => group6}/_tsvb_table.ts | 2 +- .../visualize/{ => group6}/_vega_chart.ts | 2 +- .../apps/visualize/group6/config.ts | 18 ++ .../functional/apps/visualize/group6/index.ts | 31 +++ test/functional/apps/visualize/index.ts | 112 --------- .../_area_chart.ts | 2 +- .../_line_chart_split_chart.ts | 2 +- .../_line_chart_split_series.ts | 2 +- .../_point_series_options.ts | 2 +- .../_timelion.ts | 2 +- .../_vertical_bar_chart.ts | 2 +- .../_vertical_bar_chart_nontimeindex.ts | 2 +- .../replaced_vislib_chart_types/config.ts | 18 ++ .../replaced_vislib_chart_types/index.ts | 55 ++++ test/functional/{config.js => config.base.js} | 14 -- test/functional/config.ccs.ts | 16 +- test/functional/config.edge.js | 4 +- test/functional/config.firefox.js | 14 +- .../functional/services/common/screenshots.ts | 37 +-- .../tests/enrollment_flow.ts | 2 +- .../tests/manual_configuration_flow.ts | 2 +- .../manual_configuration_flow_without_tls.ts | 2 +- ...l_configuration_without_security.config.ts | 2 +- .../tests/enrollment_token.ts | 2 +- .../tests/manual_configuration.ts | 2 +- .../manual_configuration_without_security.ts | 2 +- .../tests/manual_configuration_without_tls.ts | 2 +- test/interpreter_functional/config.ts | 2 +- test/new_visualize_flow/config.ts | 2 +- test/new_visualize_flow/index.ts | 1 - test/plugin_functional/config.ts | 2 +- .../test_suites/core_plugins/rendering.ts | 2 +- .../{config.js => config.base.js} | 2 +- .../http/platform/config.status.ts | 3 +- .../http/platform/config.ts | 2 +- test/server_integration/http/ssl/config.js | 2 +- .../http/ssl_redirect/config.js | 2 +- .../http/ssl_with_p12/config.js | 2 +- .../http/ssl_with_p12_intermediate/config.js | 2 +- test/ui_capabilities/newsfeed_err/config.ts | 10 +- test/ui_capabilities/newsfeed_err/test.ts | 2 - test/visual_regression/config.ts | 2 +- .../visual_regression/tests/discover/index.ts | 2 - test/visual_regression/tests/vega/index.ts | 2 - x-pack/README.md | 2 +- x-pack/plugins/apm/dev_docs/testing.md | 4 +- .../apm/ftr_e2e/cypress/tasks/es_archiver.ts | 6 +- x-pack/plugins/apm/ftr_e2e/ftr_config.ts | 2 +- x-pack/plugins/fleet/cypress/README.md | 4 +- x-pack/plugins/graph/README.md | 4 +- x-pack/plugins/lens/readme.md | 5 +- x-pack/plugins/maps/README.md | 5 +- x-pack/plugins/osquery/cypress/README.md | 4 +- .../security_solution/cypress/README.md | 8 +- .../cypress/tasks/es_archiver.ts | 2 +- x-pack/plugins/synthetics/e2e/config.ts | 2 +- .../synthetics/e2e/tasks/es_archiver.ts | 6 +- x-pack/scripts/functional_tests.js | 96 ------- x-pack/scripts/functional_tests_server.js | 2 +- .../accessibility/apps/advanced_settings.ts | 2 +- x-pack/test/accessibility/apps/canvas.ts | 2 +- .../apps/dashboard_edit_panel.ts | 2 +- .../accessibility/apps/enterprise_search.ts | 2 +- .../test/accessibility/apps/grok_debugger.ts | 2 +- x-pack/test/accessibility/apps/home.ts | 2 +- .../apps/index_lifecycle_management.ts | 4 +- .../apps/ingest_node_pipelines.ts | 2 +- .../accessibility/apps/kibana_overview.ts | 2 +- x-pack/test/accessibility/apps/lens.ts | 2 +- .../accessibility/apps/license_management.ts | 2 +- x-pack/test/accessibility/apps/login_page.ts | 3 +- x-pack/test/accessibility/apps/maps.ts | 2 +- x-pack/test/accessibility/apps/ml.ts | 16 +- .../apps/ml_embeddables_in_dashboard.ts | 2 +- .../test/accessibility/apps/painless_lab.ts | 2 +- .../accessibility/apps/remote_clusters.ts | 2 +- x-pack/test/accessibility/apps/reporting.ts | 2 +- x-pack/test/accessibility/apps/roles.ts | 2 +- .../accessibility/apps/search_profiler.ts | 2 +- .../accessibility/apps/search_sessions.ts | 2 +- .../accessibility/apps/security_solution.ts | 2 +- x-pack/test/accessibility/apps/spaces.ts | 2 +- x-pack/test/accessibility/apps/tags.ts | 2 +- x-pack/test/accessibility/apps/transform.ts | 2 +- .../accessibility/apps/upgrade_assistant.ts | 2 +- x-pack/test/accessibility/apps/uptime.ts | 2 +- x-pack/test/accessibility/apps/users.ts | 2 +- x-pack/test/accessibility/config.ts | 2 +- .../basic/tests/index.ts | 2 - .../alerting_api_integration/common/config.ts | 4 +- .../{ => group1}/config.ts | 3 +- .../{ => group1}/tests/alerting/create.ts | 6 +- .../{ => group1}/tests/alerting/delete.ts | 6 +- .../{ => group1}/tests/alerting/disable.ts | 6 +- .../{ => group1}/tests/alerting/enable.ts | 6 +- .../tests/alerting/execution_status.ts | 6 +- .../{ => group1}/tests/alerting/find.ts | 6 +- .../{ => group1}/tests/alerting/get.ts | 6 +- .../tests/alerting/get_alert_state.ts | 6 +- .../tests/alerting/get_alert_summary.ts | 6 +- .../group1/tests/alerting/index.ts | 35 +++ .../{ => group1}/tests/alerting/rule_types.ts | 6 +- .../security_and_spaces/group1/tests/index.ts | 17 ++ .../security_and_spaces/group2/config.ts | 18 ++ .../actions/builtin_action_types/email.ts | 4 +- .../actions/builtin_action_types/es_index.ts | 2 +- .../es_index_preconfigured.ts | 2 +- .../actions/builtin_action_types/jira.ts | 6 +- .../actions/builtin_action_types/pagerduty.ts | 6 +- .../actions/builtin_action_types/resilient.ts | 6 +- .../builtin_action_types/server_log.ts | 2 +- .../builtin_action_types/servicenow_itom.ts | 6 +- .../builtin_action_types/servicenow_itsm.ts | 6 +- .../builtin_action_types/servicenow_sir.ts | 6 +- .../actions/builtin_action_types/slack.ts | 6 +- .../actions/builtin_action_types/swimlane.ts | 6 +- .../actions/builtin_action_types/webhook.ts | 6 +- .../actions/builtin_action_types/xmatters.ts | 6 +- .../group2/tests/actions/config.ts | 18 ++ .../tests/actions/connector_types.ts | 6 +- .../{ => group2}/tests/actions/create.ts | 6 +- .../{ => group2}/tests/actions/delete.ts | 6 +- .../{ => group2}/tests/actions/execute.ts | 6 +- .../{ => group2}/tests/actions/get.ts | 6 +- .../{ => group2}/tests/actions/get_all.ts | 6 +- .../{ => group2}/tests/actions/index.ts | 4 +- .../tests/actions/manual/pr_40694.js | 0 .../{ => group2}/tests/actions/update.ts | 6 +- .../{ => group2}/tests/alerting/alerts.ts | 6 +- .../{ => group2}/tests/alerting/event_log.ts | 8 +- .../{ => group2}/tests/alerting/excluded.ts | 6 +- .../{ => group2}/tests/alerting/health.ts | 6 +- .../group2/tests/alerting/index.ts | 49 ++++ .../tests/alerting/mustache_templates.ts | 10 +- .../{ => group2}/tests/alerting/mute_all.ts | 6 +- .../tests/alerting/mute_instance.ts | 6 +- .../tests/alerting/rbac_legacy.ts | 8 +- .../{ => group2}/tests/alerting/snooze.ts | 6 +- .../{ => group2}/tests/alerting/unmute_all.ts | 6 +- .../tests/alerting/unmute_instance.ts | 6 +- .../{ => group2}/tests/alerting/unsnooze.ts | 6 +- .../{ => group2}/tests/alerting/update.ts | 6 +- .../tests/alerting/update_api_key.ts | 6 +- .../security_and_spaces/group2/tests/index.ts | 17 ++ .../tests/telemetry/actions_telemetry.ts | 6 +- .../tests/telemetry/alerting_telemetry.ts | 6 +- .../group2/tests/telemetry/config.ts | 18 ++ .../{ => group2}/tests/telemetry/index.ts | 4 +- .../{tests/index.ts => setup.ts} | 19 +- .../tests/alerting/index.ts | 68 ----- .../spaces_only/tests/index.ts | 2 - .../spaces_only_legacy/tests/index.ts | 2 - x-pack/test/api_integration/apis/index.ts | 2 - .../api_integration/apis/security/index.ts | 2 - .../apis/security/security_basic.ts | 2 - .../apis/security/security_trial.ts | 2 - .../test/api_integration/apis/spaces/index.ts | 2 - x-pack/test/api_integration/config.ts | 2 +- .../test/api_integration_basic/apis/index.ts | 2 - .../test/apm_api_integration/tests/index.ts | 2 - x-pack/test/banners_functional/config.ts | 4 +- x-pack/test/banners_functional/tests/index.ts | 2 - .../security_and_spaces/tests/basic/index.ts | 3 - .../security_and_spaces/tests/trial/index.ts | 24 +- .../spaces_only/tests/trial/index.ts | 3 - x-pack/test/cloud_integration/config.ts | 2 +- .../basic/config.ts | 11 +- .../basic/tests/index.ts | 2 - .../common/config.ts | 4 +- .../security_and_spaces/README.md | 33 +++ .../{config.ts => config.base.ts} | 2 +- .../{tests => group1}/add_actions.ts | 0 .../add_prepackaged_rules.ts | 0 .../{tests => group1}/aliases.ts | 0 .../{tests => group1}/check_privileges.ts | 0 .../security_and_spaces/group1/config.ts} | 19 +- .../{tests => group1}/create_index.ts | 0 .../{tests => group1}/create_ml.ts | 0 .../{tests => group1}/create_rules.ts | 0 .../{tests => group1}/create_rules_bulk.ts | 0 .../create_signals_migrations.ts | 0 .../create_threat_matching.ts | 0 .../{tests => group1}/delete_rules.ts | 0 .../{tests => group1}/delete_rules_bulk.ts | 0 .../delete_signals_migrations.ts | 0 .../{tests => group1}/export_rules.ts | 0 .../finalize_signals_migrations.ts | 0 .../{tests => group1}/find_rules.ts | 0 .../{tests => group1}/generating_signals.ts | 0 .../get_prepackaged_rules_status.ts | 0 .../get_rule_execution_events.ts | 0 .../get_signals_migration_status.ts | 0 .../{tests => group1}/ignore_fields.ts | 0 .../{tests => group1}/import_export_rules.ts | 0 .../{tests => group1}/import_rules.ts | 0 .../security_and_spaces/group1/index.ts | 57 +++++ .../legacy_actions_migrations.ts | 0 .../{tests => group1}/migrations.ts | 0 .../{tests => group1}/open_close_signals.ts | 0 .../{tests => group1}/patch_rules.ts | 0 .../{tests => group1}/patch_rules_bulk.ts | 0 .../{tests => group1}/perform_bulk_action.ts | 0 .../{tests => group1}/preview_rules.ts | 0 .../{tests => group1}/read_privileges.ts | 0 .../{tests => group1}/read_rules.ts | 0 .../{tests => group1}/resolve_read_rules.ts | 0 .../{tests => group1}/runtime.ts | 0 .../template_data/execution_events.ts | 0 .../{tests => group1}/throttle.ts | 0 .../{tests => group1}/timestamps.ts | 0 .../{tests => group1}/update_actions.ts | 0 .../{tests => group1}/update_rules.ts | 0 .../{tests => group1}/update_rules_bulk.ts | 0 .../security_and_spaces/group2/config.ts | 18 ++ .../create_endpoint_exceptions.ts | 0 .../security_and_spaces/group2/index.ts | 15 ++ .../security_and_spaces/group3/config.ts | 18 ++ .../{tests => group3}/create_exceptions.ts | 0 .../security_and_spaces/group3/index.ts | 15 ++ .../security_and_spaces/group4/config.ts | 18 ++ .../security_and_spaces/group4/index.ts | 15 ++ .../{tests => group4}/telemetry/README.md | 0 .../{tests => group4}/telemetry/index.ts | 15 +- .../telemetry/task_based/all_types.ts | 0 .../telemetry/task_based/detection_rules.ts | 0 .../telemetry/task_based/security_lists.ts | 0 .../telemetry/usage_collector/all_types.ts | 0 .../usage_collector/detection_rule_status.ts | 0 .../usage_collector/detection_rules.ts | 0 .../security_and_spaces/group5/config.ts | 18 ++ .../security_and_spaces/group5/index.ts | 15 ++ .../keyword_family/README.md | 0 .../keyword_family/const_keyword.ts | 0 .../{tests => group5}/keyword_family/index.ts | 10 +- .../keyword_family/keyword.ts | 0 .../keyword_mixed_with_const.ts | 0 .../alerts/alerts_compatibility.ts | 0 .../{tests => group6}/alerts/index.ts | 6 +- .../security_and_spaces/group6/config.ts | 18 ++ .../security_and_spaces/group6/index.ts | 15 ++ .../security_and_spaces/group7/config.ts | 18 ++ .../exception_operators_data_types/date.ts | 0 .../exception_operators_data_types/double.ts | 0 .../exception_operators_data_types/float.ts | 0 .../exception_operators_data_types/index.ts | 18 ++ .../exception_operators_data_types/integer.ts | 0 .../security_and_spaces/group7/index.ts | 15 ++ .../security_and_spaces/group8/config.ts | 18 ++ .../exception_operators_data_types/index.ts | 18 ++ .../exception_operators_data_types/keyword.ts | 0 .../keyword_array.ts | 0 .../exception_operators_data_types/long.ts | 0 .../exception_operators_data_types/text.ts | 0 .../security_and_spaces/group8/index.ts | 15 ++ .../security_and_spaces/group9/config.ts | 18 ++ .../exception_operators_data_types/index.ts | 17 ++ .../exception_operators_data_types/ip.ts | 0 .../ip_array.ts | 0 .../text_array.ts | 0 .../security_and_spaces/group9/index.ts | 15 ++ .../exception_operators_data_types/README.md | 21 -- .../exception_operators_data_types/index.ts | 44 ---- .../security_and_spaces/tests/index.ts | 92 ------- .../tests/index.ts | 1 - .../apis/index.ts | 1 - x-pack/test/examples/config.ts | 4 +- x-pack/test/examples/embedded_lens/index.ts | 8 +- .../test/examples/reporting_examples/index.ts | 2 - x-pack/test/examples/screenshotting/index.ts | 2 - x-pack/test/examples/search_examples/index.ts | 1 - .../test/fleet_api_integration/apis/index.js | 2 - x-pack/test/fleet_cypress/config.ts | 2 +- .../apps/fleet/agents_page.ts | 2 - .../test/fleet_functional/apps/fleet/index.ts | 1 - .../test/fleet_functional/apps/home/index.ts | 1 - x-pack/test/fleet_functional/config.ts | 4 +- .../apps/advanced_settings/config.ts | 17 ++ .../apps/advanced_settings/index.ts | 2 +- .../test/functional/apps/api_keys/config.ts | 17 ++ x-pack/test/functional/apps/api_keys/index.ts | 1 - x-pack/test/functional/apps/apm/config.ts | 17 ++ x-pack/test/functional/apps/apm/index.ts | 1 - x-pack/test/functional/apps/canvas/config.ts | 17 ++ x-pack/test/functional/apps/canvas/index.js | 1 - .../apps/cross_cluster_replication/config.ts | 17 ++ .../apps/cross_cluster_replication/index.ts | 2 +- .../test/functional/apps/dashboard/README.md | 7 + .../apps/dashboard/group1/config.ts | 17 ++ .../dashboard_to_dashboard_drilldown.ts | 2 +- .../drilldowns/dashboard_to_url_drilldown.ts | 2 +- .../drilldowns/explore_data_chart_action.ts | 2 +- .../drilldowns/explore_data_panel_action.ts | 2 +- .../{ => group1}/drilldowns/index.ts | 2 +- .../feature_controls/dashboard_security.ts | 2 +- .../feature_controls/dashboard_spaces.ts | 2 +- .../{ => group1}/feature_controls/index.ts | 2 +- .../time_to_visualize_security.ts | 2 +- .../functional/apps/dashboard/group1/index.ts | 17 ++ .../dashboard/{ => group1}/preserve_url.ts | 2 +- .../{ => group1}/reporting/README.md | 4 +- .../reporting/__snapshots__/download_csv.snap | 8 +- .../{ => group1}/reporting/download_csv.ts | 2 +- .../dashboard/{ => group1}/reporting/index.ts | 2 +- .../large_dashboard_preserve_layout.png | Bin .../small_dashboard_preserve_layout.png | Bin .../{ => group1}/reporting/screenshots.ts | 2 +- .../{ => group2}/_async_dashboard.ts | 2 +- .../apps/dashboard/group2/config.ts | 17 ++ .../{ => group2}/dashboard_lens_by_value.ts | 2 +- .../{ => group2}/dashboard_maps_by_value.ts | 2 +- .../{ => group2}/dashboard_tagging.ts | 2 +- .../functional/apps/dashboard/group2/index.ts | 24 ++ .../controls_migration_smoke_test.ts | 2 +- ...rols_dashboard_migration_test_8_0_0.ndjson | 0 ...ens_dashboard_migration_test_7_12_1.ndjson | 0 ...svb_dashboard_migration_test_7_12_1.ndjson | 0 ...svb_dashboard_migration_test_7_13_3.ndjson | 0 ...ize_dashboard_migration_test_7_12_1.ndjson | 0 .../lens_migration_smoke_test.ts | 2 +- .../tsvb_migration_smoke_test.ts | 2 +- .../visualize_migration_smoke_test.ts | 2 +- .../dashboard/{ => group2}/panel_titles.ts | 2 +- .../dashboard/{ => group2}/sync_colors.ts | 2 +- .../test/functional/apps/dashboard/index.ts | 35 --- .../test/functional/apps/data_views/config.ts | 17 ++ .../test/functional/apps/data_views/index.ts | 1 - .../test/functional/apps/dev_tools/config.ts | 17 ++ .../test/functional/apps/dev_tools/index.ts | 2 - .../test/functional/apps/discover/config.ts | 17 ++ x-pack/test/functional/apps/discover/index.ts | 2 - x-pack/test/functional/apps/graph/config.ts | 17 ++ x-pack/test/functional/apps/graph/index.ts | 2 - .../functional/apps/grok_debugger/config.ts | 17 ++ .../functional/apps/grok_debugger/index.ts | 1 - x-pack/test/functional/apps/home/config.ts | 17 ++ x-pack/test/functional/apps/home/index.ts | 1 - .../apps/index_lifecycle_management/config.ts | 17 ++ .../index_lifecycle_management/home_page.ts | 2 +- .../apps/index_lifecycle_management/index.ts | 1 - .../apps/index_management/config.ts | 17 ++ .../functional/apps/index_management/index.ts | 1 - x-pack/test/functional/apps/infra/config.ts | 17 ++ x-pack/test/functional/apps/infra/index.ts | 3 +- .../apps/ingest_pipelines/config.ts | 17 ++ .../functional/apps/ingest_pipelines/index.ts | 1 - x-pack/test/functional/apps/lens/README.md | 7 + .../functional/apps/lens/group1/config.ts | 17 ++ .../apps/lens/{ => group1}/index.ts | 56 +---- .../lens/{ => group1}/persistent_context.ts | 2 +- .../apps/lens/{ => group1}/smokescreen.ts | 2 +- .../apps/lens/{ => group1}/table.ts | 2 +- .../apps/lens/{ => group1}/table_dashboard.ts | 2 +- .../lens/{ => group2}/add_to_dashboard.ts | 2 +- .../functional/apps/lens/group2/config.ts | 17 ++ .../apps/lens/{ => group2}/dashboard.ts | 2 +- .../apps/lens/{ => group2}/epoch_millis.ts | 2 +- .../test/functional/apps/lens/group2/index.ts | 80 ++++++ .../apps/lens/{ => group2}/multi_terms.ts | 2 +- .../apps/lens/{ => group2}/runtime_fields.ts | 2 +- .../lens/{ => group2}/show_underlying_data.ts | 2 +- .../show_underlying_data_dashboard.ts | 2 +- .../apps/lens/{ => group3}/annotations.ts | 2 +- .../apps/lens/{ => group3}/chart_data.ts | 2 +- .../apps/lens/{ => group3}/colors.ts | 2 +- .../functional/apps/lens/group3/config.ts | 17 ++ .../lens/{ => group3}/disable_auto_apply.ts | 2 +- .../apps/lens/{ => group3}/drag_and_drop.ts | 2 +- .../apps/lens/{ => group3}/error_handling.ts | 2 +- .../apps/lens/{ => group3}/formula.ts | 2 +- .../apps/lens/{ => group3}/gauge.ts | 2 +- .../apps/lens/{ => group3}/geo_field.ts | 2 +- .../apps/lens/{ => group3}/heatmap.ts | 2 +- .../test/functional/apps/lens/group3/index.ts | 92 +++++++ .../apps/lens/{ => group3}/inspector.ts | 2 +- .../apps/lens/{ => group3}/lens_reporting.ts | 2 +- .../apps/lens/{ => group3}/lens_tagging.ts | 2 +- .../apps/lens/{ => group3}/metrics.ts | 2 +- .../apps/lens/{ => group3}/reference_lines.ts | 2 +- .../apps/lens/{ => group3}/rollup.ts | 2 +- .../apps/lens/{ => group3}/time_shift.ts | 2 +- .../lens/{ => group3}/tsvb_open_in_lens.ts | 2 +- .../apps/license_management/config.ts | 17 ++ .../apps/license_management/index.ts | 1 - .../test/functional/apps/logstash/config.ts | 17 ++ .../apps/logstash/feature_controls/index.ts | 2 - x-pack/test/functional/apps/logstash/index.js | 2 - .../test/functional/apps/management/config.ts | 17 ++ .../apps/management/feature_controls/index.ts | 2 - .../test/functional/apps/management/index.ts | 2 - x-pack/test/functional/apps/maps/README.md | 7 + .../maps/{ => group1}/auto_fit_to_bounds.js | 0 .../maps/{ => group1}/blended_vector_layer.js | 0 .../functional/apps/maps/group1/config.ts | 17 ++ .../documents_source/docvalue_fields.js | 0 .../{ => group1}/documents_source/index.js | 0 .../documents_source/search_hits.js | 0 .../{ => group1}/documents_source/top_hits.js | 0 .../feature_controls/maps_security.ts | 2 +- .../feature_controls/maps_spaces.ts | 2 +- .../maps/{ => group1}/full_screen_mode.js | 0 .../test/functional/apps/maps/group1/index.js | 72 ++++++ .../maps/{ => group1}/layer_visibility.js | 0 .../apps/maps/{ => group1}/sample_data.js | 0 .../{ => group1}/saved_object_management.js | 0 .../apps/maps/{ => group1}/vector_styling.js | 0 .../functional/apps/maps/group2/config.ts | 17 ++ .../embeddable/add_to_dashboard.js | 0 .../maps/{ => group2}/embeddable/dashboard.js | 0 .../embeddable/embeddable_library.js | 0 .../embeddable/embeddable_state.js | 0 .../embeddable/filter_by_map_extent.js | 0 .../maps/{ => group2}/embeddable/index.js | 0 .../embeddable/save_and_return.js | 0 .../embeddable/tooltip_filter_actions.js | 0 .../maps/{ => group2}/es_geo_grid_source.js | 0 .../test/functional/apps/maps/group2/index.js | 64 +++++ .../functional/apps/maps/group3/config.ts | 17 ++ .../test/functional/apps/maps/group3/index.js | 63 +++++ .../reports/baseline/example_map_report.png | Bin .../reports/baseline/geo_map_report.png | Bin .../apps/maps/{ => group3}/reports/index.ts | 2 +- .../apps/maps/{ => group4}/add_layer_panel.js | 0 .../functional/apps/maps/group4/config.ts | 17 ++ .../apps/maps/{ => group4}/discover.js | 0 .../maps/{ => group4}/es_pew_pew_source.js | 0 .../file_upload/files/cb_2018_us_csa_500k.dbf | Bin .../file_upload/files/cb_2018_us_csa_500k.prj | 0 .../file_upload/files/cb_2018_us_csa_500k.shp | Bin .../file_upload/files/cb_2018_us_csa_500k.shx | Bin .../{ => group4}/file_upload/files/point.json | 0 .../file_upload/files/polygon.json | 0 .../files/world_countries_v7.geo.json | 0 .../maps/{ => group4}/file_upload/geojson.js | 0 .../maps/{ => group4}/file_upload/index.js | 0 .../{ => group4}/file_upload/shapefile.js | 0 .../maps/{ => group4}/file_upload/wizard.js | 0 .../{ => group4}/geofile_wizard_auto_open.ts | 2 +- .../apps/maps/{ => group4}/index.js | 54 +--- .../apps/maps/{ => group4}/joins.js | 0 .../apps/maps/{ => group4}/layer_errors.js | 0 .../{ => group4}/lens/choropleth_chart.ts | 2 +- .../apps/maps/{ => group4}/lens/index.ts | 2 +- .../apps/maps/{ => group4}/mapbox_styles.js | 0 .../maps/{ => group4}/mvt_geotile_grid.js | 0 .../apps/maps/{ => group4}/mvt_joins.ts | 2 +- .../apps/maps/{ => group4}/mvt_scaling.js | 0 .../{ => group4}/visualize_create_menu.js | 0 x-pack/test/functional/apps/ml/README.md | 7 + .../apps/ml/data_visualizer/config.ts | 17 ++ .../data_visualizer/file_data_visualizer.ts | 10 +- .../apps/ml/data_visualizer/index.ts | 32 ++- .../test/functional/apps/ml/group1/config.ts | 17 ++ .../classification_creation.ts | 4 +- .../classification_creation_saved_search.ts | 4 +- .../data_frame_analytics/cloning.ts | 2 +- .../data_frame_analytics/index.ts | 2 +- .../outlier_detection_creation.ts | 4 +- ...outlier_detection_creation_saved_search.ts | 4 +- .../regression_creation.ts | 4 +- .../regression_creation_saved_search.ts | 4 +- .../results_view_content.ts | 2 +- .../functional/apps/ml/{ => group1}/index.ts | 29 +-- .../ml/{ => group1}/model_management/index.ts | 2 +- .../model_management/model_list.ts | 2 +- .../functional/apps/ml/{ => group1}/pages.ts | 4 +- .../permissions/full_ml_access.ts | 14 +- .../apps/ml/{ => group1}/permissions/index.ts | 2 +- .../{ => group1}/permissions/no_ml_access.ts | 4 +- .../permissions/read_ml_access.ts | 14 +- .../anomaly_detection/advanced_job.ts | 2 +- .../aggregated_scripted_job.ts | 2 +- .../anomaly_detection/annotations.ts | 2 +- .../anomaly_detection/anomaly_explorer.ts | 2 +- .../anomaly_detection/categorization_job.ts | 2 +- .../anomaly_detection/custom_urls.ts | 4 +- .../anomaly_detection/date_nanos_job.ts | 2 +- .../anomaly_detection/forecasts.ts | 2 +- .../{ => group2}/anomaly_detection/index.ts | 2 +- .../anomaly_detection/multi_metric_job.ts | 2 +- .../anomaly_detection/population_job.ts | 2 +- .../anomaly_detection/saved_search_job.ts | 2 +- .../anomaly_detection/single_metric_job.ts | 2 +- ...ingle_metric_job_without_datafeed_start.ts | 2 +- .../anomaly_detection/single_metric_viewer.ts | 2 +- .../test/functional/apps/ml/group2/config.ts | 17 ++ .../test/functional/apps/ml/group2/index.ts | 42 ++++ .../test/functional/apps/ml/group3/config.ts | 17 ++ .../anomaly_charts_dashboard_embeddables.ts | 2 +- .../anomaly_embeddables_migration.ts | 2 +- .../ml/{ => group3}/embeddables/constants.ts | 0 .../apps/ml/{ => group3}/embeddables/index.ts | 2 +- .../ml/{ => group3}/feature_controls/index.ts | 2 +- .../feature_controls/ml_security.ts | 2 +- .../feature_controls/ml_spaces.ts | 2 +- .../test/functional/apps/ml/group3/index.ts | 45 ++++ .../settings/calendar_creation.ts | 2 +- .../{ => group3}/settings/calendar_delete.ts | 2 +- .../ml/{ => group3}/settings/calendar_edit.ts | 2 +- .../apps/ml/{ => group3}/settings/common.ts | 5 +- .../settings/filter_list_creation.ts | 2 +- .../settings/filter_list_delete.ts | 2 +- .../{ => group3}/settings/filter_list_edit.ts | 2 +- .../apps/ml/{ => group3}/settings/index.ts | 2 +- .../stack_management_jobs/export_jobs.ts | 2 +- .../anomaly_detection_jobs_7.16.json | 0 .../files_to_import/bad_data.json | 0 .../data_frame_analytics_jobs_7.16.json | 0 .../stack_management_jobs/import_jobs.ts | 10 +- .../stack_management_jobs/index.ts | 2 +- .../stack_management_jobs/manage_spaces.ts | 2 +- .../stack_management_jobs/synchronize.ts | 2 +- .../test/functional/apps/monitoring/config.ts | 17 ++ .../test/functional/apps/monitoring/index.js | 1 - .../functional/apps/remote_clusters/config.ts | 17 ++ .../functional/apps/remote_clusters/index.ts | 2 +- .../test/functional/apps/reporting/README.md | 2 +- .../apps/reporting_management/config.ts | 17 ++ .../apps/reporting_management/index.js | 1 - .../test/functional/apps/rollup_job/config.ts | 17 ++ .../test/functional/apps/rollup_job/index.js | 2 - .../apps/saved_objects_management/config.ts | 17 ++ .../apps/saved_objects_management/index.ts | 2 +- .../apps/security/basic_license/index.ts | 2 - .../test/functional/apps/security/config.ts | 17 ++ x-pack/test/functional/apps/security/index.ts | 2 - .../apps/snapshot_restore/config.ts | 17 ++ .../functional/apps/snapshot_restore/index.ts | 2 +- x-pack/test/functional/apps/spaces/config.ts | 17 ++ x-pack/test/functional/apps/spaces/index.ts | 2 - .../functional/apps/status_page/config.ts | 17 ++ .../test/functional/apps/status_page/index.ts | 2 - .../test/functional/apps/transform/config.ts | 17 ++ .../test/functional/apps/transform/index.ts | 2 +- .../apps/upgrade_assistant/config.ts | 17 ++ .../apps/upgrade_assistant/index.ts | 2 - x-pack/test/functional/apps/uptime/config.ts | 17 ++ x-pack/test/functional/apps/uptime/index.ts | 2 - .../test/functional/apps/visualize/config.ts | 17 ++ .../test/functional/apps/visualize/index.ts | 2 +- x-pack/test/functional/apps/watcher/config.ts | 17 ++ x-pack/test/functional/apps/watcher/index.js | 2 +- .../functional/{config.js => config.base.js} | 44 +--- x-pack/test/functional/config.ccs.ts | 4 +- x-pack/test/functional/config.coverage.js | 22 -- x-pack/test/functional/config.edge.js | 4 +- x-pack/test/functional/config.firefox.js | 12 +- .../test/functional/config_security_basic.ts | 2 +- .../services/ml/common_data_grid.ts | 4 +- x-pack/test/functional_basic/apps/ml/index.ts | 2 +- .../apps/ml/permissions/full_ml_access.ts | 16 +- .../apps/ml/permissions/read_ml_access.ts | 16 +- x-pack/test/functional_basic/config.ts | 4 +- x-pack/test/functional_cors/config.ts | 4 +- x-pack/test/functional_cors/tests/index.ts | 1 - x-pack/test/functional_embedded/config.ts | 4 +- .../test/functional_embedded/tests/index.ts | 1 - .../without_host_configured/index.ts | 2 - .../base_config.ts | 4 +- .../functional_execution_context/config.ts | 2 +- .../tests/index.ts | 1 - .../apps/uptime/index.ts | 1 - x-pack/test/functional_synthetics/config.js | 2 +- .../apps/cases/index.ts | 1 - .../apps/discover/index.ts | 1 - .../apps/ml/alert_flyout.ts | 2 - .../apps/triggers_actions_ui/index.ts | 1 - .../apps/uptime/index.ts | 2 - x-pack/test/functional_with_es_ssl/config.ts | 4 +- x-pack/test/licensing_plugin/config.ts | 4 +- x-pack/test/licensing_plugin/public/index.ts | 1 - x-pack/test/licensing_plugin/server/index.ts | 1 - .../security_and_spaces/tests/index.ts | 2 - x-pack/test/load/config.ts | 2 +- .../basic/tests/index.ts | 1 - .../trial/tests/index.ts | 1 - .../apps/observability/index.ts | 2 - .../with_rac_write.config.ts | 4 +- x-pack/test/osquery_cypress/config.ts | 2 +- x-pack/test/performance/config.playwright.ts | 2 +- .../test_suites/event_log/index.ts | 1 - .../licensed_feature_usage/index.ts | 1 - .../test_suites/platform/index.ts | 1 - .../test_suites/task_manager/index.ts | 1 - x-pack/test/plugin_api_perf/README.md | 2 +- .../test_suites/task_manager/index.ts | 1 - x-pack/test/plugin_functional/config.ts | 4 +- .../test_suites/global_search/index.ts | 1 - .../test_suites/resolver/index.ts | 2 - .../test_suites/timelines/index.ts | 1 - .../reporting_and_security/index.ts | 2 - .../reporting_without_security/index.ts | 2 +- .../index.ts | 2 - .../reporting_and_security.config.ts | 2 +- .../reporting_and_security/index.ts | 2 - .../reporting_without_security/index.ts | 2 - .../security_and_spaces/tests/basic/index.ts | 3 - .../tests/basic/search_strategy.ts | 2 +- .../security_and_spaces/tests/trial/index.ts | 3 - .../spaces_only/tests/basic/index.ts | 3 - .../spaces_only/tests/trial/index.ts | 3 - .../common/config.ts | 4 +- .../security_and_spaces/apis/index.ts | 2 - .../spaces_only/apis/index.ts | 2 - .../security_and_spaces/apis/index.ts | 2 - .../api_integration/tagging_api/apis/index.ts | 2 - .../saved_object_tagging/functional/config.ts | 2 +- .../functional/tests/index.ts | 2 - .../test/saved_objects_field_count/config.ts | 7 +- .../test/saved_objects_field_count/runner.ts | 67 ----- x-pack/test/saved_objects_field_count/test.ts | 73 ++++++ x-pack/test/screenshot_creation/config.ts | 4 +- .../search_sessions_integration/config.ts | 4 +- .../apps/dashboard/async_search/index.ts | 2 - .../apps/dashboard/session_sharing/index.ts | 2 - .../tests/apps/discover/index.ts | 2 - .../tests/apps/lens/index.ts | 2 - .../apps/management/search_sessions/index.ts | 2 - .../tests/anonymous/index.ts | 1 - .../tests/audit/index.ts | 1 - .../tests/http_bearer/index.ts | 1 - .../tests/http_no_auth_providers/index.ts | 1 - .../tests/kerberos/index.ts | 2 - .../tests/login_selector/index.ts | 1 - .../oidc/authorization_code_flow/index.ts | 1 - .../tests/oidc/implicit_flow/index.ts | 1 - .../tests/pki/index.ts | 2 - .../tests/saml/index.ts | 2 - .../tests/session_idle/index.ts | 2 - .../tests/session_invalidate/index.ts | 2 - .../tests/session_lifespan/index.ts | 2 - .../tests/token/index.ts | 1 - .../login_selector.config.ts | 2 +- .../test/security_functional/oidc.config.ts | 2 +- .../test/security_functional/saml.config.ts | 2 +- .../tests/login_selector/index.ts | 2 - .../security_functional/tests/oidc/index.ts | 2 - .../security_functional/tests/saml/index.ts | 2 - .../config.firefox.ts | 2 +- .../test/security_solution_cypress/config.ts | 2 +- .../apps/endpoint/index.ts | 1 - .../test/security_solution_endpoint/config.ts | 4 +- .../apis/index.ts | 2 - .../spaces_api_integration/common/config.ts | 4 +- .../security_and_spaces/apis/index.ts | 2 - .../spaces_only/apis/index.ts | 2 - .../apps/telemetry/index.js | 1 - ...onfig.stack_functional_integration_base.js | 4 +- .../security_and_spaces/tests/basic/index.ts | 3 - .../security_and_spaces/tests/trial/index.ts | 3 - x-pack/test/ui_capabilities/common/config.ts | 2 +- .../security_and_spaces/tests/index.ts | 2 - .../spaces_only/tests/index.ts | 2 - x-pack/test/upgrade/config.ts | 2 +- .../upgrade_assistant_integration/config.js | 2 +- .../upgrade_assistant/index.js | 2 - x-pack/test/usage_collection/config.ts | 4 +- .../test_suites/application_usage/index.ts | 1 - .../stack_management_usage/index.ts | 1 - x-pack/test/visual_regression/config.ts | 2 +- .../visual_regression/tests/canvas/index.js | 1 - .../visual_regression/tests/infra/index.js | 1 - .../visual_regression/tests/maps/index.js | 1 - 850 files changed, 4367 insertions(+), 2774 deletions(-) create mode 100644 .buildkite/ftr_configs.yml delete mode 100755 .buildkite/scripts/steps/checks/validate_ci_groups.sh delete mode 100755 .buildkite/scripts/steps/functional/oss_accessibility.sh delete mode 100755 .buildkite/scripts/steps/functional/oss_cigroup.sh delete mode 100755 .buildkite/scripts/steps/functional/oss_firefox.sh delete mode 100755 .buildkite/scripts/steps/functional/oss_misc.sh delete mode 100755 .buildkite/scripts/steps/functional/xpack_accessibility.sh delete mode 100755 .buildkite/scripts/steps/functional/xpack_cigroup.sh delete mode 100755 .buildkite/scripts/steps/functional/xpack_firefox.sh delete mode 100755 .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh delete mode 100755 .buildkite/scripts/steps/test/api_integration.sh create mode 100755 .buildkite/scripts/steps/test/ftr_configs.sh delete mode 100644 .buildkite/scripts/steps/test/pick_jest_config_run_order.sh rename .buildkite/scripts/steps/test/{pick_jest_config_run_order.js => pick_test_group_run_order.js} (100%) create mode 100644 .buildkite/scripts/steps/test/pick_test_group_run_order.sh rename .buildkite/scripts/steps/test/{jest_env.sh => test_group_env.sh} (80%) create mode 100644 packages/kbn-test/src/functional_test_runner/lib/config/ftr_configs_manifest.ts rename packages/kbn-test/src/functional_test_runner/lib/config/{read_config_file.test.js => read_config_file.test.ts} (70%) delete mode 100644 packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts rename test/functional/{config.coverage.js => apps/bundles/config.ts} (55%) create mode 100644 test/functional/apps/console/config.ts create mode 100644 test/functional/apps/context/config.ts create mode 100644 test/functional/apps/dashboard/README.md create mode 100644 test/functional/apps/dashboard/group1/config.ts rename test/functional/apps/dashboard/{ => group1}/create_and_add_embeddables.ts (99%) rename test/functional/apps/dashboard/{ => group1}/dashboard_back_button.ts (96%) rename test/functional/apps/dashboard/{ => group1}/dashboard_error_handling.ts (97%) rename test/functional/apps/dashboard/{ => group1}/dashboard_options.ts (96%) rename test/functional/apps/dashboard/{ => group1}/dashboard_query_bar.ts (96%) rename test/functional/apps/dashboard/{ => group1}/dashboard_unsaved_listing.ts (99%) rename test/functional/apps/dashboard/{ => group1}/dashboard_unsaved_state.ts (99%) rename test/functional/apps/dashboard/{ => group1}/data_shared_attributes.ts (98%) rename test/functional/apps/dashboard/{ => group1}/edit_embeddable_redirects.ts (98%) rename test/functional/apps/dashboard/{ => group1}/edit_visualizations.js (100%) rename test/functional/apps/dashboard/{ => group1}/embed_mode.ts (98%) rename test/functional/apps/dashboard/{ => group1}/embeddable_data_grid.ts (97%) rename test/functional/apps/dashboard/{ => group1}/embeddable_rendering.ts (99%) rename test/functional/apps/dashboard/{ => group1}/empty_dashboard.ts (97%) create mode 100644 test/functional/apps/dashboard/group1/index.ts rename test/functional/apps/dashboard/{ => group1}/legacy_urls.ts (98%) rename test/functional/apps/dashboard/{ => group1}/saved_search_embeddable.ts (98%) rename test/functional/apps/dashboard/{ => group1}/share.ts (95%) rename test/functional/apps/dashboard/{ => group1}/url_field_formatter.ts (95%) create mode 100644 test/functional/apps/dashboard/group2/config.ts rename test/functional/apps/dashboard/{ => group2}/dashboard_filter_bar.ts (99%) rename test/functional/apps/dashboard/{ => group2}/dashboard_filtering.ts (99%) rename test/functional/apps/dashboard/{ => group2}/dashboard_grid.ts (96%) rename test/functional/apps/dashboard/{ => group2}/dashboard_saved_query.ts (98%) rename test/functional/apps/dashboard/{ => group2}/dashboard_snapshots.ts (98%) rename test/functional/apps/dashboard/{ => group2}/embeddable_library.ts (97%) rename test/functional/apps/dashboard/{ => group2}/full_screen_mode.ts (98%) create mode 100644 test/functional/apps/dashboard/group2/index.ts rename test/functional/apps/dashboard/{ => group2}/panel_expand_toggle.ts (97%) rename test/functional/apps/dashboard/{ => group2}/view_edit.ts (99%) rename test/functional/apps/dashboard/{ => group3}/bwc_shared_urls.ts (99%) create mode 100644 test/functional/apps/dashboard/group3/config.ts rename test/functional/apps/dashboard/{ => group3}/copy_panel_to.ts (98%) rename test/functional/apps/dashboard/{ => group3}/dashboard_state.ts (98%) rename test/functional/apps/dashboard/{ => group3}/dashboard_time_picker.ts (97%) create mode 100644 test/functional/apps/dashboard/group3/index.ts rename test/functional/apps/dashboard/{ => group3}/panel_cloning.ts (96%) rename test/functional/apps/dashboard/{ => group3}/panel_context_menu.ts (98%) rename test/functional/apps/dashboard/{ => group3}/panel_replacing.ts (97%) create mode 100644 test/functional/apps/dashboard/group4/config.ts rename test/functional/apps/dashboard/{ => group4}/dashboard_clone.ts (97%) rename test/functional/apps/dashboard/{ => group4}/dashboard_listing.ts (99%) rename test/functional/apps/dashboard/{ => group4}/dashboard_save.ts (98%) rename test/functional/apps/dashboard/{ => group4}/dashboard_time.ts (98%) create mode 100644 test/functional/apps/dashboard/group4/index.ts create mode 100644 test/functional/apps/dashboard/group5/config.ts create mode 100644 test/functional/apps/dashboard/group5/index.ts delete mode 100644 test/functional/apps/dashboard/index.ts create mode 100644 test/functional/apps/dashboard_elements/config.ts create mode 100644 test/functional/apps/discover/config.ts create mode 100644 test/functional/apps/getting_started/config.ts create mode 100644 test/functional/apps/home/config.ts create mode 100644 test/functional/apps/management/config.ts create mode 100644 test/functional/apps/saved_objects_management/config.ts create mode 100644 test/functional/apps/status_page/config.ts create mode 100644 test/functional/apps/visualize/README.md rename test/functional/apps/visualize/{ => group1}/_chart_types.ts (96%) rename test/functional/apps/visualize/{ => group1}/_data_table.ts (99%) rename test/functional/apps/visualize/{ => group1}/_data_table_nontimeindex.ts (98%) rename test/functional/apps/visualize/{ => group1}/_data_table_notimeindex_filters.ts (97%) rename test/functional/apps/visualize/{ => group1}/_embedding_chart.ts (98%) create mode 100644 test/functional/apps/visualize/group1/config.ts create mode 100644 test/functional/apps/visualize/group1/index.ts rename test/functional/apps/visualize/{ => group2}/_experimental_vis.ts (97%) rename test/functional/apps/visualize/{ => group2}/_gauge_chart.ts (98%) rename test/functional/apps/visualize/{ => group2}/_heatmap_chart.ts (98%) rename test/functional/apps/visualize/{ => group2}/_histogram_request_start.ts (98%) rename test/functional/apps/visualize/{ => group2}/_inspector.ts (98%) rename test/functional/apps/visualize/{ => group2}/_metric_chart.ts (99%) create mode 100644 test/functional/apps/visualize/group2/config.ts create mode 100644 test/functional/apps/visualize/group2/index.ts rename test/functional/apps/visualize/{ => group3}/_add_to_dashboard.ts (99%) rename test/functional/apps/visualize/{ => group3}/_lab_mode.ts (97%) rename test/functional/apps/visualize/{ => group3}/_linked_saved_searches.ts (98%) rename test/functional/apps/visualize/{ => group3}/_pie_chart.ts (95%) rename test/functional/apps/visualize/{ => group3}/_shared_item.ts (95%) rename test/functional/apps/visualize/{ => group3}/_visualize_listing.ts (98%) create mode 100644 test/functional/apps/visualize/group3/config.ts create mode 100644 test/functional/apps/visualize/group3/index.ts rename test/functional/apps/visualize/{ => group4}/_tsvb_chart.ts (99%) create mode 100644 test/functional/apps/visualize/group4/config.ts create mode 100644 test/functional/apps/visualize/group4/index.ts rename test/functional/apps/visualize/{ => group5}/_tsvb_time_series.ts (99%) create mode 100644 test/functional/apps/visualize/group5/config.ts create mode 100644 test/functional/apps/visualize/group5/index.ts rename test/functional/apps/visualize/{ => group6}/_tag_cloud.ts (99%) rename test/functional/apps/visualize/{ => group6}/_tsvb_markdown.ts (99%) rename test/functional/apps/visualize/{ => group6}/_tsvb_table.ts (99%) rename test/functional/apps/visualize/{ => group6}/_vega_chart.ts (99%) create mode 100644 test/functional/apps/visualize/group6/config.ts create mode 100644 test/functional/apps/visualize/group6/index.ts delete mode 100644 test/functional/apps/visualize/index.ts rename test/functional/apps/visualize/{ => replaced_vislib_chart_types}/_area_chart.ts (99%) rename test/functional/apps/visualize/{ => replaced_vislib_chart_types}/_line_chart_split_chart.ts (99%) rename test/functional/apps/visualize/{ => replaced_vislib_chart_types}/_line_chart_split_series.ts (99%) rename test/functional/apps/visualize/{ => replaced_vislib_chart_types}/_point_series_options.ts (99%) rename test/functional/apps/visualize/{ => replaced_vislib_chart_types}/_timelion.ts (99%) rename test/functional/apps/visualize/{ => replaced_vislib_chart_types}/_vertical_bar_chart.ts (99%) rename test/functional/apps/visualize/{ => replaced_vislib_chart_types}/_vertical_bar_chart_nontimeindex.ts (99%) create mode 100644 test/functional/apps/visualize/replaced_vislib_chart_types/config.ts create mode 100644 test/functional/apps/visualize/replaced_vislib_chart_types/index.ts rename test/functional/{config.js => config.base.js} (95%) rename test/server_integration/{config.js => config.base.js} (97%) delete mode 100644 x-pack/scripts/functional_tests.js rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group1}/config.ts (82%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group1}/tests/alerting/create.ts (99%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group1}/tests/alerting/delete.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group1}/tests/alerting/disable.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group1}/tests/alerting/enable.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group1}/tests/alerting/execution_status.ts (95%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group1}/tests/alerting/find.ts (99%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group1}/tests/alerting/get.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group1}/tests/alerting/get_alert_state.ts (97%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group1}/tests/alerting/get_alert_summary.ts (97%) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group1}/tests/alerting/rule_types.ts (96%) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/index.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/email.ts (99%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/es_index.ts (99%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/es_index_preconfigured.ts (96%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/jira.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/pagerduty.ts (96%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/resilient.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/server_log.ts (96%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/servicenow_itom.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/servicenow_itsm.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/servicenow_sir.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/slack.ts (96%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/swimlane.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/webhook.ts (97%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/builtin_action_types/xmatters.ts (96%) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/connector_types.ts (91%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/create.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/delete.ts (97%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/execute.ts (99%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/get.ts (96%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/get_all.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/index.ts (93%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/manual/pr_40694.js (100%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/actions/update.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/alerts.ts (99%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/event_log.ts (92%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/excluded.ts (94%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/health.ts (97%) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/index.ts rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/mustache_templates.ts (92%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/mute_all.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/mute_instance.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/rbac_legacy.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/snooze.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/unmute_all.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/unmute_instance.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/unsnooze.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/update.ts (99%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/alerting/update_api_key.ts (98%) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/index.ts rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/telemetry/actions_telemetry.ts (98%) rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/telemetry/alerting_telemetry.ts (99%) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/config.ts rename x-pack/test/alerting_api_integration/security_and_spaces/{ => group2}/tests/telemetry/index.ts (84%) rename x-pack/test/alerting_api_integration/security_and_spaces/{tests/index.ts => setup.ts} (73%) delete mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/README.md rename x-pack/test/detection_engine_api_integration/security_and_spaces/{config.ts => config.base.ts} (87%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/add_actions.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/add_prepackaged_rules.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/aliases.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/check_privileges.ts (100%) rename x-pack/test/{functional_embedded/config.firefox.ts => detection_engine_api_integration/security_and_spaces/group1/config.ts} (54%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/create_index.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/create_ml.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/create_rules.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/create_rules_bulk.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/create_signals_migrations.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/create_threat_matching.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/delete_rules.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/delete_rules_bulk.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/delete_signals_migrations.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/export_rules.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/finalize_signals_migrations.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/find_rules.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/generating_signals.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/get_prepackaged_rules_status.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/get_rule_execution_events.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/get_signals_migration_status.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/ignore_fields.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/import_export_rules.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/import_rules.ts (100%) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/legacy_actions_migrations.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/migrations.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/open_close_signals.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/patch_rules.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/patch_rules_bulk.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/perform_bulk_action.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/preview_rules.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/read_privileges.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/read_rules.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/resolve_read_rules.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/runtime.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/template_data/execution_events.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/throttle.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/timestamps.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/update_actions.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/update_rules.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group1}/update_rules_bulk.ts (100%) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group2/config.ts rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group2}/create_endpoint_exceptions.ts (100%) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group2/index.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group3/config.ts rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group3}/create_exceptions.ts (100%) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group3/index.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group4/config.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group4/index.ts rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group4}/telemetry/README.md (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group4}/telemetry/index.ts (51%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group4}/telemetry/task_based/all_types.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group4}/telemetry/task_based/detection_rules.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group4}/telemetry/task_based/security_lists.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group4}/telemetry/usage_collector/all_types.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group4}/telemetry/usage_collector/detection_rule_status.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group4}/telemetry/usage_collector/detection_rules.ts (100%) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group5/config.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group5/index.ts rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group5}/keyword_family/README.md (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group5}/keyword_family/const_keyword.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group5}/keyword_family/index.ts (68%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group5}/keyword_family/keyword.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group5}/keyword_family/keyword_mixed_with_const.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group6}/alerts/alerts_compatibility.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group6}/alerts/index.ts (79%) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group6/config.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group6/index.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group7/config.ts rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group7}/exception_operators_data_types/date.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group7}/exception_operators_data_types/double.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group7}/exception_operators_data_types/float.ts (100%) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/index.ts rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group7}/exception_operators_data_types/integer.ts (100%) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group7/index.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group8/config.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/index.ts rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group8}/exception_operators_data_types/keyword.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group8}/exception_operators_data_types/keyword_array.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group8}/exception_operators_data_types/long.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group8}/exception_operators_data_types/text.ts (100%) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group8/index.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group9/config.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/index.ts rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group9}/exception_operators_data_types/ip.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group9}/exception_operators_data_types/ip_array.ts (100%) rename x-pack/test/detection_engine_api_integration/security_and_spaces/{tests => group9}/exception_operators_data_types/text_array.ts (100%) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group9/index.ts delete mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/README.md delete mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts delete mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts create mode 100644 x-pack/test/functional/apps/advanced_settings/config.ts create mode 100644 x-pack/test/functional/apps/api_keys/config.ts create mode 100644 x-pack/test/functional/apps/apm/config.ts create mode 100644 x-pack/test/functional/apps/canvas/config.ts create mode 100644 x-pack/test/functional/apps/cross_cluster_replication/config.ts create mode 100644 x-pack/test/functional/apps/dashboard/README.md create mode 100644 x-pack/test/functional/apps/dashboard/group1/config.ts rename x-pack/test/functional/apps/dashboard/{ => group1}/drilldowns/dashboard_to_dashboard_drilldown.ts (99%) rename x-pack/test/functional/apps/dashboard/{ => group1}/drilldowns/dashboard_to_url_drilldown.ts (98%) rename x-pack/test/functional/apps/dashboard/{ => group1}/drilldowns/explore_data_chart_action.ts (98%) rename x-pack/test/functional/apps/dashboard/{ => group1}/drilldowns/explore_data_panel_action.ts (98%) rename x-pack/test/functional/apps/dashboard/{ => group1}/drilldowns/index.ts (95%) rename x-pack/test/functional/apps/dashboard/{ => group1}/feature_controls/dashboard_security.ts (99%) rename x-pack/test/functional/apps/dashboard/{ => group1}/feature_controls/dashboard_spaces.ts (98%) rename x-pack/test/functional/apps/dashboard/{ => group1}/feature_controls/index.ts (89%) rename x-pack/test/functional/apps/dashboard/{ => group1}/feature_controls/time_to_visualize_security.ts (99%) create mode 100644 x-pack/test/functional/apps/dashboard/group1/index.ts rename x-pack/test/functional/apps/dashboard/{ => group1}/preserve_url.ts (97%) rename x-pack/test/functional/apps/dashboard/{ => group1}/reporting/README.md (87%) rename x-pack/test/functional/apps/dashboard/{ => group1}/reporting/__snapshots__/download_csv.snap (99%) rename x-pack/test/functional/apps/dashboard/{ => group1}/reporting/download_csv.ts (99%) rename x-pack/test/functional/apps/dashboard/{ => group1}/reporting/index.ts (86%) rename x-pack/test/functional/apps/dashboard/{ => group1}/reporting/reports/baseline/large_dashboard_preserve_layout.png (100%) rename x-pack/test/functional/apps/dashboard/{ => group1}/reporting/reports/baseline/small_dashboard_preserve_layout.png (100%) rename x-pack/test/functional/apps/dashboard/{ => group1}/reporting/screenshots.ts (99%) rename x-pack/test/functional/apps/dashboard/{ => group2}/_async_dashboard.ts (98%) create mode 100644 x-pack/test/functional/apps/dashboard/group2/config.ts rename x-pack/test/functional/apps/dashboard/{ => group2}/dashboard_lens_by_value.ts (98%) rename x-pack/test/functional/apps/dashboard/{ => group2}/dashboard_maps_by_value.ts (98%) rename x-pack/test/functional/apps/dashboard/{ => group2}/dashboard_tagging.ts (98%) create mode 100644 x-pack/test/functional/apps/dashboard/group2/index.ts rename x-pack/test/functional/apps/dashboard/{ => group2}/migration_smoke_tests/controls_migration_smoke_test.ts (98%) rename x-pack/test/functional/apps/dashboard/{ => group2}/migration_smoke_tests/exports/controls_dashboard_migration_test_8_0_0.ndjson (100%) rename x-pack/test/functional/apps/dashboard/{ => group2}/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson (100%) rename x-pack/test/functional/apps/dashboard/{ => group2}/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_12_1.ndjson (100%) rename x-pack/test/functional/apps/dashboard/{ => group2}/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_13_3.ndjson (100%) rename x-pack/test/functional/apps/dashboard/{ => group2}/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson (100%) rename x-pack/test/functional/apps/dashboard/{ => group2}/migration_smoke_tests/lens_migration_smoke_test.ts (97%) rename x-pack/test/functional/apps/dashboard/{ => group2}/migration_smoke_tests/tsvb_migration_smoke_test.ts (98%) rename x-pack/test/functional/apps/dashboard/{ => group2}/migration_smoke_tests/visualize_migration_smoke_test.ts (97%) rename x-pack/test/functional/apps/dashboard/{ => group2}/panel_titles.ts (99%) rename x-pack/test/functional/apps/dashboard/{ => group2}/sync_colors.ts (98%) delete mode 100644 x-pack/test/functional/apps/dashboard/index.ts create mode 100644 x-pack/test/functional/apps/data_views/config.ts create mode 100644 x-pack/test/functional/apps/dev_tools/config.ts create mode 100644 x-pack/test/functional/apps/discover/config.ts create mode 100644 x-pack/test/functional/apps/graph/config.ts create mode 100644 x-pack/test/functional/apps/grok_debugger/config.ts create mode 100644 x-pack/test/functional/apps/home/config.ts create mode 100644 x-pack/test/functional/apps/index_lifecycle_management/config.ts create mode 100644 x-pack/test/functional/apps/index_management/config.ts create mode 100644 x-pack/test/functional/apps/infra/config.ts create mode 100644 x-pack/test/functional/apps/ingest_pipelines/config.ts create mode 100644 x-pack/test/functional/apps/lens/README.md create mode 100644 x-pack/test/functional/apps/lens/group1/config.ts rename x-pack/test/functional/apps/lens/{ => group1}/index.ts (55%) rename x-pack/test/functional/apps/lens/{ => group1}/persistent_context.ts (99%) rename x-pack/test/functional/apps/lens/{ => group1}/smokescreen.ts (99%) rename x-pack/test/functional/apps/lens/{ => group1}/table.ts (99%) rename x-pack/test/functional/apps/lens/{ => group1}/table_dashboard.ts (97%) rename x-pack/test/functional/apps/lens/{ => group2}/add_to_dashboard.ts (99%) create mode 100644 x-pack/test/functional/apps/lens/group2/config.ts rename x-pack/test/functional/apps/lens/{ => group2}/dashboard.ts (99%) rename x-pack/test/functional/apps/lens/{ => group2}/epoch_millis.ts (97%) create mode 100644 x-pack/test/functional/apps/lens/group2/index.ts rename x-pack/test/functional/apps/lens/{ => group2}/multi_terms.ts (97%) rename x-pack/test/functional/apps/lens/{ => group2}/runtime_fields.ts (97%) rename x-pack/test/functional/apps/lens/{ => group2}/show_underlying_data.ts (99%) rename x-pack/test/functional/apps/lens/{ => group2}/show_underlying_data_dashboard.ts (98%) rename x-pack/test/functional/apps/lens/{ => group3}/annotations.ts (97%) rename x-pack/test/functional/apps/lens/{ => group3}/chart_data.ts (98%) rename x-pack/test/functional/apps/lens/{ => group3}/colors.ts (96%) create mode 100644 x-pack/test/functional/apps/lens/group3/config.ts rename x-pack/test/functional/apps/lens/{ => group3}/disable_auto_apply.ts (98%) rename x-pack/test/functional/apps/lens/{ => group3}/drag_and_drop.ts (99%) rename x-pack/test/functional/apps/lens/{ => group3}/error_handling.ts (97%) rename x-pack/test/functional/apps/lens/{ => group3}/formula.ts (99%) rename x-pack/test/functional/apps/lens/{ => group3}/gauge.ts (98%) rename x-pack/test/functional/apps/lens/{ => group3}/geo_field.ts (95%) rename x-pack/test/functional/apps/lens/{ => group3}/heatmap.ts (99%) create mode 100644 x-pack/test/functional/apps/lens/group3/index.ts rename x-pack/test/functional/apps/lens/{ => group3}/inspector.ts (96%) rename x-pack/test/functional/apps/lens/{ => group3}/lens_reporting.ts (96%) rename x-pack/test/functional/apps/lens/{ => group3}/lens_tagging.ts (98%) rename x-pack/test/functional/apps/lens/{ => group3}/metrics.ts (97%) rename x-pack/test/functional/apps/lens/{ => group3}/reference_lines.ts (98%) rename x-pack/test/functional/apps/lens/{ => group3}/rollup.ts (98%) rename x-pack/test/functional/apps/lens/{ => group3}/time_shift.ts (97%) rename x-pack/test/functional/apps/lens/{ => group3}/tsvb_open_in_lens.ts (99%) create mode 100644 x-pack/test/functional/apps/license_management/config.ts create mode 100644 x-pack/test/functional/apps/logstash/config.ts create mode 100644 x-pack/test/functional/apps/management/config.ts create mode 100644 x-pack/test/functional/apps/maps/README.md rename x-pack/test/functional/apps/maps/{ => group1}/auto_fit_to_bounds.js (100%) rename x-pack/test/functional/apps/maps/{ => group1}/blended_vector_layer.js (100%) create mode 100644 x-pack/test/functional/apps/maps/group1/config.ts rename x-pack/test/functional/apps/maps/{ => group1}/documents_source/docvalue_fields.js (100%) rename x-pack/test/functional/apps/maps/{ => group1}/documents_source/index.js (100%) rename x-pack/test/functional/apps/maps/{ => group1}/documents_source/search_hits.js (100%) rename x-pack/test/functional/apps/maps/{ => group1}/documents_source/top_hits.js (100%) rename x-pack/test/functional/apps/maps/{ => group1}/feature_controls/maps_security.ts (99%) rename x-pack/test/functional/apps/maps/{ => group1}/feature_controls/maps_spaces.ts (98%) rename x-pack/test/functional/apps/maps/{ => group1}/full_screen_mode.js (100%) create mode 100644 x-pack/test/functional/apps/maps/group1/index.js rename x-pack/test/functional/apps/maps/{ => group1}/layer_visibility.js (100%) rename x-pack/test/functional/apps/maps/{ => group1}/sample_data.js (100%) rename x-pack/test/functional/apps/maps/{ => group1}/saved_object_management.js (100%) rename x-pack/test/functional/apps/maps/{ => group1}/vector_styling.js (100%) create mode 100644 x-pack/test/functional/apps/maps/group2/config.ts rename x-pack/test/functional/apps/maps/{ => group2}/embeddable/add_to_dashboard.js (100%) rename x-pack/test/functional/apps/maps/{ => group2}/embeddable/dashboard.js (100%) rename x-pack/test/functional/apps/maps/{ => group2}/embeddable/embeddable_library.js (100%) rename x-pack/test/functional/apps/maps/{ => group2}/embeddable/embeddable_state.js (100%) rename x-pack/test/functional/apps/maps/{ => group2}/embeddable/filter_by_map_extent.js (100%) rename x-pack/test/functional/apps/maps/{ => group2}/embeddable/index.js (100%) rename x-pack/test/functional/apps/maps/{ => group2}/embeddable/save_and_return.js (100%) rename x-pack/test/functional/apps/maps/{ => group2}/embeddable/tooltip_filter_actions.js (100%) rename x-pack/test/functional/apps/maps/{ => group2}/es_geo_grid_source.js (100%) create mode 100644 x-pack/test/functional/apps/maps/group2/index.js create mode 100644 x-pack/test/functional/apps/maps/group3/config.ts create mode 100644 x-pack/test/functional/apps/maps/group3/index.js rename x-pack/test/functional/apps/maps/{ => group3}/reports/baseline/example_map_report.png (100%) rename x-pack/test/functional/apps/maps/{ => group3}/reports/baseline/geo_map_report.png (100%) rename x-pack/test/functional/apps/maps/{ => group3}/reports/index.ts (97%) rename x-pack/test/functional/apps/maps/{ => group4}/add_layer_panel.js (100%) create mode 100644 x-pack/test/functional/apps/maps/group4/config.ts rename x-pack/test/functional/apps/maps/{ => group4}/discover.js (100%) rename x-pack/test/functional/apps/maps/{ => group4}/es_pew_pew_source.js (100%) rename x-pack/test/functional/apps/maps/{ => group4}/file_upload/files/cb_2018_us_csa_500k.dbf (100%) rename x-pack/test/functional/apps/maps/{ => group4}/file_upload/files/cb_2018_us_csa_500k.prj (100%) rename x-pack/test/functional/apps/maps/{ => group4}/file_upload/files/cb_2018_us_csa_500k.shp (100%) rename x-pack/test/functional/apps/maps/{ => group4}/file_upload/files/cb_2018_us_csa_500k.shx (100%) rename x-pack/test/functional/apps/maps/{ => group4}/file_upload/files/point.json (100%) rename x-pack/test/functional/apps/maps/{ => group4}/file_upload/files/polygon.json (100%) rename x-pack/test/functional/apps/maps/{ => group4}/file_upload/files/world_countries_v7.geo.json (100%) rename x-pack/test/functional/apps/maps/{ => group4}/file_upload/geojson.js (100%) rename x-pack/test/functional/apps/maps/{ => group4}/file_upload/index.js (100%) rename x-pack/test/functional/apps/maps/{ => group4}/file_upload/shapefile.js (100%) rename x-pack/test/functional/apps/maps/{ => group4}/file_upload/wizard.js (100%) rename x-pack/test/functional/apps/maps/{ => group4}/geofile_wizard_auto_open.ts (95%) rename x-pack/test/functional/apps/maps/{ => group4}/index.js (57%) rename x-pack/test/functional/apps/maps/{ => group4}/joins.js (100%) rename x-pack/test/functional/apps/maps/{ => group4}/layer_errors.js (100%) rename x-pack/test/functional/apps/maps/{ => group4}/lens/choropleth_chart.ts (97%) rename x-pack/test/functional/apps/maps/{ => group4}/lens/index.ts (85%) rename x-pack/test/functional/apps/maps/{ => group4}/mapbox_styles.js (100%) rename x-pack/test/functional/apps/maps/{ => group4}/mvt_geotile_grid.js (100%) rename x-pack/test/functional/apps/maps/{ => group4}/mvt_joins.ts (98%) rename x-pack/test/functional/apps/maps/{ => group4}/mvt_scaling.js (100%) rename x-pack/test/functional/apps/maps/{ => group4}/visualize_create_menu.js (100%) create mode 100644 x-pack/test/functional/apps/ml/README.md create mode 100644 x-pack/test/functional/apps/ml/data_visualizer/config.ts create mode 100644 x-pack/test/functional/apps/ml/group1/config.ts rename x-pack/test/functional/apps/ml/{ => group1}/data_frame_analytics/classification_creation.ts (98%) rename x-pack/test/functional/apps/ml/{ => group1}/data_frame_analytics/classification_creation_saved_search.ts (98%) rename x-pack/test/functional/apps/ml/{ => group1}/data_frame_analytics/cloning.ts (99%) rename x-pack/test/functional/apps/ml/{ => group1}/data_frame_analytics/index.ts (93%) rename x-pack/test/functional/apps/ml/{ => group1}/data_frame_analytics/outlier_detection_creation.ts (98%) rename x-pack/test/functional/apps/ml/{ => group1}/data_frame_analytics/outlier_detection_creation_saved_search.ts (99%) rename x-pack/test/functional/apps/ml/{ => group1}/data_frame_analytics/regression_creation.ts (98%) rename x-pack/test/functional/apps/ml/{ => group1}/data_frame_analytics/regression_creation_saved_search.ts (98%) rename x-pack/test/functional/apps/ml/{ => group1}/data_frame_analytics/results_view_content.ts (99%) rename x-pack/test/functional/apps/ml/{ => group1}/index.ts (64%) rename x-pack/test/functional/apps/ml/{ => group1}/model_management/index.ts (86%) rename x-pack/test/functional/apps/ml/{ => group1}/model_management/model_list.ts (99%) rename x-pack/test/functional/apps/ml/{ => group1}/pages.ts (94%) rename x-pack/test/functional/apps/ml/{ => group1}/permissions/full_ml_access.ts (98%) rename x-pack/test/functional/apps/ml/{ => group1}/permissions/index.ts (88%) rename x-pack/test/functional/apps/ml/{ => group1}/permissions/no_ml_access.ts (93%) rename x-pack/test/functional/apps/ml/{ => group1}/permissions/read_ml_access.ts (98%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/advanced_job.ts (99%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/aggregated_scripted_job.ts (99%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/annotations.ts (99%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/anomaly_explorer.ts (99%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/categorization_job.ts (99%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/custom_urls.ts (98%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/date_nanos_job.ts (99%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/forecasts.ts (98%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/index.ts (94%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/multi_metric_job.ts (99%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/population_job.ts (99%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/saved_search_job.ts (99%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/single_metric_job.ts (99%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/single_metric_job_without_datafeed_start.ts (98%) rename x-pack/test/functional/apps/ml/{ => group2}/anomaly_detection/single_metric_viewer.ts (99%) create mode 100644 x-pack/test/functional/apps/ml/group2/config.ts create mode 100644 x-pack/test/functional/apps/ml/group2/index.ts create mode 100644 x-pack/test/functional/apps/ml/group3/config.ts rename x-pack/test/functional/apps/ml/{ => group3}/embeddables/anomaly_charts_dashboard_embeddables.ts (98%) rename x-pack/test/functional/apps/ml/{ => group3}/embeddables/anomaly_embeddables_migration.ts (98%) rename x-pack/test/functional/apps/ml/{ => group3}/embeddables/constants.ts (100%) rename x-pack/test/functional/apps/ml/{ => group3}/embeddables/index.ts (88%) rename x-pack/test/functional/apps/ml/{ => group3}/feature_controls/index.ts (87%) rename x-pack/test/functional/apps/ml/{ => group3}/feature_controls/ml_security.ts (98%) rename x-pack/test/functional/apps/ml/{ => group3}/feature_controls/ml_spaces.ts (97%) create mode 100644 x-pack/test/functional/apps/ml/group3/index.ts rename x-pack/test/functional/apps/ml/{ => group3}/settings/calendar_creation.ts (98%) rename x-pack/test/functional/apps/ml/{ => group3}/settings/calendar_delete.ts (96%) rename x-pack/test/functional/apps/ml/{ => group3}/settings/calendar_edit.ts (98%) rename x-pack/test/functional/apps/ml/{ => group3}/settings/common.ts (89%) rename x-pack/test/functional/apps/ml/{ => group3}/settings/filter_list_creation.ts (96%) rename x-pack/test/functional/apps/ml/{ => group3}/settings/filter_list_delete.ts (96%) rename x-pack/test/functional/apps/ml/{ => group3}/settings/filter_list_edit.ts (97%) rename x-pack/test/functional/apps/ml/{ => group3}/settings/index.ts (91%) rename x-pack/test/functional/apps/ml/{ => group3}/stack_management_jobs/export_jobs.ts (99%) rename x-pack/test/functional/apps/ml/{ => group3}/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json (100%) rename x-pack/test/functional/apps/ml/{ => group3}/stack_management_jobs/files_to_import/bad_data.json (100%) rename x-pack/test/functional/apps/ml/{ => group3}/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json (100%) rename x-pack/test/functional/apps/ml/{ => group3}/stack_management_jobs/import_jobs.ts (92%) rename x-pack/test/functional/apps/ml/{ => group3}/stack_management_jobs/index.ts (89%) rename x-pack/test/functional/apps/ml/{ => group3}/stack_management_jobs/manage_spaces.ts (99%) rename x-pack/test/functional/apps/ml/{ => group3}/stack_management_jobs/synchronize.ts (98%) create mode 100644 x-pack/test/functional/apps/monitoring/config.ts create mode 100644 x-pack/test/functional/apps/remote_clusters/config.ts create mode 100644 x-pack/test/functional/apps/reporting_management/config.ts create mode 100644 x-pack/test/functional/apps/rollup_job/config.ts create mode 100644 x-pack/test/functional/apps/saved_objects_management/config.ts create mode 100644 x-pack/test/functional/apps/security/config.ts create mode 100644 x-pack/test/functional/apps/snapshot_restore/config.ts create mode 100644 x-pack/test/functional/apps/spaces/config.ts create mode 100644 x-pack/test/functional/apps/status_page/config.ts create mode 100644 x-pack/test/functional/apps/transform/config.ts create mode 100644 x-pack/test/functional/apps/upgrade_assistant/config.ts create mode 100644 x-pack/test/functional/apps/uptime/config.ts create mode 100644 x-pack/test/functional/apps/visualize/config.ts create mode 100644 x-pack/test/functional/apps/watcher/config.ts rename x-pack/test/functional/{config.js => config.base.js} (88%) delete mode 100644 x-pack/test/functional/config.coverage.js delete mode 100644 x-pack/test/saved_objects_field_count/runner.ts create mode 100644 x-pack/test/saved_objects_field_count/test.ts diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml new file mode 100644 index 00000000000000..26dd1b4bcfc3ca --- /dev/null +++ b/.buildkite/ftr_configs.yml @@ -0,0 +1,236 @@ +disabled: + # TODO: Enable once RBAC timeline search strategy test updated + - x-pack/test/timeline/security_and_spaces/config_basic.ts + + # Base config files, only necessary to inform config finding script + - test/functional/config.base.js + - x-pack/test/functional/config.base.js + - x-pack/test/detection_engine_api_integration/security_and_spaces/config.base.ts + - x-pack/test/functional_enterprise_search/base_config.ts + - test/server_integration/config.base.js + + # QA suites that are run out-of-band + - x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js + - x-pack/test/upgrade/config.ts + - test/functional/config.edge.js + - x-pack/test/functional/config.edge.js + + # Cypress configs, for now these are still run manually + - x-pack/test/fleet_cypress/cli_config.ts + - x-pack/test/fleet_cypress/config.ts + - x-pack/test/fleet_cypress/visual_config.ts + - x-pack/test/functional_enterprise_search/cypress.config.ts + - x-pack/test/osquery_cypress/cli_config.ts + - x-pack/test/osquery_cypress/config.ts + - x-pack/test/osquery_cypress/visual_config.ts + - x-pack/test/security_solution_cypress/cases_cli_config.ts + - x-pack/test/security_solution_cypress/ccs_config.ts + - x-pack/test/security_solution_cypress/cli_config.ts + - x-pack/test/security_solution_cypress/config.firefox.ts + - x-pack/test/security_solution_cypress/config.ts + - x-pack/test/security_solution_cypress/response_ops_cli_config.ts + - x-pack/test/security_solution_cypress/upgrade_config.ts + - x-pack/test/security_solution_cypress/visual_config.ts + - x-pack/test/functional_enterprise_search/with_host_configured.config.ts + - x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts + - x-pack/plugins/apm/ftr_e2e/ftr_config.ts + - x-pack/plugins/synthetics/e2e/config.ts + + # Configs that exist but weren't running in CI when this file was introduced + - test/visual_regression/config.ts + - x-pack/test/visual_regression/config.ts + - x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts + - x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/config.ts + - x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts + - x-pack/test/banners_functional/config.ts + - x-pack/test/cloud_integration/config.ts + - x-pack/test/performance/config.playwright.ts + - x-pack/test/load/config.ts + - x-pack/test/plugin_api_perf/config.js + - x-pack/test/screenshot_creation/config.ts + +enabled: + - test/accessibility/config.ts + - test/analytics/config.ts + - test/api_integration/config.js + - test/examples/config.js + - test/functional/apps/bundles/config.ts + - test/functional/apps/console/config.ts + - test/functional/apps/context/config.ts + - test/functional/apps/dashboard_elements/config.ts + - test/functional/apps/dashboard/group1/config.ts + - test/functional/apps/dashboard/group2/config.ts + - test/functional/apps/dashboard/group3/config.ts + - test/functional/apps/dashboard/group4/config.ts + - test/functional/apps/dashboard/group5/config.ts + - test/functional/apps/discover/config.ts + - test/functional/apps/getting_started/config.ts + - test/functional/apps/home/config.ts + - test/functional/apps/management/config.ts + - test/functional/apps/saved_objects_management/config.ts + - test/functional/apps/status_page/config.ts + - test/functional/apps/visualize/group1/config.ts + - test/functional/apps/visualize/group2/config.ts + - test/functional/apps/visualize/group3/config.ts + - test/functional/apps/visualize/group4/config.ts + - test/functional/apps/visualize/group5/config.ts + - test/functional/apps/visualize/group6/config.ts + - test/functional/apps/visualize/replaced_vislib_chart_types/config.ts + - test/functional/config.ccs.ts + - test/functional/config.firefox.js + - test/interactive_setup_api_integration/enrollment_flow.config.ts + - test/interactive_setup_api_integration/manual_configuration_flow_without_tls.config.ts + - test/interactive_setup_api_integration/manual_configuration_flow.config.ts + - test/interactive_setup_functional/enrollment_token.config.ts + - test/interactive_setup_functional/manual_configuration_without_security.config.ts + - test/interactive_setup_functional/manual_configuration_without_tls.config.ts + - test/interactive_setup_functional/manual_configuration.config.ts + - test/interpreter_functional/config.ts + - test/new_visualize_flow/config.ts + - test/plugin_functional/config.ts + - test/server_integration/http/platform/config.status.ts + - test/server_integration/http/platform/config.ts + - test/server_integration/http/ssl_redirect/config.js + - test/server_integration/http/ssl_with_p12_intermediate/config.js + - test/server_integration/http/ssl_with_p12/config.js + - test/server_integration/http/ssl/config.js + - test/ui_capabilities/newsfeed_err/config.ts + - x-pack/test/accessibility/config.ts + - x-pack/test/alerting_api_integration/basic/config.ts + - x-pack/test/alerting_api_integration/security_and_spaces/group1/config.ts + - x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts + - x-pack/test/alerting_api_integration/spaces_only/config.ts + - x-pack/test/api_integration_basic/config.ts + - x-pack/test/api_integration/config_security_basic.ts + - x-pack/test/api_integration/config_security_trial.ts + - x-pack/test/api_integration/config.ts + - x-pack/test/apm_api_integration/basic/config.ts + - x-pack/test/apm_api_integration/rules/config.ts + - x-pack/test/apm_api_integration/trial/config.ts + - x-pack/test/cases_api_integration/security_and_spaces/config_basic.ts + - x-pack/test/cases_api_integration/security_and_spaces/config_trial.ts + - x-pack/test/cases_api_integration/spaces_only/config.ts + - x-pack/test/detection_engine_api_integration/basic/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group1/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group2/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group3/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group4/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group5/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group6/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group7/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group8/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group9/config.ts + - x-pack/test/encrypted_saved_objects_api_integration/config.ts + - x-pack/test/endpoint_api_integration_no_ingest/config.ts + - x-pack/test/examples/config.ts + - x-pack/test/fleet_api_integration/config.ts + - x-pack/test/fleet_functional/config.ts + - x-pack/test/functional_basic/config.ts + - x-pack/test/functional_cors/config.ts + - x-pack/test/functional_embedded/config.ts + - x-pack/test/functional_enterprise_search/without_host_configured.config.ts + - x-pack/test/functional_execution_context/config.ts + - x-pack/test/functional_synthetics/config.js + - x-pack/test/functional_with_es_ssl/config.ts + - x-pack/test/functional/apps/advanced_settings/config.ts + - x-pack/test/functional/apps/api_keys/config.ts + - x-pack/test/functional/apps/apm/config.ts + - x-pack/test/functional/apps/canvas/config.ts + - x-pack/test/functional/apps/cross_cluster_replication/config.ts + - x-pack/test/functional/apps/dashboard/group1/config.ts + - x-pack/test/functional/apps/dashboard/group2/config.ts + - x-pack/test/functional/apps/data_views/config.ts + - x-pack/test/functional/apps/dev_tools/config.ts + - x-pack/test/functional/apps/discover/config.ts + - x-pack/test/functional/apps/graph/config.ts + - x-pack/test/functional/apps/grok_debugger/config.ts + - x-pack/test/functional/apps/home/config.ts + - x-pack/test/functional/apps/index_lifecycle_management/config.ts + - x-pack/test/functional/apps/index_management/config.ts + - x-pack/test/functional/apps/infra/config.ts + - x-pack/test/functional/apps/ingest_pipelines/config.ts + - x-pack/test/functional/apps/lens/group1/config.ts + - x-pack/test/functional/apps/lens/group2/config.ts + - x-pack/test/functional/apps/lens/group3/config.ts + - x-pack/test/functional/apps/license_management/config.ts + - x-pack/test/functional/apps/logstash/config.ts + - x-pack/test/functional/apps/management/config.ts + - x-pack/test/functional/apps/maps/group1/config.ts + - x-pack/test/functional/apps/maps/group2/config.ts + - x-pack/test/functional/apps/maps/group3/config.ts + - x-pack/test/functional/apps/maps/group4/config.ts + - x-pack/test/functional/apps/ml/data_visualizer/config.ts + - x-pack/test/functional/apps/ml/group1/config.ts + - x-pack/test/functional/apps/ml/group2/config.ts + - x-pack/test/functional/apps/ml/group3/config.ts + - x-pack/test/functional/apps/monitoring/config.ts + - x-pack/test/functional/apps/remote_clusters/config.ts + - x-pack/test/functional/apps/reporting_management/config.ts + - x-pack/test/functional/apps/rollup_job/config.ts + - x-pack/test/functional/apps/saved_objects_management/config.ts + - x-pack/test/functional/apps/security/config.ts + - x-pack/test/functional/apps/snapshot_restore/config.ts + - x-pack/test/functional/apps/spaces/config.ts + - x-pack/test/functional/apps/status_page/config.ts + - x-pack/test/functional/apps/transform/config.ts + - x-pack/test/functional/apps/upgrade_assistant/config.ts + - x-pack/test/functional/apps/uptime/config.ts + - x-pack/test/functional/apps/visualize/config.ts + - x-pack/test/functional/apps/watcher/config.ts + - x-pack/test/functional/config_security_basic.ts + - x-pack/test/functional/config.ccs.ts + - x-pack/test/functional/config.firefox.js + - x-pack/test/licensing_plugin/config.public.ts + - x-pack/test/licensing_plugin/config.ts + - x-pack/test/lists_api_integration/security_and_spaces/config.ts + - x-pack/test/observability_api_integration/basic/config.ts + - x-pack/test/observability_api_integration/trial/config.ts + - x-pack/test/observability_functional/with_rac_write.config.ts + - x-pack/test/plugin_api_integration/config.ts + - x-pack/test/plugin_functional/config.ts + - x-pack/test/reporting_api_integration/reporting_and_security.config.ts + - x-pack/test/reporting_api_integration/reporting_without_security.config.ts + - x-pack/test/reporting_functional/reporting_and_deprecated_security.config.ts + - x-pack/test/reporting_functional/reporting_and_security.config.ts + - x-pack/test/reporting_functional/reporting_without_security.config.ts + - x-pack/test/rule_registry/security_and_spaces/config_basic.ts + - x-pack/test/rule_registry/security_and_spaces/config_trial.ts + - x-pack/test/rule_registry/spaces_only/config_basic.ts + - x-pack/test/rule_registry/spaces_only/config_trial.ts + - x-pack/test/saved_object_api_integration/security_and_spaces/config_basic.ts + - x-pack/test/saved_object_api_integration/security_and_spaces/config_trial.ts + - x-pack/test/saved_object_api_integration/spaces_only/config.ts + - x-pack/test/saved_object_tagging/api_integration/security_and_spaces/config.ts + - x-pack/test/saved_object_tagging/api_integration/tagging_api/config.ts + - x-pack/test/saved_object_tagging/functional/config.ts + - x-pack/test/saved_objects_field_count/config.ts + - x-pack/test/search_sessions_integration/config.ts + - x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts + - x-pack/test/security_api_integration/anonymous.config.ts + - x-pack/test/security_api_integration/audit.config.ts + - x-pack/test/security_api_integration/http_bearer.config.ts + - x-pack/test/security_api_integration/http_no_auth_providers.config.ts + - x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts + - x-pack/test/security_api_integration/kerberos.config.ts + - x-pack/test/security_api_integration/login_selector.config.ts + - x-pack/test/security_api_integration/oidc_implicit_flow.config.ts + - x-pack/test/security_api_integration/oidc.config.ts + - x-pack/test/security_api_integration/pki.config.ts + - x-pack/test/security_api_integration/saml.config.ts + - x-pack/test/security_api_integration/session_idle.config.ts + - x-pack/test/security_api_integration/session_invalidate.config.ts + - x-pack/test/security_api_integration/session_lifespan.config.ts + - x-pack/test/security_api_integration/token.config.ts + - x-pack/test/security_functional/login_selector.config.ts + - x-pack/test/security_functional/oidc.config.ts + - x-pack/test/security_functional/saml.config.ts + - x-pack/test/security_solution_endpoint_api_int/config.ts + - x-pack/test/security_solution_endpoint/config.ts + - x-pack/test/spaces_api_integration/security_and_spaces/config_basic.ts + - x-pack/test/spaces_api_integration/security_and_spaces/config_trial.ts + - x-pack/test/spaces_api_integration/spaces_only/config.ts + - x-pack/test/timeline/security_and_spaces/config_trial.ts + - x-pack/test/ui_capabilities/security_and_spaces/config.ts + - x-pack/test/ui_capabilities/spaces_only/config.ts + - x-pack/test/upgrade_assistant_integration/config.js + - x-pack/test/usage_collection/config.ts diff --git a/.buildkite/package-lock.json b/.buildkite/package-lock.json index cfbcb9ec1aae82..734db1a6ef234b 100644 --- a/.buildkite/package-lock.json +++ b/.buildkite/package-lock.json @@ -8,7 +8,7 @@ "name": "kibana-buildkite", "version": "1.0.0", "dependencies": { - "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#0f95579ace8100de9f1ad4cc16976b9ec6d5841e" + "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#f5381bea52e0a71f50a6919cb6357ff3262cf2d6" } }, "node_modules/@nodelib/fs.scandir": { @@ -355,8 +355,8 @@ }, "node_modules/kibana-buildkite-library": { "version": "1.0.0", - "resolved": "git+https://git@github.com/elastic/kibana-buildkite-library.git#0f95579ace8100de9f1ad4cc16976b9ec6d5841e", - "integrity": "sha512-Ayiyy3rAE/jOWcR65vxiv4zacMlpxuRZ+WKvly6magfClWTWIUTcW1aiOH2/PYWP3faiCbIDHOyxLeGGajk5dQ==", + "resolved": "git+https://git@github.com/elastic/kibana-buildkite-library.git#f5381bea52e0a71f50a6919cb6357ff3262cf2d6", + "integrity": "sha512-L1JP2NvXR7mhKn9JBwROPgTEV4vDr5HWwZtkdvxtHjZ/MeOnJYFSDqB4JUY/gXTz6v3CO3eUm3GQ0BP/kewoqQ==", "license": "MIT", "dependencies": { "@octokit/rest": "^18.10.0", @@ -801,9 +801,9 @@ } }, "kibana-buildkite-library": { - "version": "git+https://git@github.com/elastic/kibana-buildkite-library.git#0f95579ace8100de9f1ad4cc16976b9ec6d5841e", - "integrity": "sha512-Ayiyy3rAE/jOWcR65vxiv4zacMlpxuRZ+WKvly6magfClWTWIUTcW1aiOH2/PYWP3faiCbIDHOyxLeGGajk5dQ==", - "from": "kibana-buildkite-library@git+https://git@github.com/elastic/kibana-buildkite-library#0f95579ace8100de9f1ad4cc16976b9ec6d5841e", + "version": "git+https://git@github.com/elastic/kibana-buildkite-library.git#f5381bea52e0a71f50a6919cb6357ff3262cf2d6", + "integrity": "sha512-L1JP2NvXR7mhKn9JBwROPgTEV4vDr5HWwZtkdvxtHjZ/MeOnJYFSDqB4JUY/gXTz6v3CO3eUm3GQ0BP/kewoqQ==", + "from": "kibana-buildkite-library@git+https://git@github.com/elastic/kibana-buildkite-library#f5381bea52e0a71f50a6919cb6357ff3262cf2d6", "requires": { "@octokit/rest": "^18.10.0", "axios": "^0.21.4", diff --git a/.buildkite/package.json b/.buildkite/package.json index 551def1fa18003..079f200a4cbc92 100644 --- a/.buildkite/package.json +++ b/.buildkite/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "private": true, "dependencies": { - "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#0f95579ace8100de9f1ad4cc16976b9ec6d5841e" + "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#f5381bea52e0a71f50a6919cb6357ff3262cf2d6" } } diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index 729d0d1b37b5fe..0d8e11e1c8e8bd 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -27,50 +27,17 @@ steps: if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" timeout_in_minutes: 60 - - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh - label: 'Default CI Group' - parallelism: 31 - agents: - queue: n2-4 - depends_on: build - timeout_in_minutes: 150 - key: default-cigroup - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_cigroup.sh - label: 'OSS CI Group' - parallelism: 12 - agents: - queue: ci-group-4d - depends_on: build - timeout_in_minutes: 120 - key: oss-cigroup - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/test/pick_jest_config_run_order.sh - label: 'Pick Jest Config Run Order' + - command: .buildkite/scripts/steps/test/pick_test_group_run_order.sh + label: 'Pick Test Group Run Order' agents: queue: kibana-default env: - FILTER_JEST_CONFIG_TYPE: integration + LIMIT_CONFIG_TYPE: integration,functional retry: automatic: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/test/api_integration.sh - label: 'API Integration Tests' - agents: - queue: n2-2 - timeout_in_minutes: 120 - key: api-integration - - command: .buildkite/scripts/steps/es_snapshots/trigger_promote.sh label: Trigger promotion timeout_in_minutes: 10 diff --git a/.buildkite/pipelines/flaky_tests/groups.json b/.buildkite/pipelines/flaky_tests/groups.json index e471d5c6a8679f..78f8e6e56b9048 100644 --- a/.buildkite/pipelines/flaky_tests/groups.json +++ b/.buildkite/pipelines/flaky_tests/groups.json @@ -1,46 +1,20 @@ { "groups": [ { - "key": "oss/cigroup", - "name": "OSS CI Group", - "ciGroups": 12 - }, - { - "key": "oss/firefox", - "name": "OSS Firefox" - }, - { - "key": "oss/accessibility", - "name": "OSS Accessibility" - }, - { - "key": "xpack/cypress/security_solution", + "key": "cypress/security_solution", "name": "Security Solution - Cypress" }, { - "key": "xpack/cypress/osquery_cypress", + "key": "cypress/osquery_cypress", "name": "Osquery - Cypress" }, { - "key": "xpack/cypress/fleet_cypress", + "key": "cypress/fleet_cypress", "name": "Fleet - Cypress" }, { - "key": "xpack/cypress/apm_cypress", + "key": "cypress/apm_cypress", "name": "APM - Cypress" - }, - { - "key": "xpack/cigroup", - "name": "Default CI Group", - "ciGroups": 30 - }, - { - "key": "xpack/firefox", - "name": "Default Firefox" - }, - { - "key": "xpack/accessibility", - "name": "Default Accessibility" } ] } diff --git a/.buildkite/pipelines/flaky_tests/pipeline.js b/.buildkite/pipelines/flaky_tests/pipeline.js index 000f9954b76f1c..d62156a08c55ad 100644 --- a/.buildkite/pipelines/flaky_tests/pipeline.js +++ b/.buildkite/pipelines/flaky_tests/pipeline.js @@ -20,8 +20,18 @@ const groups = /** @type {Array<{key: string, name: string, ciGroups: number }>} require('./groups.json').groups ); -const concurrency = 25; -const initialJobs = 3; +const concurrency = process.env.KIBANA_FLAKY_TEST_CONCURRENCY + ? parseInt(process.env.KIBANA_FLAKY_TEST_CONCURRENCY, 10) + : 25; + +if (Number.isNaN(concurrency)) { + throw new Error( + `invalid KIBANA_FLAKY_TEST_CONCURRENCY: ${process.env.KIBANA_FLAKY_TEST_CONCURRENCY}` + ); +} + +const BASE_JOBS = 1; +const MAX_JOBS = 500; function getTestSuitesFromJson(json) { const fail = (errorMsg) => { @@ -41,20 +51,40 @@ function getTestSuitesFromJson(json) { fail(`JSON test config must be an array`); } - /** @type {Array<{ key: string, count: number }>} */ + /** @type {Array<{ type: 'group', key: string; count: number } | { type: 'ftrConfig', ftrConfig: string; count: number }>} */ const testSuites = []; for (const item of parsed) { if (typeof item !== 'object' || item === null) { fail(`testSuites must be objects`); } - const key = item.key; - if (typeof key !== 'string') { - fail(`testSuite.key must be a string`); - } + const count = item.count; if (typeof count !== 'number') { fail(`testSuite.count must be a number`); } + + const type = item.type; + if (type !== 'ftrConfig' && type !== 'group') { + fail(`testSuite.type must be either "ftrConfig" or "group"`); + } + + if (item.type === 'ftrConfig') { + const ftrConfig = item.ftrConfig; + if (typeof ftrConfig !== 'string') { + fail(`testSuite.ftrConfig must be a string`); + } + + testSuites.push({ + ftrConfig, + count, + }); + continue; + } + + const key = item.key; + if (typeof key !== 'string') { + fail(`testSuite.key must be a string`); + } testSuites.push({ key, count, @@ -65,13 +95,14 @@ function getTestSuitesFromJson(json) { } const testSuites = getTestSuitesFromJson(configJson); -const totalJobs = testSuites.reduce((acc, t) => acc + t.count, initialJobs); -if (totalJobs > 500) { +const totalJobs = testSuites.reduce((acc, t) => acc + t.count, BASE_JOBS); + +if (totalJobs > MAX_JOBS) { console.error('+++ Too many tests'); console.error( - `Buildkite builds can only contain 500 steps in total. Found ${totalJobs} in total. Make sure your test runs are less than ${ - 500 - initialJobs + `Buildkite builds can only contain ${MAX_JOBS} jobs in total. Found ${totalJobs} based on this config. Make sure your test runs are less than ${ + MAX_JOBS - BASE_JOBS }` ); process.exit(1); @@ -82,7 +113,7 @@ const pipeline = { env: { IGNORE_SHIP_CI_STATS_ERROR: 'true', }, - steps: steps, + steps, }; steps.push({ @@ -94,76 +125,40 @@ steps.push({ }); for (const testSuite of testSuites) { - const TEST_SUITE = testSuite.key; - const RUN_COUNT = testSuite.count; - const UUID = process.env.UUID; - - const JOB_PARTS = TEST_SUITE.split('/'); - const IS_XPACK = JOB_PARTS[0] === 'xpack'; - const TASK = JOB_PARTS[1]; - const CI_GROUP = JOB_PARTS.length > 2 ? JOB_PARTS[2] : ''; - - if (RUN_COUNT < 1) { + if (testSuite.count <= 0) { continue; } - switch (TASK) { - case 'cigroup': - if (IS_XPACK) { - steps.push({ - command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/xpack_cigroup.sh`, - label: `Default CI Group ${CI_GROUP}`, - agents: { queue: 'n2-4' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); - } else { - steps.push({ - command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/oss_cigroup.sh`, - label: `OSS CI Group ${CI_GROUP}`, - agents: { queue: 'ci-group-4d' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); - } - break; - - case 'firefox': - steps.push({ - command: `.buildkite/scripts/steps/functional/${IS_XPACK ? 'xpack' : 'oss'}_firefox.sh`, - label: `${IS_XPACK ? 'Default' : 'OSS'} Firefox`, - agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); - break; - - case 'accessibility': - steps.push({ - command: `.buildkite/scripts/steps/functional/${ - IS_XPACK ? 'xpack' : 'oss' - }_accessibility.sh`, - label: `${IS_XPACK ? 'Default' : 'OSS'} Accessibility`, - agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); - break; + if (testSuite.ftrConfig) { + steps.push({ + command: `.buildkite/scripts/steps/test/ftr_configs.sh`, + env: { + FTR_CONFIG: testSuite.ftrConfig, + }, + label: `FTR Config: ${testSuite.ftrConfig}`, + parallelism: testSuite.count, + concurrency: concurrency, + concurrency_group: process.env.UUID, + concurrency_method: 'eager', + agents: { + queue: 'n2-4-spot-2', + }, + depends_on: 'build', + timeout_in_minutes: 150, + retry: { + automatic: [ + { exit_status: '-1', limit: 3 }, + // { exit_status: '*', limit: 1 }, + ], + }, + }); + continue; + } + const keyParts = testSuite.key.split('/'); + switch (keyParts[0]) { case 'cypress': - const CYPRESS_SUITE = CI_GROUP; + const CYPRESS_SUITE = keyParts[1]; const group = groups.find((group) => group.key.includes(CYPRESS_SUITE)); if (!group) { throw new Error( @@ -175,12 +170,14 @@ for (const testSuite of testSuites) { label: group.name, agents: { queue: 'ci-group-6' }, depends_on: 'build', - parallelism: RUN_COUNT, + parallelism: testSuite.count, concurrency: concurrency, - concurrency_group: UUID, + concurrency_group: process.env.UUID, concurrency_method: 'eager', }); break; + default: + throw new Error(`unknown test suite: ${testSuite.key}`); } } diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index 1c89c4e90893b7..26b3afbf081a20 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -49,116 +49,8 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh - label: 'Default CI Group' - parallelism: 31 - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 250 - key: default-cigroup - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_cigroup.sh - label: 'OSS CI Group' - parallelism: 12 - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - key: oss-cigroup - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_accessibility.sh - label: 'OSS Accessibility Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh - label: 'Default Accessibility Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_firefox.sh - label: 'OSS Firefox Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/xpack_firefox.sh - label: 'Default Firefox Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_misc.sh - label: 'OSS Misc Functional Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh - label: 'Saved Object Field Metrics' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/test/pick_jest_config_run_order.sh - label: 'Pick Jest Config Run Order' + - command: .buildkite/scripts/steps/test/pick_test_group_run_order.sh + label: 'Pick Test Group Run Order' agents: queue: kibana-default retry: @@ -166,17 +58,6 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/test/api_integration.sh - label: 'API Integration Tests' - agents: - queue: n2-2-spot - timeout_in_minutes: 120 - key: api-integration - retry: - automatic: - - exit_status: '-1' - limit: 3 - - command: .buildkite/scripts/steps/lint.sh label: 'Linting' agents: diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 4b4104f18c627c..dc771e53d9d75f 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -15,116 +15,8 @@ steps: if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" timeout_in_minutes: 60 - - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh - label: 'Default CI Group' - parallelism: 31 - agents: - queue: n2-4-spot-2 - depends_on: build - timeout_in_minutes: 150 - key: default-cigroup - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_cigroup.sh - label: 'OSS CI Group' - parallelism: 12 - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - key: oss-cigroup - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_accessibility.sh - label: 'OSS Accessibility Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh - label: 'Default Accessibility Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_firefox.sh - label: 'OSS Firefox Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/xpack_firefox.sh - label: 'Default Firefox Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_misc.sh - label: 'OSS Misc Functional Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh - label: 'Saved Object Field Metrics' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/test/pick_jest_config_run_order.sh - label: 'Pick Jest Config Run Order' + - command: .buildkite/scripts/steps/test/pick_test_group_run_order.sh + label: 'Pick Test Group Run Order' agents: queue: kibana-default retry: @@ -132,17 +24,6 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/test/api_integration.sh - label: 'API Integration Tests' - agents: - queue: n2-2-spot - timeout_in_minutes: 120 - key: api-integration - retry: - automatic: - - exit_status: '-1' - limit: 3 - - command: .buildkite/scripts/steps/lint.sh label: 'Linting' agents: diff --git a/.buildkite/scripts/steps/checks.sh b/.buildkite/scripts/steps/checks.sh index 42de0f2cf23937..cae019150b626e 100755 --- a/.buildkite/scripts/steps/checks.sh +++ b/.buildkite/scripts/steps/checks.sh @@ -8,7 +8,6 @@ export DISABLE_BOOTSTRAP_VALIDATION=false .buildkite/scripts/steps/checks/commit/commit.sh .buildkite/scripts/steps/checks/bazel_packages.sh .buildkite/scripts/steps/checks/telemetry.sh -.buildkite/scripts/steps/checks/validate_ci_groups.sh .buildkite/scripts/steps/checks/ts_projects.sh .buildkite/scripts/steps/checks/jest_configs.sh .buildkite/scripts/steps/checks/doc_api_changes.sh diff --git a/.buildkite/scripts/steps/checks/validate_ci_groups.sh b/.buildkite/scripts/steps/checks/validate_ci_groups.sh deleted file mode 100755 index 1216ff0a55e10a..00000000000000 --- a/.buildkite/scripts/steps/checks/validate_ci_groups.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/common/util.sh - -echo --- Ensure that all tests are in a CI Group -checks-reporter-with-killswitch "Ensure that all tests are in a CI Group" \ - node scripts/ensure_all_tests_in_ci_group diff --git a/.buildkite/scripts/steps/functional/fleet_cypress.sh b/.buildkite/scripts/steps/functional/fleet_cypress.sh index 3847ffda088229..7207a2adf454aa 100755 --- a/.buildkite/scripts/steps/functional/fleet_cypress.sh +++ b/.buildkite/scripts/steps/functional/fleet_cypress.sh @@ -11,10 +11,8 @@ export JOB=kibana-fleet-cypress echo "--- Fleet Cypress tests" -cd "$XPACK_DIR" - checks-reporter-with-killswitch "Fleet Cypress Tests" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/fleet_cypress/cli_config.ts + --config x-pack/test/fleet_cypress/cli_config.ts diff --git a/.buildkite/scripts/steps/functional/osquery_cypress.sh b/.buildkite/scripts/steps/functional/osquery_cypress.sh index a0d98713aa379b..02766e0530bfb4 100755 --- a/.buildkite/scripts/steps/functional/osquery_cypress.sh +++ b/.buildkite/scripts/steps/functional/osquery_cypress.sh @@ -12,10 +12,8 @@ export JOB=kibana-osquery-cypress echo "--- Osquery Cypress tests" -cd "$XPACK_DIR" - checks-reporter-with-killswitch "Osquery Cypress Tests" \ node scripts/functional_tests \ --debug --bail \ - --config test/osquery_cypress/cli_config.ts + --config x-pack/test/osquery_cypress/cli_config.ts diff --git a/.buildkite/scripts/steps/functional/oss_accessibility.sh b/.buildkite/scripts/steps/functional/oss_accessibility.sh deleted file mode 100755 index e8c65cfa760b26..00000000000000 --- a/.buildkite/scripts/steps/functional/oss_accessibility.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -echo --- OSS Accessibility Tests - -checks-reporter-with-killswitch "Kibana accessibility tests" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/accessibility/config.ts diff --git a/.buildkite/scripts/steps/functional/oss_cigroup.sh b/.buildkite/scripts/steps/functional/oss_cigroup.sh deleted file mode 100755 index a7a5960a41afe7..00000000000000 --- a/.buildkite/scripts/steps/functional/oss_cigroup.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -export CI_GROUP=${CI_GROUP:-$((BUILDKITE_PARALLEL_JOB+1))} -export JOB=kibana-oss-ciGroup${CI_GROUP} - -echo "--- OSS CI Group $CI_GROUP" - -checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" \ - node scripts/functional_tests \ - --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --include-tag "ciGroup$CI_GROUP" diff --git a/.buildkite/scripts/steps/functional/oss_firefox.sh b/.buildkite/scripts/steps/functional/oss_firefox.sh deleted file mode 100755 index e953973da62d65..00000000000000 --- a/.buildkite/scripts/steps/functional/oss_firefox.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -echo --- OSS Firefox Smoke Tests - -checks-reporter-with-killswitch "Firefox smoke test" \ - node scripts/functional_tests \ - --bail --debug \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --include-tag "includeFirefox" \ - --config test/functional/config.firefox.js diff --git a/.buildkite/scripts/steps/functional/oss_misc.sh b/.buildkite/scripts/steps/functional/oss_misc.sh deleted file mode 100755 index 22d4eda608cc26..00000000000000 --- a/.buildkite/scripts/steps/functional/oss_misc.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -# Required, at least for plugin_functional tests -.buildkite/scripts/build_kibana_plugins.sh - -echo --- Plugin Functional Tests -checks-reporter-with-killswitch "Plugin Functional Tests" \ - node scripts/functional_tests \ - --config test/plugin_functional/config.ts \ - --bail \ - --debug - -echo --- Interpreter Functional Tests -checks-reporter-with-killswitch "Interpreter Functional Tests" \ - node scripts/functional_tests \ - --config test/interpreter_functional/config.ts \ - --bail \ - --debug \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" - -echo --- Server Integration Tests -checks-reporter-with-killswitch "Server Integration Tests" \ - node scripts/functional_tests \ - --config test/server_integration/http/ssl/config.js \ - --config test/server_integration/http/ssl_redirect/config.js \ - --config test/server_integration/http/platform/config.ts \ - --config test/server_integration/http/ssl_with_p12/config.js \ - --config test/server_integration/http/ssl_with_p12_intermediate/config.js \ - --bail \ - --debug \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" - -# Tests that must be run against source in order to build test plugins -echo --- Status Integration Tests -checks-reporter-with-killswitch "Status Integration Tests" \ - node scripts/functional_tests \ - --config test/server_integration/http/platform/config.status.ts \ - --bail \ - --debug - -# Tests that must be run against source in order to build test plugins -echo --- Analytics Integration Tests -checks-reporter-with-killswitch "Analytics Integration Tests" \ - node scripts/functional_tests \ - --config test/analytics/config.ts \ - --bail \ - --debug diff --git a/.buildkite/scripts/steps/functional/performance_playwright.sh b/.buildkite/scripts/steps/functional/performance_playwright.sh index dad75c9f66a983..a1c3f23ced51e2 100644 --- a/.buildkite/scripts/steps/functional/performance_playwright.sh +++ b/.buildkite/scripts/steps/functional/performance_playwright.sh @@ -18,8 +18,6 @@ export TEST_ES_DISABLE_STARTUP=true sleep 120 -cd "$XPACK_DIR" - journeys=("login" "ecommerce_dashboard" "flight_dashboard" "web_logs_dashboard" "promotion_tracking_dashboard" "many_fields_discover") for i in "${journeys[@]}"; do @@ -31,8 +29,8 @@ for i in "${journeys[@]}"; do checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Journey:${i},Phase: WARMUP)" \ node scripts/functional_tests \ - --config test/performance/config.playwright.ts \ - --include "test/performance/tests/playwright/${i}.ts" \ + --config x-pack/test/performance/config.playwright.ts \ + --include "x-pack/test/performance/tests/playwright/${i}.ts" \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ --debug \ --bail @@ -42,8 +40,8 @@ for i in "${journeys[@]}"; do checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Journey:${i},Phase: TEST)" \ node scripts/functional_tests \ - --config test/performance/config.playwright.ts \ - --include "test/performance/tests/playwright/${i}.ts" \ + --config x-pack/test/performance/config.playwright.ts \ + --include "x-pack/test/performance/tests/playwright/${i}.ts" \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ --debug \ --bail diff --git a/.buildkite/scripts/steps/functional/response_ops.sh b/.buildkite/scripts/steps/functional/response_ops.sh index d8484854ed29e0..9828884e6d6a2f 100755 --- a/.buildkite/scripts/steps/functional/response_ops.sh +++ b/.buildkite/scripts/steps/functional/response_ops.sh @@ -8,10 +8,8 @@ export JOB=kibana-security-solution-chrome echo "--- Response Ops Cypress Tests on Security Solution" -cd "$XPACK_DIR" - checks-reporter-with-killswitch "Response Ops Cypress Tests on Security Solution" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/security_solution_cypress/response_ops_cli_config.ts + --config x-pack/test/security_solution_cypress/response_ops_cli_config.ts diff --git a/.buildkite/scripts/steps/functional/response_ops_cases.sh b/.buildkite/scripts/steps/functional/response_ops_cases.sh index 13d0ef52130a3f..2485e025833ed4 100755 --- a/.buildkite/scripts/steps/functional/response_ops_cases.sh +++ b/.buildkite/scripts/steps/functional/response_ops_cases.sh @@ -8,10 +8,8 @@ export JOB=kibana-security-solution-chrome echo "--- Response Ops Cases Cypress Tests on Security Solution" -cd "$XPACK_DIR" - checks-reporter-with-killswitch "Response Ops Cases Cypress Tests on Security Solution" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/security_solution_cypress/cases_cli_config.ts + --config x-pack/test/security_solution_cypress/cases_cli_config.ts diff --git a/.buildkite/scripts/steps/functional/security_solution.sh b/.buildkite/scripts/steps/functional/security_solution.sh index 9b2bfc7207a957..ae81eaa4f48e22 100755 --- a/.buildkite/scripts/steps/functional/security_solution.sh +++ b/.buildkite/scripts/steps/functional/security_solution.sh @@ -8,10 +8,8 @@ export JOB=kibana-security-solution-chrome echo "--- Security Solution tests (Chrome)" -cd "$XPACK_DIR" - checks-reporter-with-killswitch "Security Solution Cypress Tests (Chrome)" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/security_solution_cypress/cli_config.ts + --config x-pack/test/security_solution_cypress/cli_config.ts diff --git a/.buildkite/scripts/steps/functional/xpack_accessibility.sh b/.buildkite/scripts/steps/functional/xpack_accessibility.sh deleted file mode 100755 index 5b098da858c966..00000000000000 --- a/.buildkite/scripts/steps/functional/xpack_accessibility.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -cd "$XPACK_DIR" - -echo --- Default Accessibility Tests - -checks-reporter-with-killswitch "X-Pack accessibility tests" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/accessibility/config.ts diff --git a/.buildkite/scripts/steps/functional/xpack_cigroup.sh b/.buildkite/scripts/steps/functional/xpack_cigroup.sh deleted file mode 100755 index 6877e88cff8b33..00000000000000 --- a/.buildkite/scripts/steps/functional/xpack_cigroup.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -export CI_GROUP=${CI_GROUP:-$((BUILDKITE_PARALLEL_JOB+1))} -export JOB=kibana-default-ciGroup${CI_GROUP} - -echo "--- Default CI Group $CI_GROUP" - -cd "$XPACK_DIR" - -checks-reporter-with-killswitch "X-Pack Chrome Functional tests / Group ${CI_GROUP}" \ - node scripts/functional_tests \ - --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --include-tag "ciGroup$CI_GROUP" - -cd "$KIBANA_DIR" diff --git a/.buildkite/scripts/steps/functional/xpack_firefox.sh b/.buildkite/scripts/steps/functional/xpack_firefox.sh deleted file mode 100755 index a9a9704ea78dea..00000000000000 --- a/.buildkite/scripts/steps/functional/xpack_firefox.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -cd "$XPACK_DIR" - -echo --- Default Firefox Smoke Tests - -checks-reporter-with-killswitch "X-Pack firefox smoke test" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --include-tag "includeFirefox" \ - --config test/functional/config.firefox.js \ - --config test/functional_embedded/config.firefox.ts diff --git a/.buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh b/.buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh deleted file mode 100755 index 5e00d2446bf7ad..00000000000000 --- a/.buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/common/util.sh - -.buildkite/scripts/bootstrap.sh -.buildkite/scripts/download_build_artifacts.sh - -cd "$XPACK_DIR" - -echo --- Capture Kibana Saved Objects field count metrics -checks-reporter-with-killswitch "Capture Kibana Saved Objects field count metrics" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/saved_objects_field_count/config.ts; diff --git a/.buildkite/scripts/steps/test/api_integration.sh b/.buildkite/scripts/steps/test/api_integration.sh deleted file mode 100755 index f56e98903d2262..00000000000000 --- a/.buildkite/scripts/steps/test/api_integration.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/common/util.sh - -is_test_execution_step - -.buildkite/scripts/bootstrap.sh - -echo --- API Integration Tests -checks-reporter-with-killswitch "API Integration Tests" \ - node scripts/functional_tests \ - --config test/api_integration/config.js \ - --bail \ - --debug diff --git a/.buildkite/scripts/steps/test/ftr_configs.sh b/.buildkite/scripts/steps/test/ftr_configs.sh new file mode 100755 index 00000000000000..52a4b9572f5b64 --- /dev/null +++ b/.buildkite/scripts/steps/test/ftr_configs.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/steps/functional/common.sh +source .buildkite/scripts/steps/test/test_group_env.sh + +export JOB_NUM=$BUILDKITE_PARALLEL_JOB +export JOB=ftr-configs-${JOB_NUM} + +FAILED_CONFIGS_KEY="${BUILDKITE_STEP_ID}${BUILDKITE_PARALLEL_JOB:-0}" + +# a FTR failure will result in the script returning an exit code of 10 +exitCode=0 + +configs="${FTR_CONFIG:-}" + +# The first retry should only run the configs that failed in the previous attempt +# Any subsequent retries, which would generally only happen by someone clicking the button in the UI, will run everything +if [[ ! "$configs" && "${BUILDKITE_RETRY_COUNT:-0}" == "1" ]]; then + configs=$(buildkite-agent meta-data get "$FAILED_CONFIGS_KEY" --default '') + if [[ "$configs" ]]; then + echo "--- Retrying only failed configs" + echo "$configs" + fi +fi + +if [[ "$configs" == "" ]]; then + echo "--- downloading ftr test run order" + buildkite-agent artifact download ftr_run_order.json . + configs=$(jq -r '.groups[env.JOB_NUM | tonumber].names | .[]' ftr_run_order.json) +fi + +failedConfigs="" +results=() + +while read -r config; do + if [[ ! "$config" ]]; then + continue; + fi + + echo "--- $ node scripts/functional_tests --bail --config $config" + start=$(date +%s) + + # prevent non-zero exit code from breaking the loop + set +e; + node ./scripts/functional_tests \ + --bail \ + --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ + --config="$config" + lastCode=$? + set -e; + + timeSec=$(($(date +%s)-start)) + if [[ $timeSec -gt 60 ]]; then + min=$((timeSec/60)) + sec=$((timeSec-(min*60))) + duration="${min}m ${sec}s" + else + duration="${timeSec}s" + fi + + results+=("- $config + duration: ${duration} + result: ${lastCode}") + + if [ $lastCode -ne 0 ]; then + exitCode=10 + echo "FTR exited with code $lastCode" + echo "^^^ +++" + + if [[ "$failedConfigs" ]]; then + failedConfigs="${failedConfigs}"$'\n'"$config" + else + failedConfigs="$config" + fi + fi +done <<< "$configs" + +if [[ "$failedConfigs" ]]; then + buildkite-agent meta-data set "$FAILED_CONFIGS_KEY" "$failedConfigs" +fi + +echo "--- FTR configs complete" +printf "%s\n" "${results[@]}" +echo "" + +exit $exitCode diff --git a/.buildkite/scripts/steps/test/jest_parallel.sh b/.buildkite/scripts/steps/test/jest_parallel.sh index 86c685d82c7e7e..f7efc13b501bdf 100755 --- a/.buildkite/scripts/steps/test/jest_parallel.sh +++ b/.buildkite/scripts/steps/test/jest_parallel.sh @@ -1,13 +1,14 @@ #!/bin/bash -set -uo pipefail +set -euo pipefail -source .buildkite/scripts/steps/test/jest_env.sh +source .buildkite/scripts/steps/test/test_group_env.sh export JOB=$BUILDKITE_PARALLEL_JOB # a jest failure will result in the script returning an exit code of 10 exitCode=0 +results=() if [[ "$1" == 'jest.config.js' ]]; then # run unit tests in parallel @@ -20,14 +21,32 @@ else fi export TEST_TYPE -echo "--- downloading integration test run order" +echo "--- downloading jest test run order" buildkite-agent artifact download jest_run_order.json . configs=$(jq -r 'getpath([env.TEST_TYPE]) | .groups[env.JOB | tonumber].names | .[]' jest_run_order.json) while read -r config; do echo "--- $ node scripts/jest --config $config" + start=$(date +%s) + + # prevent non-zero exit code from breaking the loop + set +e; NODE_OPTIONS="--max-old-space-size=14336" node ./scripts/jest --config="$config" "$parallelism" --coverage=false --passWithNoTests lastCode=$? + set -e; + + timeSec=$(($(date +%s)-start)) + if [[ $timeSec -gt 60 ]]; then + min=$((timeSec/60)) + sec=$((timeSec-(min*60))) + duration="${min}m ${sec}s" + else + duration="${timeSec}s" + fi + + results+=("- $config + duration: ${duration} + result: ${lastCode}") if [ $lastCode -ne 0 ]; then exitCode=10 @@ -36,4 +55,8 @@ while read -r config; do fi done <<< "$configs" +echo "--- Jest configs complete" +printf "%s\n" "${results[@]}" +echo "" + exit $exitCode diff --git a/.buildkite/scripts/steps/test/pick_jest_config_run_order.sh b/.buildkite/scripts/steps/test/pick_jest_config_run_order.sh deleted file mode 100644 index 37d4e629c90b07..00000000000000 --- a/.buildkite/scripts/steps/test/pick_jest_config_run_order.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/common/util.sh -source .buildkite/scripts/steps/test/jest_env.sh - -echo '--- Pick Jest Config Run Order' -node "$(dirname "${0}")/pick_jest_config_run_order.js" diff --git a/.buildkite/scripts/steps/test/pick_jest_config_run_order.js b/.buildkite/scripts/steps/test/pick_test_group_run_order.js similarity index 100% rename from .buildkite/scripts/steps/test/pick_jest_config_run_order.js rename to .buildkite/scripts/steps/test/pick_test_group_run_order.js diff --git a/.buildkite/scripts/steps/test/pick_test_group_run_order.sh b/.buildkite/scripts/steps/test/pick_test_group_run_order.sh new file mode 100644 index 00000000000000..3bb09282efb41f --- /dev/null +++ b/.buildkite/scripts/steps/test/pick_test_group_run_order.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh +source .buildkite/scripts/steps/test/test_group_env.sh + +echo '--- Pick Test Group Run Order' +node "$(dirname "${0}")/pick_test_group_run_order.js" diff --git a/.buildkite/scripts/steps/test/jest_env.sh b/.buildkite/scripts/steps/test/test_group_env.sh similarity index 80% rename from .buildkite/scripts/steps/test/jest_env.sh rename to .buildkite/scripts/steps/test/test_group_env.sh index 80e88bebba1849..3a8c12fdb4a522 100644 --- a/.buildkite/scripts/steps/test/jest_env.sh +++ b/.buildkite/scripts/steps/test/test_group_env.sh @@ -5,3 +5,4 @@ set -euo pipefail # keys used to associate test group data in ci-stats with Jest execution order export TEST_GROUP_TYPE_UNIT="Jest Unit Tests" export TEST_GROUP_TYPE_INTEGRATION="Jest Integration Tests" +export TEST_GROUP_TYPE_FUNCTIONAL="Functional Tests" diff --git a/docs/developer/contributing/development-functional-tests.asciidoc b/docs/developer/contributing/development-functional-tests.asciidoc index af88754b316fa8..b544add73b3b11 100644 --- a/docs/developer/contributing/development-functional-tests.asciidoc +++ b/docs/developer/contributing/development-functional-tests.asciidoc @@ -6,7 +6,7 @@ We use functional tests to make sure the {kib} UI works as expected. It replaces [discrete] === Running functional tests -The `FunctionalTestRunner` is very bare bones and gets most of its functionality from its config file, located at {blob}test/functional/config.js[test/functional/config.js] or {blob}x-pack/test/functional/config.js[x-pack/test/functional/config.js]. If you’re writing a plugin outside the {kib} repo, you will have your own config file. +The `FunctionalTestRunner` (FTR) is very bare bones and gets most of its functionality from its config file. The {kib} repo contains many FTR config files which use slightly different configurations for the {kib} server or {es}, have different test files, and potentially other config differences. You can find a manifest of all the FTR config files in `.buildkite/ftr_configs.yml`. If you’re writing a plugin outside the {kib} repo, you will have your own config file. See <> for more info. There are three ways to run the tests depending on your goals: @@ -96,7 +96,8 @@ node scripts/functional_test_runner --exclude-tag skipCloud When run without any arguments the `FunctionalTestRunner` automatically loads the configuration in the standard location, but you can override that behavior with the `--config` flag. List configs with multiple --config arguments. -* `--config test/functional/config.js` starts {es} and {kib} servers with the WebDriver tests configured to run in Chrome. +* `--config test/functional/apps/app-name/config.js` starts {es} and {kib} servers with the WebDriver tests configured to run in Chrome for a specific app. For example, +`--config test/functional/apps/home/config.js` starts {es} and {kib} servers with the WebDriver tests configured to run in Chrome for the home app. * `--config test/functional/config.firefox.js` starts {es} and {kib} servers with the WebDriver tests configured to run in Firefox. * `--config test/api_integration/config.js` starts {es} and {kib} servers with the api integration tests configuration. * `--config test/accessibility/config.ts` starts {es} and {kib} servers with the WebDriver tests configured to run an accessibility audit using https://www.deque.com/axe/[axe]. @@ -416,7 +417,7 @@ export function SomethingUsefulProvider({ getService }) { ----------- + * Re-export your provider from `services/index.js` -* Import it into `src/functional/config.js` and add it to the services config: +* Import it into `src/functional/config.base.js` and add it to the services config: + ["source","js"] ----------- diff --git a/docs/developer/plugin/external-plugin-functional-tests.asciidoc b/docs/developer/plugin/external-plugin-functional-tests.asciidoc index 55b311794f9dcb..4a98ffcc5d08c7 100644 --- a/docs/developer/plugin/external-plugin-functional-tests.asciidoc +++ b/docs/developer/plugin/external-plugin-functional-tests.asciidoc @@ -24,7 +24,7 @@ export default async function ({ readConfigFile }) { // read the {kib} config file so that we can utilize some of // its services and PageObjects - const kibanaConfig = await readConfigFile(resolve(REPO_ROOT, 'test/functional/config.js')); + const kibanaConfig = await readConfigFile(resolve(REPO_ROOT, 'test/functional/config.base.js')); return { // list paths to the files that contain your plugins tests diff --git a/packages/kbn-dev-utils/src/proc_runner/proc.ts b/packages/kbn-dev-utils/src/proc_runner/proc.ts index 0402feec99d47b..c622c46456abfe 100644 --- a/packages/kbn-dev-utils/src/proc_runner/proc.ts +++ b/packages/kbn-dev-utils/src/proc_runner/proc.ts @@ -37,19 +37,7 @@ async function withTimeout( ms: number, onTimeout: () => Promise ) { - const TIMEOUT = Symbol('timeout'); - try { - await Promise.race([ - attempt(), - new Promise((_, reject) => setTimeout(() => reject(TIMEOUT), ms)), - ]); - } catch (error) { - if (error === TIMEOUT) { - await onTimeout(); - } else { - throw error; - } - } + await Rx.lastValueFrom(Rx.race(Rx.defer(attempt), Rx.timer(ms).pipe(Rx.mergeMap(onTimeout)))); } export type Proc = ReturnType; diff --git a/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts b/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts index 3b7cf008df9a6e..857a4fcfd475d5 100644 --- a/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts +++ b/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts @@ -7,7 +7,6 @@ */ import * as Rx from 'rxjs'; -import { filter, first, catchError, map } from 'rxjs/operators'; import exitHook from 'exit-hook'; import { ToolingLog } from '@kbn/tooling-log'; @@ -93,29 +92,30 @@ export class ProcRunner { try { if (wait instanceof RegExp) { // wait for process to log matching line - await Rx.race( - proc.lines$.pipe( - filter((line) => wait.test(line)), - first(), - catchError((err) => { - if (err.name !== 'EmptyError') { - throw createFailError(`[${name}] exited without matching pattern: ${wait}`); - } else { - throw err; - } - }) - ), - waitTimeout === false - ? Rx.NEVER - : Rx.timer(waitTimeout).pipe( - map(() => { - const sec = waitTimeout / SECOND; - throw createFailError( - `[${name}] failed to match pattern within ${sec} seconds [pattern=${wait}]` - ); - }) - ) - ).toPromise(); + await Rx.lastValueFrom( + Rx.race( + proc.lines$.pipe( + Rx.filter((line) => wait.test(line)), + Rx.take(1), + Rx.defaultIfEmpty(undefined), + Rx.map((line) => { + if (line === undefined) { + throw createFailError(`[${name}] exited without matching pattern: ${wait}`); + } + }) + ), + waitTimeout === false + ? Rx.NEVER + : Rx.timer(waitTimeout).pipe( + Rx.map(() => { + const sec = waitTimeout / SECOND; + throw createFailError( + `[${name}] failed to match pattern within ${sec} seconds [pattern=${wait}]` + ); + }) + ) + ) + ); } if (wait === true) { diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts index fbb5784afe5ac9..899a7843a68fc9 100644 --- a/packages/kbn-es-archiver/src/cli.ts +++ b/packages/kbn-es-archiver/src/cli.ts @@ -24,7 +24,7 @@ import { Client, HttpConnection } from '@elastic/elasticsearch'; import { EsArchiver } from './es_archiver'; const resolveConfigPath = (v: string) => Path.resolve(process.cwd(), v); -const defaultConfigPath = resolveConfigPath('test/functional/config.js'); +const defaultConfigPath = resolveConfigPath('test/functional/config.base.js'); export function runCli() { new RunWithCommands({ diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index a6faffc2cfcd77..50ca9fa91e0aab 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -260,6 +260,24 @@ exports.Cluster = class Cluster { await this._outcome; } + /** + * Stops ES process, it it's running, without waiting for it to shutdown gracefully + */ + async kill() { + if (this._stopCalled) { + return; + } + + this._stopCalled; + + if (!this._process || !this._outcome) { + throw new Error('ES has not been started'); + } + + await treeKillAsync(this._process.pid, 'SIGKILL'); + await this._outcome; + } + /** * Common logic from this.start() and this.run() * diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 6638acfef5ef4d..97ff65d4f71bc0 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -2270,8 +2270,6 @@ exports.observeLines = observeLines; var Rx = _interopRequireWildcard(__webpack_require__("../../node_modules/rxjs/dist/esm5/index.js")); -var _operators = __webpack_require__("../../node_modules/rxjs/dist/esm5/operators/index.js"); - var _observe_readable = __webpack_require__("../../node_modules/@kbn/stdio-dev-helpers/target_node/observe_readable.js"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } @@ -2297,8 +2295,8 @@ const SEP = /\r?\n/; * @return {Rx.Observable} */ function observeLines(readable) { - const done$ = (0, _observe_readable.observeReadable)(readable).pipe((0, _operators.share)()); - const scan$ = Rx.fromEvent(readable, 'data').pipe((0, _operators.scan)(({ + const done$ = (0, _observe_readable.observeReadable)(readable).pipe(Rx.share()); + const scan$ = Rx.fromEvent(readable, 'data').pipe(Rx.scan(({ buffer }, chunk) => { buffer += chunk; @@ -2322,16 +2320,15 @@ function observeLines(readable) { }, { buffer: '' }), // stop if done completes or errors - (0, _operators.takeUntil)(done$.pipe((0, _operators.materialize)())), (0, _operators.share)()); + Rx.takeUntil(done$.pipe(Rx.materialize())), Rx.share()); return Rx.merge( // use done$ to provide completion/errors done$, // merge in the "lines" from each step - scan$.pipe((0, _operators.mergeMap)(({ + scan$.pipe(Rx.mergeMap(({ lines }) => lines || [])), // inject the "unsplit" data at the end - scan$.pipe((0, _operators.last)(), (0, _operators.mergeMap)(({ + scan$.pipe(Rx.takeLast(1), Rx.mergeMap(({ buffer - }) => buffer ? [buffer] : []), // if there were no lines, last() will error, so catch and complete - (0, _operators.catchError)(() => Rx.empty()))); + }) => buffer ? [buffer] : []))); } /***/ }), @@ -2349,8 +2346,6 @@ exports.observeReadable = observeReadable; var Rx = _interopRequireWildcard(__webpack_require__("../../node_modules/rxjs/dist/esm5/index.js")); -var _operators = __webpack_require__("../../node_modules/rxjs/dist/esm5/operators/index.js"); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } @@ -2369,7 +2364,9 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && * - fails on the first "error" event */ function observeReadable(readable) { - return Rx.race(Rx.fromEvent(readable, 'end').pipe((0, _operators.first)(), (0, _operators.ignoreElements)()), Rx.fromEvent(readable, 'error').pipe((0, _operators.first)(), (0, _operators.mergeMap)(err => Rx.throwError(err)))); + return Rx.race(Rx.fromEvent(readable, 'end').pipe(Rx.first(), Rx.ignoreElements()), Rx.fromEvent(readable, 'error').pipe(Rx.first(), Rx.map(err => { + throw err; + }))); } /***/ }), diff --git a/packages/kbn-stdio-dev-helpers/src/observe_lines.ts b/packages/kbn-stdio-dev-helpers/src/observe_lines.ts index 9b6ba85fea897c..4b3a2ac2287da9 100644 --- a/packages/kbn-stdio-dev-helpers/src/observe_lines.ts +++ b/packages/kbn-stdio-dev-helpers/src/observe_lines.ts @@ -9,7 +9,6 @@ import { Readable } from 'stream'; import * as Rx from 'rxjs'; -import { scan, takeUntil, share, materialize, mergeMap, last, catchError } from 'rxjs/operators'; const SEP = /\r?\n/; @@ -25,13 +24,13 @@ import { observeReadable } from './observe_readable'; * @return {Rx.Observable} */ export function observeLines(readable: Readable): Rx.Observable { - const done$ = observeReadable(readable).pipe(share()); + const done$ = observeReadable(readable).pipe(Rx.share()); const scan$: Rx.Observable<{ buffer: string; lines?: string[] }> = Rx.fromEvent( readable, 'data' ).pipe( - scan( + Rx.scan( ({ buffer }, chunk) => { buffer += chunk; @@ -53,9 +52,9 @@ export function observeLines(readable: Readable): Rx.Observable { ), // stop if done completes or errors - takeUntil(done$.pipe(materialize())), + Rx.takeUntil(done$.pipe(Rx.materialize())), - share() + Rx.share() ); return Rx.merge( @@ -63,14 +62,12 @@ export function observeLines(readable: Readable): Rx.Observable { done$, // merge in the "lines" from each step - scan$.pipe(mergeMap(({ lines }) => lines || [])), + scan$.pipe(Rx.mergeMap(({ lines }) => lines || [])), // inject the "unsplit" data at the end scan$.pipe( - last(), - mergeMap(({ buffer }) => (buffer ? [buffer] : [])), - // if there were no lines, last() will error, so catch and complete - catchError(() => Rx.empty()) + Rx.takeLast(1), + Rx.mergeMap(({ buffer }) => (buffer ? [buffer] : [])) ) ); } diff --git a/packages/kbn-stdio-dev-helpers/src/observe_readable.ts b/packages/kbn-stdio-dev-helpers/src/observe_readable.ts index 29aa77a538601c..fa087c299aa517 100644 --- a/packages/kbn-stdio-dev-helpers/src/observe_readable.ts +++ b/packages/kbn-stdio-dev-helpers/src/observe_readable.ts @@ -9,7 +9,6 @@ import { Readable } from 'stream'; import * as Rx from 'rxjs'; -import { first, ignoreElements, mergeMap } from 'rxjs/operators'; /** * Produces an Observable from a ReadableSteam that: @@ -18,11 +17,12 @@ import { first, ignoreElements, mergeMap } from 'rxjs/operators'; */ export function observeReadable(readable: Readable): Rx.Observable { return Rx.race( - Rx.fromEvent(readable, 'end').pipe(first(), ignoreElements()), - + Rx.fromEvent(readable, 'end').pipe(Rx.first(), Rx.ignoreElements()), Rx.fromEvent(readable, 'error').pipe( - first(), - mergeMap((err) => Rx.throwError(err)) + Rx.first(), + Rx.map((err) => { + throw err; + }) ) ); } diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index 8a7aabdecd61e7..f7599e6d816498 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -64,6 +64,7 @@ RUNTIME_DEPS = [ "@npm//jest-snapshot", "@npm//jest-styled-components", "@npm//joi", + "@npm//js-yaml", "@npm//mustache", "@npm//normalize-path", "@npm//parse-link-header", @@ -108,6 +109,7 @@ TYPES_DEPS = [ "@npm//@types/he", "@npm//@types/history", "@npm//@types/jest", + "@npm//@types/js-yaml", "@npm//@types/joi", "@npm//@types/lodash", "@npm//@types/mustache", diff --git a/packages/kbn-test/README.md b/packages/kbn-test/README.md index 3159e5c2492b4d..72fb5c3358ce74 100644 --- a/packages/kbn-test/README.md +++ b/packages/kbn-test/README.md @@ -15,14 +15,14 @@ Functional testing methods exist in the `src/functional_tests` directory. They d #### runTests(configPaths: Array) For each config file specified in configPaths, starts Elasticsearch and Kibana once, runs tests specified in that config file, and shuts down Elasticsearch and Kibana once completed. (Repeats for every config file.) -`configPaths`: array of strings, each an absolute path to a config file that looks like [this](../../test/functional/config.js), following the config schema specified [here](../../src/functional_test_runner/lib/config/schema.js). +`configPaths`: array of strings, each an absolute path to a config file that looks like [this](../../test/functional/config.base.js), following the config schema specified [here](../../src/functional_test_runner/lib/config/schema.js). Internally the method that starts Elasticsearch comes from [kbn-es](../../packages/kbn-es). #### startServers(configPath: string) Starts Elasticsearch and Kibana servers given a specified config. -`configPath`: absolute path to a config file that looks like [this](../../test/functional/config.js), following the config schema specified [here](../../src/functional_test_runner/lib/config/schema.js). +`configPath`: absolute path to a config file that looks like [this](../../test/functional/config.base.js), following the config schema specified [here](../../src/functional_test_runner/lib/config/schema.js). Allows users to start another process to run just the tests while keeping the servers running with this method. Start servers _and_ run tests using the same config file ([see how](../../scripts/README.md)). diff --git a/packages/kbn-test/src/es/test_es_cluster.ts b/packages/kbn-test/src/es/test_es_cluster.ts index a12b1d852ced9d..42dc19445c2931 100644 --- a/packages/kbn-test/src/es/test_es_cluster.ts +++ b/packages/kbn-test/src/es/test_es_cluster.ts @@ -41,6 +41,7 @@ interface Node { ) => Promise<{ insallPath: string }>; start: (installPath: string, opts: Record) => Promise; stop: () => Promise; + kill: () => Promise; } export interface ICluster { @@ -268,20 +269,26 @@ export function createTestEsCluster< } async stop() { - const nodeStopPromises = []; - for (let i = 0; i < this.nodes.length; i++) { - nodeStopPromises.push(async () => { + await Promise.all( + this.nodes.map(async (node, i) => { log.info(`[es] stopping node ${nodes[i].name}`); - return await this.nodes[i].stop(); - }); - } - await Promise.all(nodeStopPromises.map(async (stop) => await stop())); + await node.stop(); + }) + ); log.info('[es] stopped'); } async cleanup() { - await this.stop(); + log.info('[es] killing', this.nodes.length === 1 ? 'node' : `${this.nodes.length} nodes`); + await Promise.all( + this.nodes.map(async (node, i) => { + log.info(`[es] stopping node ${nodes[i].name}`); + // we are deleting this install, stop ES more aggressively + await node.kill(); + }) + ); + await del(config.installPath, { force: true }); log.info('[es] cleanup complete'); } diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index ee01b0ccfde9c1..4159533e628bca 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -98,11 +98,7 @@ export function runFtrCli() { }); } - try { - await functionalTestRunner.close(); - } finally { - process.exit(); - } + process.exit(); }; process.on('unhandledRejection', (err) => diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts index 1ba99fc69a5d37..0ceba511f9b9bf 100644 --- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts +++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts @@ -14,10 +14,9 @@ import { REPO_ROOT } from '@kbn/utils'; import { Suite, Test } from './fake_mocha_types'; import { Lifecycle, - LifecyclePhase, - TestMetadata, readConfigFile, ProviderCollection, + Providers, readProviderSpec, setupMocha, runTests, @@ -29,10 +28,6 @@ import { import { createEsClientForFtrConfig } from '../es'; export class FunctionalTestRunner { - public readonly lifecycle = new Lifecycle(); - public readonly testMetadata = new TestMetadata(this.lifecycle); - private closed = false; - private readonly esVersion: EsVersion; constructor( private readonly log: ToolingLog, @@ -40,12 +35,6 @@ export class FunctionalTestRunner { private readonly configOverrides: any, esVersion?: string | EsVersion ) { - for (const [key, value] of Object.entries(this.lifecycle)) { - if (value instanceof LifecyclePhase) { - value.before$.subscribe(() => log.verbose('starting %j lifecycle phase', key)); - value.after$.subscribe(() => log.verbose('starting %j lifecycle phase', key)); - } - } this.esVersion = esVersion === undefined ? EsVersion.getDefault() @@ -55,19 +44,28 @@ export class FunctionalTestRunner { } async run() { - return await this._run(async (config, coreProviders) => { - SuiteTracker.startTracking(this.lifecycle, this.configFile); + const testStats = await this.getTestStats(); + + return await this.runHarness(async (config, lifecycle, coreProviders) => { + SuiteTracker.startTracking(lifecycle, this.configFile); + + const realServices = + !testStats || (testStats.testCount > 0 && testStats.nonSkippedTestCount > 0); - const providers = new ProviderCollection(this.log, [ - ...coreProviders, - ...readProviderSpec('Service', config.get('services')), - ...readProviderSpec('PageObject', config.get('pageObjects')), - ]); + const providers = realServices + ? new ProviderCollection(this.log, [ + ...coreProviders, + ...readProviderSpec('Service', config.get('services')), + ...readProviderSpec('PageObject', config.get('pageObjects')), + ]) + : this.getStubProviderCollection(config, coreProviders); - if (providers.hasService('es')) { - await this.validateEsVersion(config); + if (realServices) { + if (providers.hasService('es')) { + await this.validateEsVersion(config); + } + await providers.loadAll(); } - await providers.loadAll(); const customTestRunner = config.get('testRunner'); if (customTestRunner) { @@ -90,7 +88,7 @@ export class FunctionalTestRunner { } const mocha = await setupMocha( - this.lifecycle, + lifecycle, this.log, config, providers, @@ -108,10 +106,10 @@ export class FunctionalTestRunner { return this.simulateMochaDryRun(mocha); } - await this.lifecycle.beforeTests.trigger(mocha.suite); + await lifecycle.beforeTests.trigger(mocha.suite); this.log.info('Starting tests'); - return await runTests(this.lifecycle, mocha); + return await runTests(lifecycle, mocha); }); } @@ -143,60 +141,73 @@ export class FunctionalTestRunner { } async getTestStats() { - return await this._run(async (config, coreProviders) => { + return await this.runHarness(async (config, lifecycle, coreProviders) => { if (config.get('testRunner')) { - throw new Error('Unable to get test stats for config that uses a custom test runner'); + return; } - // replace the function of custom service providers so that they return - // promise-like objects which never resolve, essentially disabling them - // allowing us to load the test files and populate the mocha suites - const readStubbedProviderSpec = (type: string, providers: any, skip: string[]) => - readProviderSpec(type, providers).map((p) => ({ - ...p, - fn: skip.includes(p.name) - ? (ctx: any) => { - const result = ProviderCollection.callProviderFn(p.fn, ctx); - - if ('then' in result) { - throw new Error( - `Provider [${p.name}] returns a promise so it can't loaded during test analysis` - ); - } - - return result; - } - : () => ({ - then: () => {}, - }), - })); - - const providers = new ProviderCollection(this.log, [ - ...coreProviders, - ...readStubbedProviderSpec( - 'Service', - config.get('services'), - config.get('servicesRequiredForTestAnalysis') - ), - ...readStubbedProviderSpec('PageObject', config.get('pageObjects'), []), - ]); - - const mocha = await setupMocha(this.lifecycle, this.log, config, providers, this.esVersion); - - const countTests = (suite: Suite): number => - suite.suites.reduce((sum, s) => sum + countTests(s), suite.tests.length); + const providers = this.getStubProviderCollection(config, coreProviders); + const mocha = await setupMocha(lifecycle, this.log, config, providers, this.esVersion); + + const queue = new Set([mocha.suite]); + const allTests: Test[] = []; + for (const suite of queue) { + for (const test of suite.tests) { + allTests.push(test); + } + for (const childSuite of suite.suites) { + queue.add(childSuite); + } + } return { - testCount: countTests(mocha.suite), + testCount: allTests.length, + nonSkippedTestCount: allTests.filter((t) => !t.pending).length, testsExcludedByTag: mocha.testsExcludedByTag.map((t: Test) => t.fullTitle()), }; }); } - async _run( - handler: (config: Config, coreProvider: ReturnType) => Promise + private getStubProviderCollection(config: Config, coreProviders: Providers) { + // when we want to load the tests but not actually run anything we can + // use stubbed providers which allow mocha to do it's thing without taking + // too much time + const readStubbedProviderSpec = (type: string, providers: any, skip: string[]) => + readProviderSpec(type, providers).map((p) => ({ + ...p, + fn: skip.includes(p.name) + ? (ctx: any) => { + const result = ProviderCollection.callProviderFn(p.fn, ctx); + + if ('then' in result) { + throw new Error( + `Provider [${p.name}] returns a promise so it can't loaded during test analysis` + ); + } + + return result; + } + : () => ({ + then: () => {}, + }), + })); + + return new ProviderCollection(this.log, [ + ...coreProviders, + ...readStubbedProviderSpec( + 'Service', + config.get('services'), + config.get('servicesRequiredForTestAnalysis') + ), + ...readStubbedProviderSpec('PageObject', config.get('pageObjects'), []), + ]); + } + + private async runHarness( + handler: (config: Config, lifecycle: Lifecycle, coreProviders: Providers) => Promise ): Promise { let runErrorOccurred = false; + const lifecycle = new Lifecycle(this.log); try { const config = await readConfigFile( @@ -205,7 +216,7 @@ export class FunctionalTestRunner { this.configFile, this.configOverrides ); - this.log.info('Config loaded'); + this.log.debug('Config loaded'); if ( (!config.get('testFiles') || config.get('testFiles').length === 0) && @@ -217,26 +228,25 @@ export class FunctionalTestRunner { const dockerServers = new DockerServersService( config.get('dockerServers'), this.log, - this.lifecycle + lifecycle ); // base level services that functional_test_runner exposes const coreProviders = readProviderSpec('Service', { - lifecycle: () => this.lifecycle, + lifecycle: () => lifecycle, log: () => this.log, - testMetadata: () => this.testMetadata, config: () => config, dockerServers: () => dockerServers, esVersion: () => this.esVersion, }); - return await handler(config, coreProviders); + return await handler(config, lifecycle, coreProviders); } catch (runError) { runErrorOccurred = true; throw runError; } finally { try { - await this.close(); + await lifecycle.cleanup.trigger(); } catch (closeError) { if (runErrorOccurred) { this.log.error('failed to close functional_test_runner'); @@ -249,13 +259,6 @@ export class FunctionalTestRunner { } } - async close() { - if (this.closed) return; - - this.closed = true; - await this.lifecycle.cleanup.trigger(); - } - simulateMochaDryRun(mocha: any) { interface TestEntry { file: string; diff --git a/packages/kbn-test/src/functional_test_runner/index.ts b/packages/kbn-test/src/functional_test_runner/index.ts index b5d55c28ee9b28..1a8efb6097048f 100644 --- a/packages/kbn-test/src/functional_test_runner/index.ts +++ b/packages/kbn-test/src/functional_test_runner/index.ts @@ -15,7 +15,6 @@ export { Lifecycle, LifecyclePhase, } from './lib'; -export type { ScreenshotRecord } from './lib'; export { runFtrCli } from './cli'; export * from './lib/docker_servers'; export * from './public_types'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/ftr_configs_manifest.ts b/packages/kbn-test/src/functional_test_runner/lib/config/ftr_configs_manifest.ts new file mode 100644 index 00000000000000..93cab4bfaac957 --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/config/ftr_configs_manifest.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import Fs from 'fs'; + +import { REPO_ROOT } from '@kbn/utils'; +import JsYaml from 'js-yaml'; + +export const FTR_CONFIGS_MANIFEST_REL = '.buildkite/ftr_configs.yml'; + +const ftrConfigsManifest = JsYaml.safeLoad( + Fs.readFileSync(Path.resolve(REPO_ROOT, FTR_CONFIGS_MANIFEST_REL), 'utf8') +); + +export const FTR_CONFIGS_MANIFEST_PATHS = (Object.values(ftrConfigsManifest) as string[][]) + .flat() + .map((rel) => Path.resolve(REPO_ROOT, rel)); diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.js b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.ts similarity index 70% rename from packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.js rename to packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.ts index d1ce17cc95b7be..29b723dae7195f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.js +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.ts @@ -14,36 +14,35 @@ import { EsVersion } from '../es_version'; const log = new ToolingLog(); const esVersion = new EsVersion('8.0.0'); +const CONFIG_PATH_1 = require.resolve('./__fixtures__/config.1.js'); +const CONFIG_PATH_2 = require.resolve('./__fixtures__/config.2.js'); +const CONFIG_PATH_INVALID = require.resolve('./__fixtures__/config.invalid.js'); + describe('readConfigFile()', () => { it('reads config from a file, returns an instance of Config class', async () => { - const config = await readConfigFile(log, esVersion, require.resolve('./__fixtures__/config.1')); + const config = await readConfigFile(log, esVersion, CONFIG_PATH_1); expect(config instanceof Config).toBeTruthy(); expect(config.get('testFiles')).toEqual(['config.1']); }); it('merges setting overrides into log', async () => { - const config = await readConfigFile( - log, - esVersion, - require.resolve('./__fixtures__/config.1'), - { - screenshots: { - directory: 'foo.bar', - }, - } - ); + const config = await readConfigFile(log, esVersion, CONFIG_PATH_1, { + screenshots: { + directory: 'foo.bar', + }, + }); expect(config.get('screenshots.directory')).toBe('foo.bar'); }); it('supports loading config files from within config files', async () => { - const config = await readConfigFile(log, esVersion, require.resolve('./__fixtures__/config.2')); + const config = await readConfigFile(log, esVersion, CONFIG_PATH_2); expect(config.get('testFiles')).toEqual(['config.1', 'config.2']); }); it('throws if settings are invalid', async () => { try { - await readConfigFile(log, esVersion, require.resolve('./__fixtures__/config.invalid')); + await readConfigFile(log, esVersion, CONFIG_PATH_INVALID); throw new Error('expected readConfigFile() to fail'); } catch (err) { expect(err.message).toMatch(/"foo"/); diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts index d026842f3a4f11..24702d699064cd 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts @@ -6,25 +6,41 @@ * Side Public License, v 1. */ +import Path from 'path'; import { ToolingLog } from '@kbn/tooling-log'; import { defaultsDeep } from 'lodash'; +import { createFlagError } from '@kbn/dev-utils'; import { Config } from './config'; import { EsVersion } from '../es_version'; +import { FTR_CONFIGS_MANIFEST_REL, FTR_CONFIGS_MANIFEST_PATHS } from './ftr_configs_manifest'; const cache = new WeakMap(); async function getSettingsFromFile( log: ToolingLog, esVersion: EsVersion, - path: string, - settingOverrides: any + options: { + path: string; + settingOverrides: any; + primary: boolean; + } ) { - const configModule = require(path); // eslint-disable-line @typescript-eslint/no-var-requires + if ( + options.primary && + !FTR_CONFIGS_MANIFEST_PATHS.includes(options.path) && + !options.path.includes(`${Path.sep}__fixtures__${Path.sep}`) + ) { + throw createFlagError( + `Refusing to load FTR Config which is not listed in [${FTR_CONFIGS_MANIFEST_REL}]. All FTR Config files must be listed there, use the "enabled" key if the FTR Config should be run on automatically on PR CI, or the "disabled" key if it is run manually or by a special job.` + ); + } + + const configModule = require(options.path); // eslint-disable-line @typescript-eslint/no-var-requires const configProvider = configModule.__esModule ? configModule.default : configModule; if (!cache.has(configProvider)) { - log.debug('Loading config file from %j', path); + log.debug('Loading config file from %j', options.path); cache.set( configProvider, configProvider({ @@ -32,7 +48,11 @@ async function getSettingsFromFile( esVersion, async readConfigFile(p: string, o: any) { return new Config({ - settings: await getSettingsFromFile(log, esVersion, p, o), + settings: await getSettingsFromFile(log, esVersion, { + path: p, + settingOverrides: o, + primary: false, + }), primary: false, path: p, }); @@ -43,7 +63,7 @@ async function getSettingsFromFile( const settingsWithDefaults: any = defaultsDeep( {}, - settingOverrides, + options.settingOverrides, await cache.get(configProvider)! ); @@ -57,7 +77,11 @@ export async function readConfigFile( settingOverrides: any = {} ) { return new Config({ - settings: await getSettingsFromFile(log, esVersion, path, settingOverrides), + settings: await getSettingsFromFile(log, esVersion, { + path, + settingOverrides, + primary: true, + }), primary: true, path, }); 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 17c3af046f92f0..d2182064d352e5 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 @@ -226,6 +226,11 @@ export const schema = Joi.object() wait: Joi.object() .regex() .default(/Kibana is now available/), + + /** + * Does this test config only work when run against source? + */ + alwaysUseSource: Joi.boolean().default(false), }) .default(), env: Joi.object().unknown().default(), diff --git a/packages/kbn-test/src/functional_test_runner/lib/index.ts b/packages/kbn-test/src/functional_test_runner/lib/index.ts index 077a62e8e74e54..9f637f8bd5b4f2 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/index.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/index.ts @@ -12,7 +12,6 @@ export { readConfigFile, Config } from './config'; export * from './providers'; // @internal export { runTests, setupMocha } from './mocha'; -export * from './test_metadata'; export * from './docker_servers'; export { SuiteTracker } from './suite_tracker'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts index e683ec23a8d84b..230eacb91008ee 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts @@ -6,29 +6,51 @@ * Side Public License, v 1. */ +import * as Rx from 'rxjs'; +import { ToolingLog } from '@kbn/tooling-log'; + import { LifecyclePhase } from './lifecycle_phase'; import { Suite, Test } from '../fake_mocha_types'; export class Lifecycle { + /** root subscription to cleanup lifecycle phases when lifecycle completes */ + private readonly sub = new Rx.Subscription(); + /** lifecycle phase that will run handlers once before tests execute */ - public readonly beforeTests = new LifecyclePhase<[Suite]>({ + public readonly beforeTests = new LifecyclePhase<[Suite]>(this.sub, { singular: true, }); /** lifecycle phase that runs handlers before each runnable (test and hooks) */ - public readonly beforeEachRunnable = new LifecyclePhase<[Test]>(); + public readonly beforeEachRunnable = new LifecyclePhase<[Test]>(this.sub); /** lifecycle phase that runs handlers before each suite */ - public readonly beforeTestSuite = new LifecyclePhase<[Suite]>(); + public readonly beforeTestSuite = new LifecyclePhase<[Suite]>(this.sub); /** lifecycle phase that runs handlers before each test */ - public readonly beforeEachTest = new LifecyclePhase<[Test]>(); + public readonly beforeEachTest = new LifecyclePhase<[Test]>(this.sub); /** lifecycle phase that runs handlers after each suite */ - public readonly afterTestSuite = new LifecyclePhase<[Suite]>(); + public readonly afterTestSuite = new LifecyclePhase<[Suite]>(this.sub); /** lifecycle phase that runs handlers after a test fails */ - public readonly testFailure = new LifecyclePhase<[Error, Test]>(); + public readonly testFailure = new LifecyclePhase<[Error, Test]>(this.sub); /** lifecycle phase that runs handlers after a hook fails */ - public readonly testHookFailure = new LifecyclePhase<[Error, Test]>(); + public readonly testHookFailure = new LifecyclePhase<[Error, Test]>(this.sub); /** lifecycle phase that runs handlers at the very end of execution */ - public readonly cleanup = new LifecyclePhase<[]>({ + public readonly cleanup = new LifecyclePhase<[]>(this.sub, { singular: true, }); + + constructor(log: ToolingLog) { + for (const [name, phase] of Object.entries(this)) { + if (phase instanceof LifecyclePhase) { + phase.before$.subscribe(() => log.verbose('starting %j lifecycle phase', name)); + phase.after$.subscribe(() => log.verbose('starting %j lifecycle phase', name)); + } + } + + // after the singular cleanup lifecycle phase completes unsubscribe from the root subscription + this.cleanup.after$.pipe(Rx.materialize()).subscribe((n) => { + if (n.kind === 'C') { + this.sub.unsubscribe(); + } + }); + } } diff --git a/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.test.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.test.ts index 503a9490f26645..47ab24169d2042 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.test.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.test.ts @@ -26,7 +26,7 @@ describe('with randomness', () => { }); it('calls handlers in random order', async () => { - const phase = new LifecyclePhase(); + const phase = new LifecyclePhase(new Rx.Subscription()); const order: string[] = []; phase.add( @@ -69,7 +69,7 @@ describe('without randomness', () => { afterEach(() => jest.restoreAllMocks()); it('calls all handlers and throws first error', async () => { - const phase = new LifecyclePhase(); + const phase = new LifecyclePhase(new Rx.Subscription()); const fn1 = jest.fn(); phase.add(fn1); @@ -88,7 +88,7 @@ describe('without randomness', () => { }); it('triggers before$ just before calling handler and after$ once it resolves', async () => { - const phase = new LifecyclePhase(); + const phase = new LifecyclePhase(new Rx.Subscription()); const order: string[] = []; const beforeSub = jest.fn(() => order.push('before')); @@ -116,7 +116,7 @@ describe('without randomness', () => { }); it('completes before$ and after$ if phase is singular', async () => { - const phase = new LifecyclePhase({ singular: true }); + const phase = new LifecyclePhase(new Rx.Subscription(), { singular: true }); const beforeNotifs: Array> = []; phase.before$.pipe(materialize()).subscribe((n) => beforeNotifs.push(n)); @@ -160,7 +160,7 @@ describe('without randomness', () => { }); it('completes before$ subscribers after trigger of singular phase', async () => { - const phase = new LifecyclePhase({ singular: true }); + const phase = new LifecyclePhase(new Rx.Subscription(), { singular: true }); await phase.trigger(); await expect(phase.before$.pipe(materialize(), toArray()).toPromise()).resolves @@ -177,7 +177,7 @@ describe('without randomness', () => { }); it('replays after$ event subscribers after trigger of singular phase', async () => { - const phase = new LifecyclePhase({ singular: true }); + const phase = new LifecyclePhase(new Rx.Subscription(), { singular: true }); await phase.trigger(); await expect(phase.after$.pipe(materialize(), toArray()).toPromise()).resolves diff --git a/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.ts index 09e7c6f3b8d159..df4b26230d4daf 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.ts @@ -26,6 +26,7 @@ export class LifecyclePhase { public readonly after$: Rx.Observable; constructor( + sub: Rx.Subscription, private readonly options: { singular?: boolean; } = {} @@ -35,6 +36,12 @@ export class LifecyclePhase { this.afterSubj = this.options.singular ? new Rx.ReplaySubject(1) : new Rx.Subject(); this.after$ = this.afterSubj.asObservable(); + + sub.add(() => { + this.beforeSubj.complete(); + this.afterSubj.complete(); + this.handlers.length = 0; + }); } public add(fn: (...args: Args) => Promise | void) { diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js index b9d4ed6ef7b5e9..62104cebf9cba7 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js @@ -12,6 +12,32 @@ import { createAssignmentProxy } from './assignment_proxy'; import { wrapFunction } from './wrap_function'; import { wrapRunnableArgs } from './wrap_runnable_args'; +const allTestsSkippedCache = new WeakMap(); +function allTestsAreSkipped(suite) { + // cache result for each suite so we don't have to traverse over and over + const cache = allTestsSkippedCache.get(suite); + if (cache) { + return cache; + } + + // if this suite is skipped directly then all it's children are skipped + if (suite.pending) { + allTestsSkippedCache.set(suite, true); + return true; + } + + // if any of this suites own tests are not skipped, then we don't need to traverse to child suites + if (suite.tests.some((t) => !t.pending)) { + allTestsSkippedCache.set(suite, false); + return false; + } + + // otherwise traverse down through the child suites and return true only if all children are all skipped + const childrenSkipped = suite.suites.every(allTestsAreSkipped); + allTestsSkippedCache.set(suite, childrenSkipped); + return childrenSkipped; +} + export function decorateMochaUi(log, lifecycle, context, { rootTags }) { // incremented at the start of each suite, decremented after // so that in each non-suite call we can know if we are within @@ -71,6 +97,12 @@ export function decorateMochaUi(log, lifecycle, context, { rootTags }) { provider.call(this); + if (allTestsAreSkipped(this)) { + // all the children in this suite are skipped, so make sure the suite is + // marked as pending so that its hooks are not run + this.pending = true; + } + after('afterTestSuite.trigger', async () => { await lifecycle.afterTestSuite.trigger(this); }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.test.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.test.js index 191503af123d01..3d1867aa0eed0f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.test.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.test.js @@ -69,6 +69,9 @@ function setup({ include, exclude, esVersion }) { info(...args) { history.push(`info: ${format(...args)}`); }, + debug(...args) { + history.push(`debg: ${format(...args)}`); + }, }, mocha, include, @@ -221,7 +224,7 @@ it(`excludes tests which don't meet the esVersionRequirement`, async () => { expect(history).toMatchInlineSnapshot(` Array [ - "info: Only running suites which are compatible with ES version 9.0.0", + "debg: Only running suites which are compatible with ES version 9.0.0", "suite: ", "suite: level 1", "suite: level 1 level 1a", diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.ts index 98db434b3b0886..6bb95acd407de0 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.ts @@ -44,7 +44,7 @@ export function filterSuites({ log, mocha, include, exclude, esVersion }: Option if (esVersion) { // traverse the test graph and exclude any tests which don't meet their esVersionRequirement - log.info('Only running suites which are compatible with ES version', esVersion.toString()); + log.debug('Only running suites which are compatible with ES version', esVersion.toString()); (function recurse(parentSuite: SuiteInternal) { const children = parentSuite.suites; parentSuite.suites = []; diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts index ee993122d7d9c1..96900555db7459 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts @@ -17,7 +17,6 @@ import { import { Config } from '../../config'; import { Runner } from '../../../fake_mocha_types'; -import { TestMetadata, ScreenshotRecord } from '../../test_metadata'; import { Lifecycle } from '../../lifecycle'; import { getSnapshotOfRunnableLogs } from '../../../../mocha'; @@ -36,7 +35,6 @@ interface Runnable { file: string; title: string; parent: Suite; - _screenshots?: ScreenshotRecord[]; } function getHookType(hook: Runnable): CiStatsTestType { @@ -60,15 +58,18 @@ export function setupCiStatsFtrTestGroupReporter({ config, lifecycle, runner, - testMetadata, reporter, }: { config: Config; lifecycle: Lifecycle; runner: Runner; - testMetadata: TestMetadata; reporter: CiStatsReporter; }) { + const testGroupType = process.env.TEST_GROUP_TYPE_FUNCTIONAL; + if (!testGroupType) { + throw new Error('missing process.env.TEST_GROUP_TYPE_FUNCTIONAL'); + } + let startMs: number | undefined; runner.on('start', () => { startMs = Date.now(); @@ -78,7 +79,7 @@ export function setupCiStatsFtrTestGroupReporter({ const group: CiStatsReportTestsOptions['group'] = { startTime: new Date(start).toJSON(), durationMs: 0, - type: config.path.startsWith('x-pack') ? 'X-Pack Functional Tests' : 'Functional Tests', + type: testGroupType, name: Path.relative(REPO_ROOT, config.path), result: 'skip', meta: { @@ -106,10 +107,6 @@ export function setupCiStatsFtrTestGroupReporter({ type, error: error?.stack, stdout: getSnapshotOfRunnableLogs(runnable), - screenshots: testMetadata.getScreenshots(runnable).map((s) => ({ - base64Png: s.base64Png, - name: s.name, - })), }); } diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js index 973a552ebb7280..66a4c9ce4fd04b 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js @@ -24,7 +24,6 @@ export function MochaReporterProvider({ getService }) { const log = getService('log'); const config = getService('config'); const lifecycle = getService('lifecycle'); - const testMetadata = getService('testMetadata'); let originalLogWriters; let reporterCaptureStartTime; @@ -61,7 +60,6 @@ export function MochaReporterProvider({ getService }) { config, lifecycle, runner, - testMetadata, }); } } diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js index 4f798839d72312..a0298b635a135c 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js @@ -48,8 +48,10 @@ export function validateCiGroupTags(log, mocha) { const queue = [mocha.suite]; while (queue.length) { const suite = queue.shift(); - if (getCiGroups(suite).length > 1) { - suitesWithMultipleCiGroups.push(suite); + if (getCiGroups(suite).length) { + throw new Error( + 'ciGroups are no longer needed and should be removed. If you need to split up your FTR config because it is taking too long to complete then create one or more a new FTR config files and split your test files amoungst them' + ); } else { queue.push(...(suite.suites ?? [])); } diff --git a/packages/kbn-test/src/functional_test_runner/lib/providers/index.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/index.ts index 578e41ca8e8273..c0b85370d321cd 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/providers/index.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/providers/index.ts @@ -7,6 +7,6 @@ */ export { ProviderCollection } from './provider_collection'; -export { readProviderSpec } from './read_provider_spec'; +export * from './read_provider_spec'; export { createAsyncInstance } from './async_instance'; export type { Provider } from './read_provider_spec'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts index c199dfc0927897..403708b893db83 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts @@ -6,11 +6,14 @@ * Side Public License, v 1. */ +import path from 'path'; +import fs from 'fs'; + +import { ToolingLog } from '@kbn/tooling-log'; + import { Suite, Test } from '../../fake_mocha_types'; import { Lifecycle } from '../lifecycle'; import { decorateSnapshotUi, expectSnapshot } from './decorate_snapshot_ui'; -import path from 'path'; -import fs from 'fs'; const createRootSuite = () => { const suite = { @@ -65,7 +68,7 @@ describe('decorateSnapshotUi', () => { let lifecycle: Lifecycle; let rootSuite: Suite; beforeEach(async () => { - lifecycle = new Lifecycle(); + lifecycle = new Lifecycle(new ToolingLog()); rootSuite = createRootSuite(); decorateSnapshotUi({ lifecycle, updateSnapshots: false, isCi: false }); @@ -116,7 +119,7 @@ describe('decorateSnapshotUi', () => { let lifecycle: Lifecycle; let rootSuite: Suite; beforeEach(async () => { - lifecycle = new Lifecycle(); + lifecycle = new Lifecycle(new ToolingLog()); rootSuite = createRootSuite(); decorateSnapshotUi({ lifecycle, updateSnapshots: false, isCi: false }); @@ -162,7 +165,7 @@ exports[\`Test2 1\`] = \`"bar"\`; let lifecycle: Lifecycle; let rootSuite: Suite; beforeEach(async () => { - lifecycle = new Lifecycle(); + lifecycle = new Lifecycle(new ToolingLog()); rootSuite = createRootSuite(); decorateSnapshotUi({ lifecycle, updateSnapshots: true, isCi: false }); @@ -185,7 +188,7 @@ exports[\`Test2 1\`] = \`"bar"\`; fs.writeFileSync( snapshotFile, `// Jest Snapshot v1, https://goo.gl/fbAQLP - + exports[\`Test 1\`] = \`"foo"\`; `, { encoding: 'utf-8' } @@ -219,7 +222,7 @@ exports[\`Test2 1\`] = \`"bar"\`; let lifecycle: Lifecycle; let rootSuite: Suite; beforeEach(async () => { - lifecycle = new Lifecycle(); + lifecycle = new Lifecycle(new ToolingLog()); rootSuite = createRootSuite(); decorateSnapshotUi({ lifecycle, updateSnapshots: false, isCi: true }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts b/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts index 53ce4c74c13887..43f1508ab7938a 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts @@ -9,6 +9,8 @@ import fs from 'fs'; import { join, resolve } from 'path'; +import { ToolingLog } from '@kbn/tooling-log'; + jest.mock('fs'); jest.mock('@kbn/utils', () => { return { REPO_ROOT: '/dev/null/root' }; @@ -60,7 +62,7 @@ describe('SuiteTracker', () => { }; const runLifecycleWithMocks = async (mocks: Suite[], fn: (objs: any) => any = () => {}) => { - const lifecycle = new Lifecycle(); + const lifecycle = new Lifecycle(new ToolingLog()); const suiteTracker = SuiteTracker.startTracking( lifecycle, resolve(REPO_ROOT, MOCK_CONFIG_PATH) diff --git a/packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts b/packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts deleted file mode 100644 index 5789231f87044d..00000000000000 --- a/packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Lifecycle } from './lifecycle'; - -export interface ScreenshotRecord { - name: string; - base64Png: string; - baselinePath?: string; - failurePath?: string; -} - -export class TestMetadata { - // mocha's global types mean we can't import Mocha or it will override the global jest types.............. - private currentRunnable?: any; - - constructor(lifecycle: Lifecycle) { - lifecycle.beforeEachRunnable.add((runnable) => { - this.currentRunnable = runnable; - }); - } - - addScreenshot(screenshot: ScreenshotRecord) { - this.currentRunnable._screenshots = (this.currentRunnable._screenshots || []).concat( - screenshot - ); - } - - getScreenshots(test: any): ScreenshotRecord[] { - if (!test || typeof test !== 'object' || !test._screenshots) { - return []; - } - - return test._screenshots.slice(); - } -} diff --git a/packages/kbn-test/src/functional_test_runner/public_types.ts b/packages/kbn-test/src/functional_test_runner/public_types.ts index 2d632b28d6e212..67adceaf22323c 100644 --- a/packages/kbn-test/src/functional_test_runner/public_types.ts +++ b/packages/kbn-test/src/functional_test_runner/public_types.ts @@ -8,10 +8,10 @@ import type { ToolingLog } from '@kbn/tooling-log'; -import type { Config, Lifecycle, TestMetadata, DockerServersService, EsVersion } from './lib'; +import type { Config, Lifecycle, DockerServersService, EsVersion } from './lib'; import type { Test, Suite } from './fake_mocha_types'; -export { Lifecycle, Config, TestMetadata }; +export { Lifecycle, Config }; export interface AsyncInstance { /** @@ -56,9 +56,7 @@ export interface GenericFtrProviderContext< * Determine if a service is avaliable * @param serviceName */ - hasService( - serviceName: 'config' | 'log' | 'lifecycle' | 'testMetadata' | 'dockerServers' | 'esVersion' - ): true; + hasService(serviceName: 'config' | 'log' | 'lifecycle' | 'dockerServers' | 'esVersion'): true; hasService(serviceName: K): serviceName is K; hasService(serviceName: string): serviceName is Extract; @@ -71,7 +69,6 @@ export interface GenericFtrProviderContext< getService(serviceName: 'log'): ToolingLog; getService(serviceName: 'lifecycle'): Lifecycle; getService(serviceName: 'dockerServers'): DockerServersService; - getService(serviceName: 'testMetadata'): TestMetadata; getService(serviceName: 'esVersion'): EsVersion; getService(serviceName: T): ServiceMap[T]; diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.ts b/packages/kbn-test/src/functional_tests/lib/run_ftr.ts index 6887a664d66574..4c4a7128a05a9c 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.ts @@ -90,6 +90,9 @@ export async function assertNoneExcluded({ configPath, options }: CreateFtrParam } const stats = await ftr.getTestStats(); + if (!stats) { + throw new Error('unable to get test stats'); + } if (stats.testsExcludedByTag.length > 0) { throw new CliError(` ${stats.testsExcludedByTag.length} tests in the ${configPath} config @@ -122,5 +125,8 @@ export async function hasTests({ configPath, options }: CreateFtrParams) { return true; } const stats = await ftr.getTestStats(); - return stats.testCount > 0; + if (!stats) { + throw new Error('unable to get test stats'); + } + return stats.nonSkippedTestCount > 0; } diff --git a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts index 6305e522b39292..47d0b1c93b620b 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts @@ -36,13 +36,13 @@ export async function runKibanaServer({ config: Config; options: { installDir?: string; extraKbnOpts?: string[] }; }) { - const { installDir } = options; const runOptions = config.get('kbnTestServer.runOptions'); + const installDir = runOptions.alwaysUseSource ? undefined : options.installDir; const env = config.get('kbnTestServer.env'); await procs.run('kibana', { cmd: getKibanaCmd(installDir), - args: filterCliArgs(collectCliArgs(config, options)), + args: filterCliArgs(collectCliArgs(config, installDir, options.extraKbnOpts)), env: { FORCE_COLOR: 1, ...process.env, @@ -70,10 +70,7 @@ function getKibanaCmd(installDir?: string) { * passed, we run from source code. We also allow passing in extra * Kibana server options, so we tack those on here. */ -function collectCliArgs( - config: Config, - { installDir, extraKbnOpts }: { installDir?: string; extraKbnOpts?: string[] } -) { +function collectCliArgs(config: Config, installDir?: string, extraKbnOpts: string[] = []) { const buildArgs: string[] = config.get('kbnTestServer.buildArgs') || []; const sourceArgs: string[] = config.get('kbnTestServer.sourceArgs') || []; const serverArgs: string[] = config.get('kbnTestServer.serverArgs') || []; @@ -82,7 +79,7 @@ function collectCliArgs( serverArgs, (args) => (installDir ? args.filter((a: string) => a !== '--oss') : args), (args) => (installDir ? [...buildArgs, ...args] : [KIBANA_EXEC_PATH, ...sourceArgs, ...args]), - (args) => args.concat(extraKbnOpts || []) + (args) => args.concat(extraKbnOpts) ); } diff --git a/packages/kbn-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts index 8116fa25650a1e..dd9fe4c93016c4 100644 --- a/packages/kbn-test/src/functional_tests/tasks.ts +++ b/packages/kbn-test/src/functional_tests/tasks.ts @@ -8,6 +8,7 @@ import { relative } from 'path'; import * as Rx from 'rxjs'; +import { setTimeout } from 'timers/promises'; import { startWith, switchMap, take } from 'rxjs/operators'; import { withProcRunner } from '@kbn/dev-utils'; import { ToolingLog } from '@kbn/tooling-log'; @@ -63,7 +64,7 @@ interface RunTestsParams extends CreateFtrOptions { assertNoneExcluded: boolean; } export async function runTests(options: RunTestsParams) { - if (!process.env.KBN_NP_PLUGINS_BUILT && !options.assertNoneExcluded) { + if (!process.env.CI && !options.assertNoneExcluded) { const log = options.createLogger(); log.warning('❗️❗️❗️'); log.warning('❗️❗️❗️'); @@ -91,21 +92,18 @@ export async function runTests(options: RunTestsParams) { return; } - log.write('--- determining which ftr configs to run'); - const configPathsWithTests: string[] = []; - for (const configPath of options.configs) { - log.info('testing', relative(REPO_ROOT, configPath)); - await log.indent(4, async () => { - if (await hasTests({ configPath, options: { ...options, log } })) { - configPathsWithTests.push(configPath); + for (const [i, configPath] of options.configs.entries()) { + await log.indent(0, async () => { + if (options.configs.length > 1) { + const progress = `${i + 1}/${options.configs.length}`; + log.write(`--- [${progress}] Running ${relative(REPO_ROOT, configPath)}`); } - }); - } - for (const [i, configPath] of configPathsWithTests.entries()) { - await log.indent(0, async () => { - const progress = `${i + 1}/${configPathsWithTests.length}`; - log.write(`--- [${progress}] Running ${relative(REPO_ROOT, configPath)}`); + if (!(await hasTests({ configPath, options: { ...options, log } }))) { + // just run the FTR, no Kibana or ES, which will quickly report a skipped test group to ci-stats and continue + await runFtr({ configPath, options: { ...options, log } }); + return; + } await withProcRunner(log, async (procs) => { const config = await readConfigFile(log, options.esVersion, configPath); @@ -122,7 +120,7 @@ export async function runTests(options: RunTestsParams) { const delay = config.get('kbnTestServer.delayShutdown'); if (typeof delay === 'number') { log.info('Delaying shutdown of Kibana for', delay, 'ms'); - await new Promise((r) => setTimeout(r, delay)); + await setTimeout(delay); } await procs.stop('kibana'); diff --git a/scripts/README.md b/scripts/README.md index 960e8ab2ed0b8f..a743ce5e1d53d9 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -17,13 +17,13 @@ This directory is excluded from the build and tools within it should help users ## Functional Test Scripts -**`node scripts/functional_tests [--config test/functional/config.js --config test/api_integration/config.js]`** +**`node scripts/functional_tests [--config test/functional/config.base.js --config test/api_integration/config.js]`** Runs all the functional tests: selenium tests and api integration tests. List configs with multiple `--config` arguments. Uses the [@kbn/test](../packages/kbn-test) library to run Elasticsearch and Kibana servers and tests against those servers, for multiple server+test setups. In particular, calls out to [`runTests()`](../packages/kbn-test/src/functional_tests/tasks.js). Can be run on a single config. -**`node scripts/functional_tests_server [--config test/functional/config.js]`** +**`node scripts/functional_tests_server [--config test/functional/config.base.js]`** -Starts just the Elasticsearch and Kibana servers given a single config, i.e. via `--config test/functional/config.js` or `--config test/api_integration/config`. Allows the user to start just the servers with this script, and keep them running while running tests against these servers. The idea is that the same config file configures both Elasticsearch and Kibana servers. Uses the [`startServers()`](../packages/kbn-test/src/functional_tests/tasks.js#L52-L80) method from [@kbn/test](../packages/kbn-test) library. +Starts just the Elasticsearch and Kibana servers given a single config, i.e. via `--config test/functional/config.base.js` or `--config test/api_integration/config`. Allows the user to start just the servers with this script, and keep them running while running tests against these servers. The idea is that the same config file configures both Elasticsearch and Kibana servers. Uses the [`startServers()`](../packages/kbn-test/src/functional_tests/tasks.js#L52-L80) method from [@kbn/test](../packages/kbn-test) library. Example. Start servers _and_ run tests, separately, but using the same config: @@ -51,7 +51,7 @@ If you wish to load up specific es archived data for your test, you can do so vi node scripts/es_archiver.js load [--es-url=http://username:password@localhost:9200] [--kibana-url=http://username:password@localhost:5601/{basepath?}] ``` -That will load the specified archive located in the archive directory specified by the default functional config file, located in `test/functional/config.js`. To load archives from other function config files you can pass `--config path/to/config.js`. +That will load the specified archive located in the archive directory specified by the default functional config file, located in `test/functional/config.base.js`. To load archives from other function config files you can pass `--config path/to/config.js`. *Note:* The `--es-url` and `--kibana-url` options may or may not be neccessary depending on your current Kibana configuration settings, and their values may also change based on those settings (for example if you are not running with security you will not need the `username:password` portion). diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 1e963660a1e03b..eb1dea2dcab367 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -7,26 +7,4 @@ */ require('../src/setup_node_env'); -require('@kbn/test').runTestsCli([ - require.resolve('../test/functional/config.ccs.ts'), - require.resolve('../test/functional/config.js'), - require.resolve('../test/plugin_functional/config.ts'), - require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), - require.resolve('../test/new_visualize_flow/config.ts'), - require.resolve('../test/interactive_setup_api_integration/enrollment_flow.config.ts'), - require.resolve('../test/interactive_setup_api_integration/manual_configuration_flow.config.ts'), - require.resolve( - '../test/interactive_setup_api_integration/manual_configuration_flow_without_tls.config.ts' - ), - require.resolve('../test/interactive_setup_functional/enrollment_token.config.ts'), - require.resolve('../test/interactive_setup_functional/manual_configuration.config.ts'), - require.resolve( - '../test/interactive_setup_functional/manual_configuration_without_security.config.ts' - ), - require.resolve( - '../test/interactive_setup_functional/manual_configuration_without_tls.config.ts' - ), - require.resolve('../test/api_integration/config.js'), - require.resolve('../test/interpreter_functional/config.ts'), - require.resolve('../test/examples/config.js'), -]); +require('@kbn/test').runTestsCli(); diff --git a/scripts/functional_tests_server.js b/scripts/functional_tests_server.js index 4995eba4de670d..836a1ede126e31 100644 --- a/scripts/functional_tests_server.js +++ b/scripts/functional_tests_server.js @@ -7,4 +7,4 @@ */ require('../src/setup_node_env'); -require('@kbn/test').startServersCli(require.resolve('../test/functional/config.js')); +require('@kbn/test').startServersCli(require.resolve('../test/functional/config.base.js')); diff --git a/test/accessibility/config.ts b/test/accessibility/config.ts index 59194fcb67826d..9ed89694db5d88 100644 --- a/test/accessibility/config.ts +++ b/test/accessibility/config.ts @@ -11,7 +11,7 @@ import { services } from './services'; import { pageObjects } from './page_objects'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { ...functionalConfig.getAll(), diff --git a/test/analytics/config.ts b/test/analytics/config.ts index 1ecac5af0d01ad..9dee422762e151 100644 --- a/test/analytics/config.ts +++ b/test/analytics/config.ts @@ -19,7 +19,7 @@ import { services } from './services'; */ export default async function ({ readConfigFile }: FtrConfigProviderContext) { const commonConfig = await readConfigFile(require.resolve('../common/config')); - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { testFiles: [require.resolve('./tests')], diff --git a/test/api_integration/apis/saved_objects/lib/saved_objects_test_utils.ts b/test/api_integration/apis/saved_objects/lib/saved_objects_test_utils.ts index d915fc75ed0c35..4e6fc6158e8817 100644 --- a/test/api_integration/apis/saved_objects/lib/saved_objects_test_utils.ts +++ b/test/api_integration/apis/saved_objects/lib/saved_objects_test_utils.ts @@ -14,5 +14,6 @@ export async function getKibanaVersion(getService: FtrProviderContext['getServic const kibanaVersion = await kibanaServer.version.get(); expect(typeof kibanaVersion).to.eql('string'); expect(kibanaVersion.length).to.be.greaterThan(0); - return kibanaVersion; + // mimic SavedObjectsService.stripVersionQualifier() + return kibanaVersion.split('-')[0]; } diff --git a/test/api_integration/config.js b/test/api_integration/config.js index 4988094dad7a2d..7f3f4b45298d17 100644 --- a/test/api_integration/config.js +++ b/test/api_integration/config.js @@ -10,7 +10,7 @@ import { services } from './services'; export default async function ({ readConfigFile }) { const commonConfig = await readConfigFile(require.resolve('../common/config')); - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { rootTags: ['runOutsideOfCiGroups'], diff --git a/test/examples/bfetch_explorer/index.ts b/test/examples/bfetch_explorer/index.ts index 247cef07a487ee..b487704663c624 100644 --- a/test/examples/bfetch_explorer/index.ts +++ b/test/examples/bfetch_explorer/index.ts @@ -14,7 +14,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid const PageObjects = getPageObjects(['common', 'header']); describe('bfetch explorer', function () { - this.tags('ciGroup11'); before(async () => { await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('bfetch-explorer', { insertTimestamp: false }); diff --git a/test/examples/config.js b/test/examples/config.js index 6d1f1ec4723509..25537a22e19ac5 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -12,7 +12,7 @@ import fs from 'fs'; import { KIBANA_ROOT } from '@kbn/test'; export default async function ({ readConfigFile }) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); // Find all folders in /examples and /x-pack/examples since we treat all them as plugin folder const examplesFiles = fs.readdirSync(resolve(KIBANA_ROOT, 'examples')); diff --git a/test/examples/data_view_field_editor_example/index.ts b/test/examples/data_view_field_editor_example/index.ts index 0f8517cb3ed292..fcb8300749f0c8 100644 --- a/test/examples/data_view_field_editor_example/index.ts +++ b/test/examples/data_view_field_editor_example/index.ts @@ -20,7 +20,6 @@ export default function ({ const PageObjects = getPageObjects(['common', 'header', 'settings']); describe('data view field editor example', function () { - this.tags('ciGroup11'); before(async () => { await esArchiver.emptyKibanaIndex(); await browser.setWindowSize(1300, 900); diff --git a/test/examples/embeddables/index.ts b/test/examples/embeddables/index.ts index 364c4001383a7b..6cd95c699e7b8c 100644 --- a/test/examples/embeddables/index.ts +++ b/test/examples/embeddables/index.ts @@ -18,7 +18,6 @@ export default function ({ const PageObjects = getPageObjects(['common', 'header']); describe('embeddable explorer', function () { - this.tags('ciGroup11'); before(async () => { await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('embeddableExplorer'); diff --git a/test/examples/expressions_explorer/index.ts b/test/examples/expressions_explorer/index.ts index 34f3c77cb0d3e9..d7a47b63bd0124 100644 --- a/test/examples/expressions_explorer/index.ts +++ b/test/examples/expressions_explorer/index.ts @@ -18,7 +18,6 @@ export default function ({ const PageObjects = getPageObjects(['common', 'header']); describe('expressions explorer', function () { - this.tags('ciGroup11'); before(async () => { await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('expressionsExplorer'); diff --git a/test/examples/field_formats/index.ts b/test/examples/field_formats/index.ts index f9692c910fda0d..aebd92728b1af8 100644 --- a/test/examples/field_formats/index.ts +++ b/test/examples/field_formats/index.ts @@ -16,7 +16,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Field formats example', function () { before(async () => { - this.tags('ciGroup11'); await PageObjects.common.navigateToApp('fieldFormatsExample'); }); diff --git a/test/examples/hello_world/index.ts b/test/examples/hello_world/index.ts index 1ffb7ff6d69afd..604d014401b8fb 100644 --- a/test/examples/hello_world/index.ts +++ b/test/examples/hello_world/index.ts @@ -17,7 +17,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid describe('Hello world', function () { before(async () => { - this.tags('ciGroup11'); await PageObjects.common.navigateToApp('helloWorld'); }); diff --git a/test/examples/partial_results/index.ts b/test/examples/partial_results/index.ts index 84ccff4cd35b77..6dc76f6a8856c7 100644 --- a/test/examples/partial_results/index.ts +++ b/test/examples/partial_results/index.ts @@ -16,7 +16,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Partial Results Example', function () { before(async () => { - this.tags('ciGroup11'); await PageObjects.common.navigateToApp('partialResultsExample'); const element = await testSubjects.find('example-help'); diff --git a/test/examples/routing/index.ts b/test/examples/routing/index.ts index 949d8cfc7547a6..0012283d8535f7 100644 --- a/test/examples/routing/index.ts +++ b/test/examples/routing/index.ts @@ -17,7 +17,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid describe('routing examples', function () { before(async () => { - this.tags('ciGroup11'); await PageObjects.common.navigateToApp('routingExample'); }); diff --git a/test/examples/state_sync/index.ts b/test/examples/state_sync/index.ts index a33c014f4dd9dc..6f70794497bef6 100644 --- a/test/examples/state_sync/index.ts +++ b/test/examples/state_sync/index.ts @@ -17,7 +17,6 @@ export default function ({ const browser = getService('browser'); describe('state sync examples', function () { - this.tags('ciGroup11'); before(async () => { await browser.setWindowSize(1300, 900); }); diff --git a/test/examples/ui_actions/index.ts b/test/examples/ui_actions/index.ts index b04d361cc86ecf..400af962053ba1 100644 --- a/test/examples/ui_actions/index.ts +++ b/test/examples/ui_actions/index.ts @@ -18,7 +18,6 @@ export default function ({ const PageObjects = getPageObjects(['common', 'header']); describe('ui actions explorer', function () { - this.tags('ciGroup11'); before(async () => { await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('uiActionsExplorer'); diff --git a/test/functional/config.coverage.js b/test/functional/apps/bundles/config.ts similarity index 55% rename from test/functional/config.coverage.js rename to test/functional/apps/bundles/config.ts index 8b0a59ac88b1b3..e487d31dcb657c 100644 --- a/test/functional/config.coverage.js +++ b/test/functional/apps/bundles/config.ts @@ -6,18 +6,13 @@ * Side Public License, v 1. */ -export default async function ({ readConfigFile }) { - const defaultConfig = await readConfigFile(require.resolve('./config')); +import { FtrConfigProviderContext } from '@kbn/test'; - return { - ...defaultConfig.getAll(), - - suiteTags: { - exclude: ['skipCoverage'], - }, +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); - junit: { - reportName: 'Code Coverage for Functional Tests', - }, + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], }; } diff --git a/test/functional/apps/bundles/index.js b/test/functional/apps/bundles/index.js index c3ce4201a470c8..105aa155a9f1fc 100644 --- a/test/functional/apps/bundles/index.js +++ b/test/functional/apps/bundles/index.js @@ -14,7 +14,7 @@ export default function ({ getService }) { const supertest = getService('supertest'); describe('bundle compression', function () { - this.tags(['ciGroup11', 'skipCoverage']); + this.tags('skipCoverage'); let buildNum; before(async () => { diff --git a/test/functional/apps/console/config.ts b/test/functional/apps/console/config.ts new file mode 100644 index 00000000000000..e487d31dcb657c --- /dev/null +++ b/test/functional/apps/console/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/console/index.js b/test/functional/apps/console/index.js index c3d0553514cb55..1944e10b5239f8 100644 --- a/test/functional/apps/console/index.js +++ b/test/functional/apps/console/index.js @@ -10,8 +10,6 @@ export default function ({ getService, loadTestFile }) { const browser = getService('browser'); describe('console app', function () { - this.tags('ciGroup1'); - before(async function () { await browser.setWindowSize(1300, 1100); }); diff --git a/test/functional/apps/context/config.ts b/test/functional/apps/context/config.ts new file mode 100644 index 00000000000000..e487d31dcb657c --- /dev/null +++ b/test/functional/apps/context/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/context/index.ts b/test/functional/apps/context/index.ts index 1320a22aad09bd..20e1bcc2a3cb43 100644 --- a/test/functional/apps/context/index.ts +++ b/test/functional/apps/context/index.ts @@ -15,8 +15,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid const kibanaServer = getService('kibanaServer'); describe('context app', function () { - this.tags('ciGroup1'); - before(async () => { await browser.setWindowSize(1200, 800); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); diff --git a/test/functional/apps/dashboard/README.md b/test/functional/apps/dashboard/README.md new file mode 100644 index 00000000000000..5e87a8b210bdd4 --- /dev/null +++ b/test/functional/apps/dashboard/README.md @@ -0,0 +1,7 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations \ No newline at end of file diff --git a/test/functional/apps/dashboard/group1/config.ts b/test/functional/apps/dashboard/group1/config.ts new file mode 100644 index 00000000000000..a70a190ca63f81 --- /dev/null +++ b/test/functional/apps/dashboard/group1/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.ts b/test/functional/apps/dashboard/group1/create_and_add_embeddables.ts similarity index 99% rename from test/functional/apps/dashboard/create_and_add_embeddables.ts rename to test/functional/apps/dashboard/group1/create_and_add_embeddables.ts index 30100f0e1aa07e..c96e596a88ecfe 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.ts +++ b/test/functional/apps/dashboard/group1/create_and_add_embeddables.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import { VisualizeConstants } from '@kbn/visualizations-plugin/common/constants'; import { VISUALIZE_ENABLE_LABS_SETTING } from '@kbn/visualizations-plugin/common/constants'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/dashboard_back_button.ts b/test/functional/apps/dashboard/group1/dashboard_back_button.ts similarity index 96% rename from test/functional/apps/dashboard/dashboard_back_button.ts rename to test/functional/apps/dashboard/group1/dashboard_back_button.ts index d532444befdab9..1fd9614d2421a4 100644 --- a/test/functional/apps/dashboard/dashboard_back_button.ts +++ b/test/functional/apps/dashboard/group1/dashboard_back_button.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); diff --git a/test/functional/apps/dashboard/dashboard_error_handling.ts b/test/functional/apps/dashboard/group1/dashboard_error_handling.ts similarity index 97% rename from test/functional/apps/dashboard/dashboard_error_handling.ts rename to test/functional/apps/dashboard/group1/dashboard_error_handling.ts index 58304359458c7e..e950b8aef975d7 100644 --- a/test/functional/apps/dashboard/dashboard_error_handling.ts +++ b/test/functional/apps/dashboard/group1/dashboard_error_handling.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'common']); diff --git a/test/functional/apps/dashboard/dashboard_options.ts b/test/functional/apps/dashboard/group1/dashboard_options.ts similarity index 96% rename from test/functional/apps/dashboard/dashboard_options.ts rename to test/functional/apps/dashboard/group1/dashboard_options.ts index 282674d0cec983..096f8595072bf9 100644 --- a/test/functional/apps/dashboard/dashboard_options.ts +++ b/test/functional/apps/dashboard/group1/dashboard_options.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/dashboard_query_bar.ts b/test/functional/apps/dashboard/group1/dashboard_query_bar.ts similarity index 96% rename from test/functional/apps/dashboard/dashboard_query_bar.ts rename to test/functional/apps/dashboard/group1/dashboard_query_bar.ts index 5092cadaf9d26c..290cc62dca58f2 100644 --- a/test/functional/apps/dashboard/dashboard_query_bar.ts +++ b/test/functional/apps/dashboard/group1/dashboard_query_bar.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/test/functional/apps/dashboard/dashboard_unsaved_listing.ts b/test/functional/apps/dashboard/group1/dashboard_unsaved_listing.ts similarity index 99% rename from test/functional/apps/dashboard/dashboard_unsaved_listing.ts rename to test/functional/apps/dashboard/group1/dashboard_unsaved_listing.ts index a1db57784b5f8d..6b55a44ff9e79d 100644 --- a/test/functional/apps/dashboard/dashboard_unsaved_listing.ts +++ b/test/functional/apps/dashboard/group1/dashboard_unsaved_listing.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); diff --git a/test/functional/apps/dashboard/dashboard_unsaved_state.ts b/test/functional/apps/dashboard/group1/dashboard_unsaved_state.ts similarity index 99% rename from test/functional/apps/dashboard/dashboard_unsaved_state.ts rename to test/functional/apps/dashboard/group1/dashboard_unsaved_state.ts index 5afe3b9937433f..2447a122a77aae 100644 --- a/test/functional/apps/dashboard/dashboard_unsaved_state.ts +++ b/test/functional/apps/dashboard/group1/dashboard_unsaved_state.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); diff --git a/test/functional/apps/dashboard/data_shared_attributes.ts b/test/functional/apps/dashboard/group1/data_shared_attributes.ts similarity index 98% rename from test/functional/apps/dashboard/data_shared_attributes.ts rename to test/functional/apps/dashboard/group1/data_shared_attributes.ts index a94cf1b6063a92..d4070c700a9251 100644 --- a/test/functional/apps/dashboard/data_shared_attributes.ts +++ b/test/functional/apps/dashboard/group1/data_shared_attributes.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/edit_embeddable_redirects.ts b/test/functional/apps/dashboard/group1/edit_embeddable_redirects.ts similarity index 98% rename from test/functional/apps/dashboard/edit_embeddable_redirects.ts rename to test/functional/apps/dashboard/group1/edit_embeddable_redirects.ts index 763488cc21ab1f..aca22d84e68439 100644 --- a/test/functional/apps/dashboard/edit_embeddable_redirects.ts +++ b/test/functional/apps/dashboard/group1/edit_embeddable_redirects.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); diff --git a/test/functional/apps/dashboard/edit_visualizations.js b/test/functional/apps/dashboard/group1/edit_visualizations.js similarity index 100% rename from test/functional/apps/dashboard/edit_visualizations.js rename to test/functional/apps/dashboard/group1/edit_visualizations.js diff --git a/test/functional/apps/dashboard/embed_mode.ts b/test/functional/apps/dashboard/group1/embed_mode.ts similarity index 98% rename from test/functional/apps/dashboard/embed_mode.ts rename to test/functional/apps/dashboard/group1/embed_mode.ts index 7e53bff7387ca2..25f48236ab7d58 100644 --- a/test/functional/apps/dashboard/embed_mode.ts +++ b/test/functional/apps/dashboard/group1/embed_mode.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/test/functional/apps/dashboard/embeddable_data_grid.ts b/test/functional/apps/dashboard/group1/embeddable_data_grid.ts similarity index 97% rename from test/functional/apps/dashboard/embeddable_data_grid.ts rename to test/functional/apps/dashboard/group1/embeddable_data_grid.ts index 060c4676566627..85277e63d6f6c0 100644 --- a/test/functional/apps/dashboard/embeddable_data_grid.ts +++ b/test/functional/apps/dashboard/group1/embeddable_data_grid.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardAddPanel = getService('dashboardAddPanel'); diff --git a/test/functional/apps/dashboard/embeddable_rendering.ts b/test/functional/apps/dashboard/group1/embeddable_rendering.ts similarity index 99% rename from test/functional/apps/dashboard/embeddable_rendering.ts rename to test/functional/apps/dashboard/group1/embeddable_rendering.ts index 840826be46532b..5274a2c12e878b 100644 --- a/test/functional/apps/dashboard/embeddable_rendering.ts +++ b/test/functional/apps/dashboard/group1/embeddable_rendering.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; /** * This tests both that one of each visualization can be added to a dashboard (as opposed to opening an existing diff --git a/test/functional/apps/dashboard/empty_dashboard.ts b/test/functional/apps/dashboard/group1/empty_dashboard.ts similarity index 97% rename from test/functional/apps/dashboard/empty_dashboard.ts rename to test/functional/apps/dashboard/group1/empty_dashboard.ts index a7524eaa94b8a9..e559c0ef81f607 100644 --- a/test/functional/apps/dashboard/empty_dashboard.ts +++ b/test/functional/apps/dashboard/group1/empty_dashboard.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/test/functional/apps/dashboard/group1/index.ts b/test/functional/apps/dashboard/group1/index.ts new file mode 100644 index 00000000000000..597102433ef45f --- /dev/null +++ b/test/functional/apps/dashboard/group1/index.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + async function loadCurrentData() { + await browser.setWindowSize(1300, 900); + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data'); + } + + async function unloadCurrentData() { + await esArchiver.unload('test/functional/fixtures/es_archiver/dashboard/current/data'); + } + + describe('dashboard app - group 1', function () { + before(loadCurrentData); + after(unloadCurrentData); + + // This has to be first since the other tests create some embeddables as side affects and our counting assumes + // a fresh index. + loadTestFile(require.resolve('./empty_dashboard')); + loadTestFile(require.resolve('./url_field_formatter')); + loadTestFile(require.resolve('./embeddable_rendering')); + loadTestFile(require.resolve('./embeddable_data_grid')); + loadTestFile(require.resolve('./create_and_add_embeddables')); + loadTestFile(require.resolve('./edit_embeddable_redirects')); + loadTestFile(require.resolve('./dashboard_unsaved_state')); + loadTestFile(require.resolve('./dashboard_unsaved_listing')); + loadTestFile(require.resolve('./edit_visualizations')); + loadTestFile(require.resolve('./dashboard_options')); + loadTestFile(require.resolve('./data_shared_attributes')); + loadTestFile(require.resolve('./share')); + loadTestFile(require.resolve('./embed_mode')); + loadTestFile(require.resolve('./dashboard_back_button')); + loadTestFile(require.resolve('./dashboard_error_handling')); + loadTestFile(require.resolve('./legacy_urls')); + loadTestFile(require.resolve('./saved_search_embeddable')); + + // Note: This one must be last because it unloads some data for one of its tests! + // No, this isn't ideal, but loading/unloading takes so much time and these are all bunched + // to improve efficiency... + loadTestFile(require.resolve('./dashboard_query_bar')); + }); +} diff --git a/test/functional/apps/dashboard/legacy_urls.ts b/test/functional/apps/dashboard/group1/legacy_urls.ts similarity index 98% rename from test/functional/apps/dashboard/legacy_urls.ts rename to test/functional/apps/dashboard/group1/legacy_urls.ts index 1e4138e63d393c..e11da2d82fe478 100644 --- a/test/functional/apps/dashboard/legacy_urls.ts +++ b/test/functional/apps/dashboard/group1/legacy_urls.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/test/functional/apps/dashboard/saved_search_embeddable.ts b/test/functional/apps/dashboard/group1/saved_search_embeddable.ts similarity index 98% rename from test/functional/apps/dashboard/saved_search_embeddable.ts rename to test/functional/apps/dashboard/group1/saved_search_embeddable.ts index 02050eec30227b..e0ecc40d2486b6 100644 --- a/test/functional/apps/dashboard/saved_search_embeddable.ts +++ b/test/functional/apps/dashboard/group1/saved_search_embeddable.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardAddPanel = getService('dashboardAddPanel'); diff --git a/test/functional/apps/dashboard/share.ts b/test/functional/apps/dashboard/group1/share.ts similarity index 95% rename from test/functional/apps/dashboard/share.ts rename to test/functional/apps/dashboard/group1/share.ts index 7fe8048ab7c047..871ab5bed14884 100644 --- a/test/functional/apps/dashboard/share.ts +++ b/test/functional/apps/dashboard/group1/share.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); diff --git a/test/functional/apps/dashboard/url_field_formatter.ts b/test/functional/apps/dashboard/group1/url_field_formatter.ts similarity index 95% rename from test/functional/apps/dashboard/url_field_formatter.ts rename to test/functional/apps/dashboard/group1/url_field_formatter.ts index 8e9dd7b66e79fd..be454549af378e 100644 --- a/test/functional/apps/dashboard/url_field_formatter.ts +++ b/test/functional/apps/dashboard/group1/url_field_formatter.ts @@ -7,8 +7,8 @@ */ import expect from '@kbn/expect'; -import { WebElementWrapper } from '../../services/lib/web_element_wrapper'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../../../services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const { common, dashboard, settings, timePicker, visChart } = getPageObjects([ diff --git a/test/functional/apps/dashboard/group2/config.ts b/test/functional/apps/dashboard/group2/config.ts new file mode 100644 index 00000000000000..a70a190ca63f81 --- /dev/null +++ b/test/functional/apps/dashboard/group2/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.ts b/test/functional/apps/dashboard/group2/dashboard_filter_bar.ts similarity index 99% rename from test/functional/apps/dashboard/dashboard_filter_bar.ts rename to test/functional/apps/dashboard/group2/dashboard_filter_bar.ts index 3f74c4bc2f0dc5..966b453409433f 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.ts +++ b/test/functional/apps/dashboard/group2/dashboard_filter_bar.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataGrid = getService('dataGrid'); diff --git a/test/functional/apps/dashboard/dashboard_filtering.ts b/test/functional/apps/dashboard/group2/dashboard_filtering.ts similarity index 99% rename from test/functional/apps/dashboard/dashboard_filtering.ts rename to test/functional/apps/dashboard/group2/dashboard_filtering.ts index 9522c47f907fca..09acbd5965020e 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.ts +++ b/test/functional/apps/dashboard/group2/dashboard_filtering.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; /** * Test the querying capabilities of dashboard, and make sure visualizations show the expected results, especially diff --git a/test/functional/apps/dashboard/dashboard_grid.ts b/test/functional/apps/dashboard/group2/dashboard_grid.ts similarity index 96% rename from test/functional/apps/dashboard/dashboard_grid.ts rename to test/functional/apps/dashboard/group2/dashboard_grid.ts index 25e901fd25d8b1..90e2187e19eb44 100644 --- a/test/functional/apps/dashboard/dashboard_grid.ts +++ b/test/functional/apps/dashboard/group2/dashboard_grid.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/test/functional/apps/dashboard/dashboard_saved_query.ts b/test/functional/apps/dashboard/group2/dashboard_saved_query.ts similarity index 98% rename from test/functional/apps/dashboard/dashboard_saved_query.ts rename to test/functional/apps/dashboard/group2/dashboard_saved_query.ts index 658afb9c641b23..ac9613f4bf400b 100644 --- a/test/functional/apps/dashboard/dashboard_saved_query.ts +++ b/test/functional/apps/dashboard/group2/dashboard_saved_query.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); diff --git a/test/functional/apps/dashboard/dashboard_snapshots.ts b/test/functional/apps/dashboard/group2/dashboard_snapshots.ts similarity index 98% rename from test/functional/apps/dashboard/dashboard_snapshots.ts rename to test/functional/apps/dashboard/group2/dashboard_snapshots.ts index 9cb52c5dd55118..dc1a74ea74b7d8 100644 --- a/test/functional/apps/dashboard/dashboard_snapshots.ts +++ b/test/functional/apps/dashboard/group2/dashboard_snapshots.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, diff --git a/test/functional/apps/dashboard/embeddable_library.ts b/test/functional/apps/dashboard/group2/embeddable_library.ts similarity index 97% rename from test/functional/apps/dashboard/embeddable_library.ts rename to test/functional/apps/dashboard/group2/embeddable_library.ts index 2abf75f6385ac5..ca52eaecaf46e5 100644 --- a/test/functional/apps/dashboard/embeddable_library.ts +++ b/test/functional/apps/dashboard/group2/embeddable_library.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); diff --git a/test/functional/apps/dashboard/full_screen_mode.ts b/test/functional/apps/dashboard/group2/full_screen_mode.ts similarity index 98% rename from test/functional/apps/dashboard/full_screen_mode.ts rename to test/functional/apps/dashboard/group2/full_screen_mode.ts index 74fa2168a1461a..35d9ed8a2a15c6 100644 --- a/test/functional/apps/dashboard/full_screen_mode.ts +++ b/test/functional/apps/dashboard/group2/full_screen_mode.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/group2/index.ts b/test/functional/apps/dashboard/group2/index.ts new file mode 100644 index 00000000000000..004c85f2da7601 --- /dev/null +++ b/test/functional/apps/dashboard/group2/index.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + async function loadCurrentData() { + await browser.setWindowSize(1300, 900); + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data'); + } + + async function unloadCurrentData() { + await esArchiver.unload('test/functional/fixtures/es_archiver/dashboard/current/data'); + } + + describe('dashboard app - group 2', function () { + before(loadCurrentData); + after(unloadCurrentData); + + loadTestFile(require.resolve('./full_screen_mode')); + loadTestFile(require.resolve('./dashboard_filter_bar')); + loadTestFile(require.resolve('./dashboard_filtering')); + loadTestFile(require.resolve('./panel_expand_toggle')); + loadTestFile(require.resolve('./dashboard_grid')); + loadTestFile(require.resolve('./view_edit')); + loadTestFile(require.resolve('./dashboard_saved_query')); + // Order of test suites *shouldn't* be important but there's a bug for the view_edit test above + // https://github.com/elastic/kibana/issues/46752 + // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. + // If we don't use the timestamp in the URL, the colors in the charts will be different. + loadTestFile(require.resolve('./dashboard_snapshots')); + loadTestFile(require.resolve('./embeddable_library')); + }); +} diff --git a/test/functional/apps/dashboard/panel_expand_toggle.ts b/test/functional/apps/dashboard/group2/panel_expand_toggle.ts similarity index 97% rename from test/functional/apps/dashboard/panel_expand_toggle.ts rename to test/functional/apps/dashboard/group2/panel_expand_toggle.ts index 272ec3824e2338..f33280ba7bb799 100644 --- a/test/functional/apps/dashboard/panel_expand_toggle.ts +++ b/test/functional/apps/dashboard/group2/panel_expand_toggle.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/view_edit.ts b/test/functional/apps/dashboard/group2/view_edit.ts similarity index 99% rename from test/functional/apps/dashboard/view_edit.ts rename to test/functional/apps/dashboard/group2/view_edit.ts index a73924a8ae75f6..dfd62eeaa6cb3c 100644 --- a/test/functional/apps/dashboard/view_edit.ts +++ b/test/functional/apps/dashboard/group2/view_edit.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const queryBar = getService('queryBar'); diff --git a/test/functional/apps/dashboard/bwc_shared_urls.ts b/test/functional/apps/dashboard/group3/bwc_shared_urls.ts similarity index 99% rename from test/functional/apps/dashboard/bwc_shared_urls.ts rename to test/functional/apps/dashboard/group3/bwc_shared_urls.ts index 569cd8e2a67d51..01b1c8379089eb 100644 --- a/test/functional/apps/dashboard/bwc_shared_urls.ts +++ b/test/functional/apps/dashboard/group3/bwc_shared_urls.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header']); diff --git a/test/functional/apps/dashboard/group3/config.ts b/test/functional/apps/dashboard/group3/config.ts new file mode 100644 index 00000000000000..a70a190ca63f81 --- /dev/null +++ b/test/functional/apps/dashboard/group3/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard/copy_panel_to.ts b/test/functional/apps/dashboard/group3/copy_panel_to.ts similarity index 98% rename from test/functional/apps/dashboard/copy_panel_to.ts rename to test/functional/apps/dashboard/group3/copy_panel_to.ts index 9a61b289ee1f3b..1f40f780a5398c 100644 --- a/test/functional/apps/dashboard/copy_panel_to.ts +++ b/test/functional/apps/dashboard/group3/copy_panel_to.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardVisualizations = getService('dashboardVisualizations'); diff --git a/test/functional/apps/dashboard/dashboard_state.ts b/test/functional/apps/dashboard/group3/dashboard_state.ts similarity index 98% rename from test/functional/apps/dashboard/dashboard_state.ts rename to test/functional/apps/dashboard/group3/dashboard_state.ts index d931475766776b..48fb9233682ad0 100644 --- a/test/functional/apps/dashboard/dashboard_state.ts +++ b/test/functional/apps/dashboard/group3/dashboard_state.ts @@ -10,8 +10,8 @@ import expect from '@kbn/expect'; import chroma from 'chroma-js'; import { DEFAULT_PANEL_WIDTH } from '@kbn/dashboard-plugin/public/application/embeddable/dashboard_constants'; -import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME } from '../../page_objects/dashboard_page'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME } from '../../../page_objects/dashboard_page'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/test/functional/apps/dashboard/dashboard_time_picker.ts b/test/functional/apps/dashboard/group3/dashboard_time_picker.ts similarity index 97% rename from test/functional/apps/dashboard/dashboard_time_picker.ts rename to test/functional/apps/dashboard/group3/dashboard_time_picker.ts index 6f876185fd8dd7..37f6e4f2ef5df6 100644 --- a/test/functional/apps/dashboard/dashboard_time_picker.ts +++ b/test/functional/apps/dashboard/group3/dashboard_time_picker.ts @@ -8,8 +8,8 @@ import expect from '@kbn/expect'; -import { PIE_CHART_VIS_NAME } from '../../page_objects/dashboard_page'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { PIE_CHART_VIS_NAME } from '../../../page_objects/dashboard_page'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardExpect = getService('dashboardExpect'); diff --git a/test/functional/apps/dashboard/group3/index.ts b/test/functional/apps/dashboard/group3/index.ts new file mode 100644 index 00000000000000..f3a10500fe4e6a --- /dev/null +++ b/test/functional/apps/dashboard/group3/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + async function loadLogstash() { + await browser.setWindowSize(1200, 900); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + } + + async function unloadLogstash() { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + } + + describe('dashboard app - group 3', function () { + before(loadLogstash); + after(unloadLogstash); + + loadTestFile(require.resolve('./dashboard_time_picker')); + loadTestFile(require.resolve('./bwc_shared_urls')); + loadTestFile(require.resolve('./panel_replacing')); + loadTestFile(require.resolve('./panel_cloning')); + loadTestFile(require.resolve('./copy_panel_to')); + loadTestFile(require.resolve('./panel_context_menu')); + loadTestFile(require.resolve('./dashboard_state')); + }); +} diff --git a/test/functional/apps/dashboard/panel_cloning.ts b/test/functional/apps/dashboard/group3/panel_cloning.ts similarity index 96% rename from test/functional/apps/dashboard/panel_cloning.ts rename to test/functional/apps/dashboard/group3/panel_cloning.ts index a2cadd89f486ae..4de65419d2ecb2 100644 --- a/test/functional/apps/dashboard/panel_cloning.ts +++ b/test/functional/apps/dashboard/group3/panel_cloning.ts @@ -7,8 +7,8 @@ */ import expect from '@kbn/expect'; -import { PIE_CHART_VIS_NAME } from '../../page_objects/dashboard_page'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { PIE_CHART_VIS_NAME } from '../../../page_objects/dashboard_page'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); diff --git a/test/functional/apps/dashboard/panel_context_menu.ts b/test/functional/apps/dashboard/group3/panel_context_menu.ts similarity index 98% rename from test/functional/apps/dashboard/panel_context_menu.ts rename to test/functional/apps/dashboard/group3/panel_context_menu.ts index 8c82c162f5e861..f78cd27614b3b1 100644 --- a/test/functional/apps/dashboard/panel_context_menu.ts +++ b/test/functional/apps/dashboard/group3/panel_context_menu.ts @@ -8,8 +8,8 @@ import expect from '@kbn/expect'; import { VisualizeConstants } from '@kbn/visualizations-plugin/common/constants'; -import { PIE_CHART_VIS_NAME } from '../../page_objects/dashboard_page'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { PIE_CHART_VIS_NAME } from '../../../page_objects/dashboard_page'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/test/functional/apps/dashboard/panel_replacing.ts b/test/functional/apps/dashboard/group3/panel_replacing.ts similarity index 97% rename from test/functional/apps/dashboard/panel_replacing.ts rename to test/functional/apps/dashboard/group3/panel_replacing.ts index b9ba731beee290..e6ff8c4f940bb9 100644 --- a/test/functional/apps/dashboard/panel_replacing.ts +++ b/test/functional/apps/dashboard/group3/panel_replacing.ts @@ -11,8 +11,8 @@ import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME, LINE_CHART_VIS_NAME, -} from '../../page_objects/dashboard_page'; -import { FtrProviderContext } from '../../ftr_provider_context'; +} from '../../../page_objects/dashboard_page'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/test/functional/apps/dashboard/group4/config.ts b/test/functional/apps/dashboard/group4/config.ts new file mode 100644 index 00000000000000..a70a190ca63f81 --- /dev/null +++ b/test/functional/apps/dashboard/group4/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard/dashboard_clone.ts b/test/functional/apps/dashboard/group4/dashboard_clone.ts similarity index 97% rename from test/functional/apps/dashboard/dashboard_clone.ts rename to test/functional/apps/dashboard/group4/dashboard_clone.ts index 5fc0b0c28c9142..26738760b28e46 100644 --- a/test/functional/apps/dashboard/dashboard_clone.ts +++ b/test/functional/apps/dashboard/group4/dashboard_clone.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/dashboard_listing.ts b/test/functional/apps/dashboard/group4/dashboard_listing.ts similarity index 99% rename from test/functional/apps/dashboard/dashboard_listing.ts rename to test/functional/apps/dashboard/group4/dashboard_listing.ts index 9182a0d3182287..4a9827ce02f91a 100644 --- a/test/functional/apps/dashboard/dashboard_listing.ts +++ b/test/functional/apps/dashboard/group4/dashboard_listing.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'common']); diff --git a/test/functional/apps/dashboard/dashboard_save.ts b/test/functional/apps/dashboard/group4/dashboard_save.ts similarity index 98% rename from test/functional/apps/dashboard/dashboard_save.ts rename to test/functional/apps/dashboard/group4/dashboard_save.ts index 4ab8633a5619b3..f20817c65d25d2 100644 --- a/test/functional/apps/dashboard/dashboard_save.ts +++ b/test/functional/apps/dashboard/group4/dashboard_save.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize']); diff --git a/test/functional/apps/dashboard/dashboard_time.ts b/test/functional/apps/dashboard/group4/dashboard_time.ts similarity index 98% rename from test/functional/apps/dashboard/dashboard_time.ts rename to test/functional/apps/dashboard/group4/dashboard_time.ts index 2c0394474adabe..2ff91185be60a6 100644 --- a/test/functional/apps/dashboard/dashboard_time.ts +++ b/test/functional/apps/dashboard/group4/dashboard_time.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; const dashboardName = 'Dashboard Test Time'; diff --git a/test/functional/apps/dashboard/group4/index.ts b/test/functional/apps/dashboard/group4/index.ts new file mode 100644 index 00000000000000..8c9a291d2e5771 --- /dev/null +++ b/test/functional/apps/dashboard/group4/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + async function loadLogstash() { + await browser.setWindowSize(1200, 900); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + } + + async function unloadLogstash() { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + } + + describe('dashboard app - group 4', function () { + before(loadLogstash); + after(unloadLogstash); + + loadTestFile(require.resolve('./dashboard_save')); + loadTestFile(require.resolve('./dashboard_time')); + loadTestFile(require.resolve('./dashboard_listing')); + loadTestFile(require.resolve('./dashboard_clone')); + }); +} diff --git a/test/functional/apps/dashboard/group5/config.ts b/test/functional/apps/dashboard/group5/config.ts new file mode 100644 index 00000000000000..a70a190ca63f81 --- /dev/null +++ b/test/functional/apps/dashboard/group5/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard/group5/index.ts b/test/functional/apps/dashboard/group5/index.ts new file mode 100644 index 00000000000000..14f4a6366477dc --- /dev/null +++ b/test/functional/apps/dashboard/group5/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + + async function loadLogstash() { + await browser.setWindowSize(1200, 900); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + } + + async function unloadLogstash() { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + } + + describe('dashboard app - group 5', function () { + // TODO: Remove when vislib is removed + // https://github.com/elastic/kibana/issues/56143 + describe('new charts library', function () { + before(async () => { + await loadLogstash(); + await kibanaServer.uiSettings.update({ + 'visualization:visualize:legacyPieChartsLibrary': false, + }); + await browser.refresh(); + }); + + after(async () => { + await unloadLogstash(); + await kibanaServer.uiSettings.update({ + 'visualization:visualize:legacyPieChartsLibrary': true, + }); + await browser.refresh(); + }); + + loadTestFile(require.resolve('../group3/dashboard_state')); + }); + }); +} diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts deleted file mode 100644 index 4f69504ef7d5cf..00000000000000 --- a/test/functional/apps/dashboard/index.ts +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, loadTestFile }: FtrProviderContext) { - const browser = getService('browser'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - - async function loadCurrentData() { - await browser.setWindowSize(1300, 900); - await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data'); - } - - async function unloadCurrentData() { - await esArchiver.unload('test/functional/fixtures/es_archiver/dashboard/current/data'); - } - - async function loadLogstash() { - await browser.setWindowSize(1200, 900); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - } - - async function unloadLogstash() { - await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); - } - - describe('dashboard app', function () { - // This has to be first since the other tests create some embeddables as side affects and our counting assumes - // a fresh index. - describe('using current data', function () { - this.tags('ciGroup2'); - before(loadCurrentData); - after(unloadCurrentData); - - loadTestFile(require.resolve('./empty_dashboard')); - loadTestFile(require.resolve('./url_field_formatter')); - loadTestFile(require.resolve('./embeddable_rendering')); - loadTestFile(require.resolve('./embeddable_data_grid')); - loadTestFile(require.resolve('./create_and_add_embeddables')); - loadTestFile(require.resolve('./edit_embeddable_redirects')); - loadTestFile(require.resolve('./dashboard_unsaved_state')); - loadTestFile(require.resolve('./dashboard_unsaved_listing')); - loadTestFile(require.resolve('./edit_visualizations')); - loadTestFile(require.resolve('./dashboard_options')); - loadTestFile(require.resolve('./data_shared_attributes')); - loadTestFile(require.resolve('./share')); - loadTestFile(require.resolve('./embed_mode')); - loadTestFile(require.resolve('./dashboard_back_button')); - loadTestFile(require.resolve('./dashboard_error_handling')); - loadTestFile(require.resolve('./legacy_urls')); - loadTestFile(require.resolve('./saved_search_embeddable')); - - // Note: This one must be last because it unloads some data for one of its tests! - // No, this isn't ideal, but loading/unloading takes so much time and these are all bunched - // to improve efficiency... - loadTestFile(require.resolve('./dashboard_query_bar')); - }); - - describe('using current data', function () { - this.tags('ciGroup3'); - before(loadCurrentData); - after(unloadCurrentData); - - loadTestFile(require.resolve('./full_screen_mode')); - loadTestFile(require.resolve('./dashboard_filter_bar')); - loadTestFile(require.resolve('./dashboard_filtering')); - loadTestFile(require.resolve('./panel_expand_toggle')); - loadTestFile(require.resolve('./dashboard_grid')); - loadTestFile(require.resolve('./view_edit')); - loadTestFile(require.resolve('./dashboard_saved_query')); - // Order of test suites *shouldn't* be important but there's a bug for the view_edit test above - // https://github.com/elastic/kibana/issues/46752 - // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. - // If we don't use the timestamp in the URL, the colors in the charts will be different. - loadTestFile(require.resolve('./dashboard_snapshots')); - loadTestFile(require.resolve('./embeddable_library')); - }); - - // Each of these tests call initTests themselves, the way it was originally written. The above tests only load - // the data once to save on time. Eventually, all of these tests should just use current data and we can reserve - // legacy data only for specifically testing BWC situations. - describe('using legacy data', function () { - this.tags('ciGroup4'); - before(loadLogstash); - after(unloadLogstash); - - loadTestFile(require.resolve('./dashboard_time_picker')); - loadTestFile(require.resolve('./bwc_shared_urls')); - loadTestFile(require.resolve('./panel_replacing')); - loadTestFile(require.resolve('./panel_cloning')); - loadTestFile(require.resolve('./copy_panel_to')); - loadTestFile(require.resolve('./panel_context_menu')); - loadTestFile(require.resolve('./dashboard_state')); - }); - - describe('using legacy data', function () { - this.tags('ciGroup5'); - before(loadLogstash); - after(unloadLogstash); - - loadTestFile(require.resolve('./dashboard_save')); - loadTestFile(require.resolve('./dashboard_time')); - loadTestFile(require.resolve('./dashboard_listing')); - loadTestFile(require.resolve('./dashboard_clone')); - }); - - // TODO: Remove when vislib is removed - // https://github.com/elastic/kibana/issues/56143 - describe('new charts library', function () { - this.tags('ciGroup5'); - - before(async () => { - await loadLogstash(); - await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyPieChartsLibrary': false, - }); - await browser.refresh(); - }); - - after(async () => { - await unloadLogstash(); - await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyPieChartsLibrary': true, - }); - await browser.refresh(); - }); - - loadTestFile(require.resolve('./dashboard_state')); - }); - }); -} diff --git a/test/functional/apps/dashboard_elements/config.ts b/test/functional/apps/dashboard_elements/config.ts new file mode 100644 index 00000000000000..e487d31dcb657c --- /dev/null +++ b/test/functional/apps/dashboard_elements/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard_elements/index.ts b/test/functional/apps/dashboard_elements/index.ts index 6bd3e4e04a9c95..10b0c6e5ecff57 100644 --- a/test/functional/apps/dashboard_elements/index.ts +++ b/test/functional/apps/dashboard_elements/index.ts @@ -29,9 +29,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('test/functional/fixtures/es_archiver/long_window_logstash'); }); - describe('dashboard elements ciGroup10', function () { - this.tags('ciGroup10'); - + describe('dashboard elements', function () { loadTestFile(require.resolve('./input_control_vis')); loadTestFile(require.resolve('./controls')); loadTestFile(require.resolve('./_markdown_vis')); diff --git a/test/functional/apps/discover/config.ts b/test/functional/apps/discover/config.ts new file mode 100644 index 00000000000000..e487d31dcb657c --- /dev/null +++ b/test/functional/apps/discover/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index e2895f3ca56b4f..20f8f017b084fa 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -13,8 +13,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const config = getService('config'); describe('discover app', function () { - this.tags('ciGroup6'); - before(async function () { await browser.setWindowSize(1300, 800); }); diff --git a/test/functional/apps/getting_started/config.ts b/test/functional/apps/getting_started/config.ts new file mode 100644 index 00000000000000..e487d31dcb657c --- /dev/null +++ b/test/functional/apps/getting_started/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/getting_started/index.ts b/test/functional/apps/getting_started/index.ts index c88999e23be3de..a0506ce52e0283 100644 --- a/test/functional/apps/getting_started/index.ts +++ b/test/functional/apps/getting_started/index.ts @@ -13,8 +13,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); describe('Getting Started ', function () { - this.tags(['ciGroup5']); - before(async function () { await browser.setWindowSize(1200, 800); }); diff --git a/test/functional/apps/home/config.ts b/test/functional/apps/home/config.ts new file mode 100644 index 00000000000000..e487d31dcb657c --- /dev/null +++ b/test/functional/apps/home/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/home/index.js b/test/functional/apps/home/index.js index 992c5b1f2f4744..ada4aa54073cf4 100644 --- a/test/functional/apps/home/index.js +++ b/test/functional/apps/home/index.js @@ -9,9 +9,7 @@ export default function ({ getService, loadTestFile }) { const browser = getService('browser'); - describe('homepage app', function () { - this.tags('ciGroup5'); - + describe('home app', function () { before(function () { return browser.setWindowSize(1200, 800); }); diff --git a/test/functional/apps/management/config.ts b/test/functional/apps/management/config.ts new file mode 100644 index 00000000000000..e487d31dcb657c --- /dev/null +++ b/test/functional/apps/management/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index a4271ae73d9e2f..840d04d0d1aedc 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -21,33 +21,24 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('test/functional/fixtures/es_archiver/makelogs'); }); - describe('', function () { - this.tags('ciGroup9'); - - loadTestFile(require.resolve('./_create_index_pattern_wizard')); - loadTestFile(require.resolve('./_index_pattern_create_delete')); - loadTestFile(require.resolve('./_index_pattern_results_sort')); - loadTestFile(require.resolve('./_index_pattern_popularity')); - loadTestFile(require.resolve('./_kibana_settings')); - loadTestFile(require.resolve('./_scripted_fields_preview')); - loadTestFile(require.resolve('./_mgmt_import_saved_objects')); - loadTestFile(require.resolve('./_index_patterns_empty')); - loadTestFile(require.resolve('./_scripted_fields')); - loadTestFile(require.resolve('./_runtime_fields')); - loadTestFile(require.resolve('./_field_formatter')); - loadTestFile(require.resolve('./_legacy_url_redirect')); - loadTestFile(require.resolve('./_exclude_index_pattern')); - }); - - describe('', function () { - this.tags('ciGroup8'); - - loadTestFile(require.resolve('./_index_pattern_filter')); - loadTestFile(require.resolve('./_scripted_fields_filter')); - loadTestFile(require.resolve('./_import_objects')); - loadTestFile(require.resolve('./_test_huge_fields')); - loadTestFile(require.resolve('./_handle_alias')); - loadTestFile(require.resolve('./_handle_version_conflict')); - }); + loadTestFile(require.resolve('./_create_index_pattern_wizard')); + loadTestFile(require.resolve('./_index_pattern_create_delete')); + loadTestFile(require.resolve('./_index_pattern_results_sort')); + loadTestFile(require.resolve('./_index_pattern_popularity')); + loadTestFile(require.resolve('./_kibana_settings')); + loadTestFile(require.resolve('./_scripted_fields_preview')); + loadTestFile(require.resolve('./_mgmt_import_saved_objects')); + loadTestFile(require.resolve('./_index_patterns_empty')); + loadTestFile(require.resolve('./_scripted_fields')); + loadTestFile(require.resolve('./_runtime_fields')); + loadTestFile(require.resolve('./_field_formatter')); + loadTestFile(require.resolve('./_legacy_url_redirect')); + loadTestFile(require.resolve('./_exclude_index_pattern')); + loadTestFile(require.resolve('./_index_pattern_filter')); + loadTestFile(require.resolve('./_scripted_fields_filter')); + loadTestFile(require.resolve('./_import_objects')); + loadTestFile(require.resolve('./_test_huge_fields')); + loadTestFile(require.resolve('./_handle_alias')); + loadTestFile(require.resolve('./_handle_version_conflict')); }); } diff --git a/test/functional/apps/saved_objects_management/config.ts b/test/functional/apps/saved_objects_management/config.ts new file mode 100644 index 00000000000000..e487d31dcb657c --- /dev/null +++ b/test/functional/apps/saved_objects_management/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/saved_objects_management/index.ts b/test/functional/apps/saved_objects_management/index.ts index 12e0cc8863f126..c70b2c6d25b179 100644 --- a/test/functional/apps/saved_objects_management/index.ts +++ b/test/functional/apps/saved_objects_management/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function savedObjectsManagementApp({ loadTestFile }: FtrProviderContext) { describe('saved objects management', function savedObjectsManagementAppTestSuite() { - this.tags('ciGroup7'); loadTestFile(require.resolve('./inspect_saved_objects')); loadTestFile(require.resolve('./show_relationships')); }); diff --git a/test/functional/apps/status_page/config.ts b/test/functional/apps/status_page/config.ts new file mode 100644 index 00000000000000..e487d31dcb657c --- /dev/null +++ b/test/functional/apps/status_page/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/status_page/index.ts b/test/functional/apps/status_page/index.ts index 509abeb4f03466..971f9c4984c99e 100644 --- a/test/functional/apps/status_page/index.ts +++ b/test/functional/apps/status_page/index.ts @@ -14,8 +14,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common']); describe('status page', function () { - this.tags('ciGroup1'); - beforeEach(async () => { await PageObjects.common.navigateToApp('status_page'); }); diff --git a/test/functional/apps/visualize/README.md b/test/functional/apps/visualize/README.md new file mode 100644 index 00000000000000..5e87a8b210bdd4 --- /dev/null +++ b/test/functional/apps/visualize/README.md @@ -0,0 +1,7 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations \ No newline at end of file diff --git a/test/functional/apps/visualize/_chart_types.ts b/test/functional/apps/visualize/group1/_chart_types.ts similarity index 96% rename from test/functional/apps/visualize/_chart_types.ts rename to test/functional/apps/visualize/group1/_chart_types.ts index 1afc372f75b0e8..4b5922e21a51c2 100644 --- a/test/functional/apps/visualize/_chart_types.ts +++ b/test/functional/apps/visualize/group1/_chart_types.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_data_table.ts b/test/functional/apps/visualize/group1/_data_table.ts similarity index 99% rename from test/functional/apps/visualize/_data_table.ts rename to test/functional/apps/visualize/group1/_data_table.ts index e165e40e83dc13..9b95c5b69fd411 100644 --- a/test/functional/apps/visualize/_data_table.ts +++ b/test/functional/apps/visualize/group1/_data_table.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.ts b/test/functional/apps/visualize/group1/_data_table_nontimeindex.ts similarity index 98% rename from test/functional/apps/visualize/_data_table_nontimeindex.ts rename to test/functional/apps/visualize/group1/_data_table_nontimeindex.ts index 1549f2aac07353..43407d3a899ea9 100644 --- a/test/functional/apps/visualize/_data_table_nontimeindex.ts +++ b/test/functional/apps/visualize/group1/_data_table_nontimeindex.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts b/test/functional/apps/visualize/group1/_data_table_notimeindex_filters.ts similarity index 97% rename from test/functional/apps/visualize/_data_table_notimeindex_filters.ts rename to test/functional/apps/visualize/group1/_data_table_notimeindex_filters.ts index 51ceef947bfac1..d62bdd86ecf9b2 100644 --- a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts +++ b/test/functional/apps/visualize/group1/_data_table_notimeindex_filters.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_embedding_chart.ts b/test/functional/apps/visualize/group1/_embedding_chart.ts similarity index 98% rename from test/functional/apps/visualize/_embedding_chart.ts rename to test/functional/apps/visualize/group1/_embedding_chart.ts index 9531eafc33bedc..a07e3a36e2aea9 100644 --- a/test/functional/apps/visualize/_embedding_chart.ts +++ b/test/functional/apps/visualize/group1/_embedding_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); diff --git a/test/functional/apps/visualize/group1/config.ts b/test/functional/apps/visualize/group1/config.ts new file mode 100644 index 00000000000000..a70a190ca63f81 --- /dev/null +++ b/test/functional/apps/visualize/group1/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/group1/index.ts b/test/functional/apps/visualize/group1/index.ts new file mode 100644 index 00000000000000..fa3379b632cc17 --- /dev/null +++ b/test/functional/apps/visualize/group1/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('visualize app - group1', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + loadTestFile(require.resolve('./_embedding_chart')); + loadTestFile(require.resolve('./_data_table')); + loadTestFile(require.resolve('./_data_table_nontimeindex')); + loadTestFile(require.resolve('./_data_table_notimeindex_filters')); + loadTestFile(require.resolve('./_chart_types')); + }); +} diff --git a/test/functional/apps/visualize/_experimental_vis.ts b/test/functional/apps/visualize/group2/_experimental_vis.ts similarity index 97% rename from test/functional/apps/visualize/_experimental_vis.ts rename to test/functional/apps/visualize/group2/_experimental_vis.ts index 8e33285f909bed..26460192a6b968 100644 --- a/test/functional/apps/visualize/_experimental_vis.ts +++ b/test/functional/apps/visualize/group2/_experimental_vis.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_gauge_chart.ts b/test/functional/apps/visualize/group2/_gauge_chart.ts similarity index 98% rename from test/functional/apps/visualize/_gauge_chart.ts rename to test/functional/apps/visualize/group2/_gauge_chart.ts index 6dd460d4ac32b4..2c20c913b4d16d 100644 --- a/test/functional/apps/visualize/_gauge_chart.ts +++ b/test/functional/apps/visualize/group2/_gauge_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_heatmap_chart.ts b/test/functional/apps/visualize/group2/_heatmap_chart.ts similarity index 98% rename from test/functional/apps/visualize/_heatmap_chart.ts rename to test/functional/apps/visualize/group2/_heatmap_chart.ts index 54cec19be97ff8..1c82b66273251f 100644 --- a/test/functional/apps/visualize/_heatmap_chart.ts +++ b/test/functional/apps/visualize/group2/_heatmap_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_histogram_request_start.ts b/test/functional/apps/visualize/group2/_histogram_request_start.ts similarity index 98% rename from test/functional/apps/visualize/_histogram_request_start.ts rename to test/functional/apps/visualize/group2/_histogram_request_start.ts index 28ebb25744d3fb..a12474d9ebc2ee 100644 --- a/test/functional/apps/visualize/_histogram_request_start.ts +++ b/test/functional/apps/visualize/group2/_histogram_request_start.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_inspector.ts b/test/functional/apps/visualize/group2/_inspector.ts similarity index 98% rename from test/functional/apps/visualize/_inspector.ts rename to test/functional/apps/visualize/group2/_inspector.ts index f83eae2fc00bc2..7b306f7817f5c6 100644 --- a/test/functional/apps/visualize/_inspector.ts +++ b/test/functional/apps/visualize/group2/_inspector.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_metric_chart.ts b/test/functional/apps/visualize/group2/_metric_chart.ts similarity index 99% rename from test/functional/apps/visualize/_metric_chart.ts rename to test/functional/apps/visualize/group2/_metric_chart.ts index 7853a3a845bfc4..b797ccb6303637 100644 --- a/test/functional/apps/visualize/_metric_chart.ts +++ b/test/functional/apps/visualize/group2/_metric_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/group2/config.ts b/test/functional/apps/visualize/group2/config.ts new file mode 100644 index 00000000000000..a70a190ca63f81 --- /dev/null +++ b/test/functional/apps/visualize/group2/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/group2/index.ts b/test/functional/apps/visualize/group2/index.ts new file mode 100644 index 00000000000000..ea5ad24e2f8733 --- /dev/null +++ b/test/functional/apps/visualize/group2/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('visualize app', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + loadTestFile(require.resolve('./_inspector')); + loadTestFile(require.resolve('./_experimental_vis')); + loadTestFile(require.resolve('./_gauge_chart')); + loadTestFile(require.resolve('./_heatmap_chart')); + loadTestFile(require.resolve('./_histogram_request_start')); + loadTestFile(require.resolve('./_metric_chart')); + }); +} diff --git a/test/functional/apps/visualize/_add_to_dashboard.ts b/test/functional/apps/visualize/group3/_add_to_dashboard.ts similarity index 99% rename from test/functional/apps/visualize/_add_to_dashboard.ts rename to test/functional/apps/visualize/group3/_add_to_dashboard.ts index 9e8984675eccd8..32d329cd181da0 100644 --- a/test/functional/apps/visualize/_add_to_dashboard.ts +++ b/test/functional/apps/visualize/group3/_add_to_dashboard.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardExpect = getService('dashboardExpect'); diff --git a/test/functional/apps/visualize/_lab_mode.ts b/test/functional/apps/visualize/group3/_lab_mode.ts similarity index 97% rename from test/functional/apps/visualize/_lab_mode.ts rename to test/functional/apps/visualize/group3/_lab_mode.ts index 2af593f2acc4a8..ba1b8ae33e2ce3 100644 --- a/test/functional/apps/visualize/_lab_mode.ts +++ b/test/functional/apps/visualize/group3/_lab_mode.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { VISUALIZE_ENABLE_LABS_SETTING } from '@kbn/visualizations-plugin/common/constants'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_linked_saved_searches.ts b/test/functional/apps/visualize/group3/_linked_saved_searches.ts similarity index 98% rename from test/functional/apps/visualize/_linked_saved_searches.ts rename to test/functional/apps/visualize/group3/_linked_saved_searches.ts index 6fa8acac8e7819..e64a3f18bde951 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.ts +++ b/test/functional/apps/visualize/group3/_linked_saved_searches.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/test/functional/apps/visualize/_pie_chart.ts b/test/functional/apps/visualize/group3/_pie_chart.ts similarity index 95% rename from test/functional/apps/visualize/_pie_chart.ts rename to test/functional/apps/visualize/group3/_pie_chart.ts index 48d49d3007b685..23b008c690cba1 100644 --- a/test/functional/apps/visualize/_pie_chart.ts +++ b/test/functional/apps/visualize/group3/_pie_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); @@ -401,7 +401,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ['360,000', '47', 'US', '4'], ['360,000', '47', 'BD', '3'], ['360,000', '47', 'BR', '2'], - ]; + ].map((row) => + // the count of records is not shown for every split level in the new charting library + isNewChartsLibraryEnabled ? [row[0], ...row.slice(2)] : row + ); await inspector.open(); await inspector.setTablePageSize(50); @@ -447,6 +450,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should still showing pie chart when a subseries have zero data', async function () { + if (isNewChartsLibraryEnabled) { + // TODO: it seems that adding a filter agg which has no results to a pie chart breaks it and instead it shows "no data" + return; + } + await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickPieChart'); await PageObjects.visualize.clickPieChart(); @@ -518,7 +526,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ['osx', '1,322', 'US', '130'], ['osx', '1,322', 'ID', '56'], ['osx', '1,322', 'BR', '30'], - ]; + ].map((row) => + // the count of records is not shown for every split level in the new charting library + isNewChartsLibraryEnabled ? [row[0], ...row.slice(2)] : row + ); await inspector.open(); await inspector.setTablePageSize(50); await inspector.expectTableData(expectedTableData); @@ -532,7 +543,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ['win xp', '526', 'CN', '526'], ['ios', '478', 'CN', '478'], ['osx', '228', 'CN', '228'], - ]; + ].map((row) => + // the count of records is not shown for every split level in the new charting library + isNewChartsLibraryEnabled ? [row[0], ...row.slice(2)] : row + ); await PageObjects.visChart.filterLegend('CN'); await PageObjects.header.waitUntilLoadingHasFinished(); await inspector.open(); diff --git a/test/functional/apps/visualize/_shared_item.ts b/test/functional/apps/visualize/group3/_shared_item.ts similarity index 95% rename from test/functional/apps/visualize/_shared_item.ts rename to test/functional/apps/visualize/group3/_shared_item.ts index 3f9016ca2ff827..0a84ae1962c63b 100644 --- a/test/functional/apps/visualize/_shared_item.ts +++ b/test/functional/apps/visualize/group3/_shared_item.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_visualize_listing.ts b/test/functional/apps/visualize/group3/_visualize_listing.ts similarity index 98% rename from test/functional/apps/visualize/_visualize_listing.ts rename to test/functional/apps/visualize/group3/_visualize_listing.ts index 30b19b52af258e..ad370939f22604 100644 --- a/test/functional/apps/visualize/_visualize_listing.ts +++ b/test/functional/apps/visualize/group3/_visualize_listing.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visEditor']); diff --git a/test/functional/apps/visualize/group3/config.ts b/test/functional/apps/visualize/group3/config.ts new file mode 100644 index 00000000000000..a70a190ca63f81 --- /dev/null +++ b/test/functional/apps/visualize/group3/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/group3/index.ts b/test/functional/apps/visualize/group3/index.ts new file mode 100644 index 00000000000000..93eff60575cb3b --- /dev/null +++ b/test/functional/apps/visualize/group3/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('visualize app', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + loadTestFile(require.resolve('./_pie_chart')); + loadTestFile(require.resolve('./_shared_item')); + loadTestFile(require.resolve('./_lab_mode')); + loadTestFile(require.resolve('./_linked_saved_searches')); + loadTestFile(require.resolve('./_visualize_listing')); + loadTestFile(require.resolve('./_add_to_dashboard.ts')); + }); +} diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/group4/_tsvb_chart.ts similarity index 99% rename from test/functional/apps/visualize/_tsvb_chart.ts rename to test/functional/apps/visualize/group4/_tsvb_chart.ts index d462a89108c09f..013c0473a59b97 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/group4/_tsvb_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/test/functional/apps/visualize/group4/config.ts b/test/functional/apps/visualize/group4/config.ts new file mode 100644 index 00000000000000..a70a190ca63f81 --- /dev/null +++ b/test/functional/apps/visualize/group4/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/group4/index.ts b/test/functional/apps/visualize/group4/index.ts new file mode 100644 index 00000000000000..34764897064152 --- /dev/null +++ b/test/functional/apps/visualize/group4/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('visualize app', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + loadTestFile(require.resolve('./_tsvb_chart')); + }); +} diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/group5/_tsvb_time_series.ts similarity index 99% rename from test/functional/apps/visualize/_tsvb_time_series.ts rename to test/functional/apps/visualize/group5/_tsvb_time_series.ts index 3f6661aaecf004..409b2b3610f5c7 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/group5/_tsvb_time_series.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const { visualize, visualBuilder, timeToVisualize, dashboard, common } = getPageObjects([ diff --git a/test/functional/apps/visualize/group5/config.ts b/test/functional/apps/visualize/group5/config.ts new file mode 100644 index 00000000000000..a70a190ca63f81 --- /dev/null +++ b/test/functional/apps/visualize/group5/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/group5/index.ts b/test/functional/apps/visualize/group5/index.ts new file mode 100644 index 00000000000000..eafa39962ff568 --- /dev/null +++ b/test/functional/apps/visualize/group5/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('visualize app', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + loadTestFile(require.resolve('./_tsvb_time_series')); + }); +} diff --git a/test/functional/apps/visualize/_tag_cloud.ts b/test/functional/apps/visualize/group6/_tag_cloud.ts similarity index 99% rename from test/functional/apps/visualize/_tag_cloud.ts rename to test/functional/apps/visualize/group6/_tag_cloud.ts index 9380f40e0d36c8..93a7fb22a2ca83 100644 --- a/test/functional/apps/visualize/_tag_cloud.ts +++ b/test/functional/apps/visualize/group6/_tag_cloud.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/group6/_tsvb_markdown.ts similarity index 99% rename from test/functional/apps/visualize/_tsvb_markdown.ts rename to test/functional/apps/visualize/group6/_tsvb_markdown.ts index 98ed05d854f0c4..80756b1eacbfbf 100644 --- a/test/functional/apps/visualize/_tsvb_markdown.ts +++ b/test/functional/apps/visualize/group6/_tsvb_markdown.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const { visualBuilder, timePicker, visualize, visChart } = getPageObjects([ diff --git a/test/functional/apps/visualize/_tsvb_table.ts b/test/functional/apps/visualize/group6/_tsvb_table.ts similarity index 99% rename from test/functional/apps/visualize/_tsvb_table.ts rename to test/functional/apps/visualize/group6/_tsvb_table.ts index ed668e4bca8e5e..e7e24885cb406b 100644 --- a/test/functional/apps/visualize/_tsvb_table.ts +++ b/test/functional/apps/visualize/group6/_tsvb_table.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const { visualBuilder, visualize, visChart, settings } = getPageObjects([ diff --git a/test/functional/apps/visualize/_vega_chart.ts b/test/functional/apps/visualize/group6/_vega_chart.ts similarity index 99% rename from test/functional/apps/visualize/_vega_chart.ts rename to test/functional/apps/visualize/group6/_vega_chart.ts index 6640b37b4a28a6..78a370523071bb 100644 --- a/test/functional/apps/visualize/_vega_chart.ts +++ b/test/functional/apps/visualize/group6/_vega_chart.ts @@ -9,7 +9,7 @@ import { unzip } from 'lodash'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; const getTestSpec = (expression: string) => ` { diff --git a/test/functional/apps/visualize/group6/config.ts b/test/functional/apps/visualize/group6/config.ts new file mode 100644 index 00000000000000..a70a190ca63f81 --- /dev/null +++ b/test/functional/apps/visualize/group6/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/group6/index.ts b/test/functional/apps/visualize/group6/index.ts new file mode 100644 index 00000000000000..05fe3b232d3708 --- /dev/null +++ b/test/functional/apps/visualize/group6/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('visualize app', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + loadTestFile(require.resolve('./_tag_cloud')); + loadTestFile(require.resolve('./_tsvb_markdown')); + loadTestFile(require.resolve('./_tsvb_table')); + loadTestFile(require.resolve('./_vega_chart')); + }); +} diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts deleted file mode 100644 index d68fb4b2531239..00000000000000 --- a/test/functional/apps/visualize/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, loadTestFile }: FtrProviderContext) { - const browser = getService('browser'); - const log = getService('log'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - - describe('visualize app', () => { - before(async () => { - log.debug('Starting visualize before method'); - await browser.setWindowSize(1280, 800); - await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); - - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); - }); - - // TODO: Remove when vislib is removed - describe('new charts library visualize ciGroup7', function () { - this.tags('ciGroup7'); - - before(async () => { - await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyPieChartsLibrary': false, - 'visualization:visualize:legacyHeatmapChartsLibrary': false, - }); - await browser.refresh(); - }); - - after(async () => { - await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyPieChartsLibrary': true, - 'visualization:visualize:legacyHeatmapChartsLibrary': true, - }); - await browser.refresh(); - }); - - // Test replaced vislib chart types - loadTestFile(require.resolve('./_area_chart')); - loadTestFile(require.resolve('./_line_chart_split_series')); - loadTestFile(require.resolve('./_line_chart_split_chart')); - loadTestFile(require.resolve('./_point_series_options')); - loadTestFile(require.resolve('./_vertical_bar_chart')); - loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); - loadTestFile(require.resolve('./_pie_chart')); - loadTestFile(require.resolve('./_timelion')); - loadTestFile(require.resolve('./_heatmap_chart')); - }); - - describe('visualize ciGroup9', function () { - this.tags('ciGroup9'); - - loadTestFile(require.resolve('./_embedding_chart')); - loadTestFile(require.resolve('./_data_table')); - loadTestFile(require.resolve('./_data_table_nontimeindex')); - loadTestFile(require.resolve('./_data_table_notimeindex_filters')); - loadTestFile(require.resolve('./_chart_types')); - }); - - describe('visualize ciGroup10', function () { - this.tags('ciGroup10'); - - loadTestFile(require.resolve('./_inspector')); - loadTestFile(require.resolve('./_experimental_vis')); - loadTestFile(require.resolve('./_gauge_chart')); - loadTestFile(require.resolve('./_heatmap_chart')); - loadTestFile(require.resolve('./_histogram_request_start')); - loadTestFile(require.resolve('./_metric_chart')); - }); - - describe('visualize ciGroup1', function () { - this.tags('ciGroup1'); - - loadTestFile(require.resolve('./_pie_chart')); - loadTestFile(require.resolve('./_shared_item')); - loadTestFile(require.resolve('./_lab_mode')); - loadTestFile(require.resolve('./_linked_saved_searches')); - loadTestFile(require.resolve('./_visualize_listing')); - loadTestFile(require.resolve('./_add_to_dashboard.ts')); - }); - - describe('visualize ciGroup8', function () { - this.tags('ciGroup8'); - - loadTestFile(require.resolve('./_tsvb_chart')); - }); - - describe('visualize ciGroup11', function () { - this.tags('ciGroup11'); - - loadTestFile(require.resolve('./_tsvb_time_series')); - }); - - describe('visualize ciGroup12', function () { - this.tags('ciGroup12'); - - loadTestFile(require.resolve('./_tag_cloud')); - loadTestFile(require.resolve('./_tsvb_markdown')); - loadTestFile(require.resolve('./_tsvb_table')); - loadTestFile(require.resolve('./_vega_chart')); - }); - }); -} diff --git a/test/functional/apps/visualize/_area_chart.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts similarity index 99% rename from test/functional/apps/visualize/_area_chart.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts index 76bb1d2f58d055..5fbb264910dca3 100644 --- a/test/functional/apps/visualize/_area_chart.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_line_chart_split_chart.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_chart.ts similarity index 99% rename from test/functional/apps/visualize/_line_chart_split_chart.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_chart.ts index 0e44c30499ed38..77ddc3bbac1a4c 100644 --- a/test/functional/apps/visualize/_line_chart_split_chart.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_line_chart_split_series.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_series.ts similarity index 99% rename from test/functional/apps/visualize/_line_chart_split_series.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_series.ts index d10b4ebd9b312e..a46c46fda48ad6 100644 --- a/test/functional/apps/visualize/_line_chart_split_series.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_series.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_point_series_options.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_point_series_options.ts similarity index 99% rename from test/functional/apps/visualize/_point_series_options.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_point_series_options.ts index a2d2831c879334..1a11d19064ce71 100644 --- a/test/functional/apps/visualize/_point_series_options.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_point_series_options.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_timelion.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_timelion.ts similarity index 99% rename from test/functional/apps/visualize/_timelion.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_timelion.ts index e10ba03a0e19f7..dc80083a67697d 100644 --- a/test/functional/apps/visualize/_timelion.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_timelion.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const { timePicker, visChart, visEditor, visualize, timelion, common } = getPageObjects([ diff --git a/test/functional/apps/visualize/_vertical_bar_chart.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart.ts similarity index 99% rename from test/functional/apps/visualize/_vertical_bar_chart.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart.ts index 7c4f989724ad9f..b8d5cd64bbc1ff 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart_nontimeindex.ts similarity index 99% rename from test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart_nontimeindex.ts index eadc7c58af5a5a..4f00bac7792c40 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart_nontimeindex.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/replaced_vislib_chart_types/config.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/config.ts new file mode 100644 index 00000000000000..a70a190ca63f81 --- /dev/null +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/replaced_vislib_chart_types/index.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/index.ts new file mode 100644 index 00000000000000..5794edef68555b --- /dev/null +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/index.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + + // TODO: Remove when vislib is removed + describe('visualize app - new charts library visualize', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + before(async () => { + await kibanaServer.uiSettings.update({ + 'visualization:visualize:legacyPieChartsLibrary': false, + 'visualization:visualize:legacyHeatmapChartsLibrary': false, + }); + await browser.refresh(); + }); + + after(async () => { + await kibanaServer.uiSettings.update({ + 'visualization:visualize:legacyPieChartsLibrary': true, + 'visualization:visualize:legacyHeatmapChartsLibrary': true, + }); + await browser.refresh(); + }); + + // Test replaced vislib chart types + loadTestFile(require.resolve('./_area_chart')); + loadTestFile(require.resolve('./_line_chart_split_series')); + loadTestFile(require.resolve('./_line_chart_split_chart')); + loadTestFile(require.resolve('./_point_series_options')); + loadTestFile(require.resolve('./_vertical_bar_chart')); + loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); + loadTestFile(require.resolve('./_timelion')); + loadTestFile(require.resolve('../group3/_pie_chart')); + loadTestFile(require.resolve('../group2/_heatmap_chart')); + }); +} diff --git a/test/functional/config.js b/test/functional/config.base.js similarity index 95% rename from test/functional/config.js rename to test/functional/config.base.js index 9221bef808e011..40b50da505951f 100644 --- a/test/functional/config.js +++ b/test/functional/config.base.js @@ -13,20 +13,6 @@ export default async function ({ readConfigFile }) { const commonConfig = await readConfigFile(require.resolve('../common/config')); return { - testFiles: [ - require.resolve('./apps/status_page'), - require.resolve('./apps/bundles'), - require.resolve('./apps/console'), - require.resolve('./apps/context'), - require.resolve('./apps/dashboard'), - require.resolve('./apps/dashboard_elements'), - require.resolve('./apps/discover'), - require.resolve('./apps/getting_started'), - require.resolve('./apps/home'), - require.resolve('./apps/management'), - require.resolve('./apps/saved_objects_management'), - require.resolve('./apps/visualize'), - ], pageObjects, services, diff --git a/test/functional/config.ccs.ts b/test/functional/config.ccs.ts index 8137635b474daa..e5a3736d6fbc3e 100644 --- a/test/functional/config.ccs.ts +++ b/test/functional/config.ccs.ts @@ -12,15 +12,15 @@ import { RemoteEsProvider } from './services/remote_es/remote_es'; // eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('./config')); + const baseConfig = await readConfigFile(require.resolve('./config.base.js')); return { - ...functionalConfig.getAll(), + ...baseConfig.getAll(), testFiles: [require.resolve('./apps/discover')], services: { - ...functionalConfig.get('services'), + ...baseConfig.get('services'), remoteEs: RemoteEsProvider, remoteEsArchiver: RemoteEsArchiverProvider, }, @@ -30,7 +30,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, security: { - ...functionalConfig.get('security'), + ...baseConfig.get('security'), remoteEsRoles: { ccs_remote_search: { indices: [ @@ -41,17 +41,15 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ], }, }, - defaultRoles: [...(functionalConfig.get('security.defaultRoles') ?? []), 'ccs_remote_search'], + defaultRoles: [...(baseConfig.get('security.defaultRoles') ?? []), 'ccs_remote_search'], }, esTestCluster: { - ...functionalConfig.get('esTestCluster'), + ...baseConfig.get('esTestCluster'), ccs: { remoteClusterUrl: process.env.REMOTE_CLUSTER_URL ?? - `http://elastic:changeme@localhost:${ - functionalConfig.get('servers.elasticsearch.port') + 1 - }`, + `http://elastic:changeme@localhost:${baseConfig.get('servers.elasticsearch.port') + 1}`, }, }, }; diff --git a/test/functional/config.edge.js b/test/functional/config.edge.js index 89dc4c39bca4c0..86e213fd31e9f5 100644 --- a/test/functional/config.edge.js +++ b/test/functional/config.edge.js @@ -7,10 +7,10 @@ */ export default async function ({ readConfigFile }) { - const defaultConfig = await readConfigFile(require.resolve('./config')); + const firefoxConfig = await readConfigFile(require.resolve('./config.firefox.js')); return { - ...defaultConfig.getAll(), + ...firefoxConfig.getAll(), browser: { type: 'msedge', diff --git a/test/functional/config.firefox.js b/test/functional/config.firefox.js index 145add328c9cac..79a757b1f1116f 100644 --- a/test/functional/config.firefox.js +++ b/test/functional/config.firefox.js @@ -7,16 +7,26 @@ */ export default async function ({ readConfigFile }) { - const defaultConfig = await readConfigFile(require.resolve('./config')); + const baseConfig = await readConfigFile(require.resolve('./config.base.js')); return { - ...defaultConfig.getAll(), + ...baseConfig.getAll(), + + testFiles: [ + require.resolve('./apps/console'), + require.resolve('./apps/dashboard/group4/dashboard_save'), + require.resolve('./apps/dashboard_elements'), + require.resolve('./apps/discover'), + require.resolve('./apps/home'), + require.resolve('./apps/visualize/group5'), + ], browser: { type: 'firefox', }, suiteTags: { + include: ['includeFirefox'], exclude: ['skipFirefox'], }, diff --git a/test/functional/services/common/screenshots.ts b/test/functional/services/common/screenshots.ts index d5f901300941f7..5fca603407760c 100644 --- a/test/functional/services/common/screenshots.ts +++ b/test/functional/services/common/screenshots.ts @@ -22,7 +22,6 @@ const writeFileAsync = promisify(writeFile); export class ScreenshotsService extends FtrService { private readonly log = this.ctx.getService('log'); private readonly config = this.ctx.getService('config'); - private readonly testMetadata = this.ctx.getService('testMetadata'); private readonly browser = this.ctx.getService('browser'); private readonly SESSION_DIRECTORY = resolve(this.config.get('screenshots.directory'), 'session'); @@ -54,13 +53,7 @@ export class ScreenshotsService extends FtrService { const baselinePath = resolve(this.BASELINE_DIRECTORY, `${name}.png`); const failurePath = resolve(this.FAILURE_DIRECTORY, `${name}.png`); - await this.capture({ - path: sessionPath, - name, - el, - baselinePath, - failurePath, - }); + await this.capture(sessionPath, el); if (updateBaselines) { this.log.debug('Updating baseline snapshot'); @@ -82,42 +75,20 @@ export class ScreenshotsService extends FtrService { async take(name: string, el?: WebElementWrapper, subDirectories: string[] = []) { const path = resolve(this.SESSION_DIRECTORY, ...subDirectories, `${name}.png`); - await this.capture({ path, name, el }); + await this.capture(path, el); } async takeForFailure(name: string, el?: WebElementWrapper) { const path = resolve(this.FAILURE_DIRECTORY, `${name}.png`); - await this.capture({ - path, - name: `failure[${name}]`, - el, - }); + await this.capture(path, el); } - private async capture({ - path, - el, - name, - baselinePath, - failurePath, - }: { - path: string; - name: string; - el?: WebElementWrapper; - baselinePath?: string; - failurePath?: string; - }) { + private async capture(path: string, el?: WebElementWrapper) { try { this.log.info(`Taking screenshot "${path}"`); const screenshot = await (el ? el.takeScreenshot() : this.browser.takeScreenshot()); await mkdirAsync(dirname(path), { recursive: true }); await writeFileAsync(path, screenshot, 'base64'); - this.testMetadata.addScreenshot({ - name, - base64Png: Buffer.isBuffer(screenshot) ? screenshot.toString('base64') : screenshot, - baselinePath, - failurePath, - }); } catch (err) { this.log.error('SCREENSHOT FAILED'); this.log.error(err); diff --git a/test/interactive_setup_api_integration/tests/enrollment_flow.ts b/test/interactive_setup_api_integration/tests/enrollment_flow.ts index f35509f49480a3..a9cec78e391c19 100644 --- a/test/interactive_setup_api_integration/tests/enrollment_flow.ts +++ b/test/interactive_setup_api_integration/tests/enrollment_flow.ts @@ -20,7 +20,7 @@ export default function (context: FtrProviderContext) { const config = context.getService('config'); describe('Interactive setup APIs - Enrollment flow', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); let kibanaVerificationCode: string; let elasticsearchCaFingerprint: string; diff --git a/test/interactive_setup_api_integration/tests/manual_configuration_flow.ts b/test/interactive_setup_api_integration/tests/manual_configuration_flow.ts index 94a06363367ad7..156834d503e8f3 100644 --- a/test/interactive_setup_api_integration/tests/manual_configuration_flow.ts +++ b/test/interactive_setup_api_integration/tests/manual_configuration_flow.ts @@ -19,7 +19,7 @@ export default function (context: FtrProviderContext) { const config = context.getService('config'); describe('Interactive setup APIs - Manual configuration flow', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); let kibanaVerificationCode: string; let elasticsearchCaCertificate: string; diff --git a/test/interactive_setup_api_integration/tests/manual_configuration_flow_without_tls.ts b/test/interactive_setup_api_integration/tests/manual_configuration_flow_without_tls.ts index a3964c5fd5aa6e..6035b6571a1fc8 100644 --- a/test/interactive_setup_api_integration/tests/manual_configuration_flow_without_tls.ts +++ b/test/interactive_setup_api_integration/tests/manual_configuration_flow_without_tls.ts @@ -18,7 +18,7 @@ export default function (context: FtrProviderContext) { const config = context.getService('config'); describe('Interactive setup APIs - Manual configuration flow without TLS', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); let kibanaVerificationCode: string; before(async () => { diff --git a/test/interactive_setup_functional/manual_configuration_without_security.config.ts b/test/interactive_setup_functional/manual_configuration_without_security.config.ts index 953b33d4e2077c..48c917e853b5a0 100644 --- a/test/interactive_setup_functional/manual_configuration_without_security.config.ts +++ b/test/interactive_setup_functional/manual_configuration_without_security.config.ts @@ -13,7 +13,7 @@ import type { FtrConfigProviderContext } from '@kbn/test'; import { getDataPath } from '@kbn/utils'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); const testEndpointsPlugin = resolve( __dirname, diff --git a/test/interactive_setup_functional/tests/enrollment_token.ts b/test/interactive_setup_functional/tests/enrollment_token.ts index 20d9da65396929..5af4ed0fc3f2e0 100644 --- a/test/interactive_setup_functional/tests/enrollment_token.ts +++ b/test/interactive_setup_functional/tests/enrollment_token.ts @@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) { const log = getService('log'); describe('Interactive Setup Functional Tests (Enrollment token)', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); const elasticsearchConfig = config.get('servers.elasticsearch'); let verificationCode: string; diff --git a/test/interactive_setup_functional/tests/manual_configuration.ts b/test/interactive_setup_functional/tests/manual_configuration.ts index 68c5068acd267a..3f41cf0659567d 100644 --- a/test/interactive_setup_functional/tests/manual_configuration.ts +++ b/test/interactive_setup_functional/tests/manual_configuration.ts @@ -19,7 +19,7 @@ export default function ({ getService }: FtrProviderContext) { const log = getService('log'); describe('Interactive Setup Functional Tests (Manual configuration)', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); let verificationCode: string; before(async function () { diff --git a/test/interactive_setup_functional/tests/manual_configuration_without_security.ts b/test/interactive_setup_functional/tests/manual_configuration_without_security.ts index 20f7bac890da40..f8925e4add1065 100644 --- a/test/interactive_setup_functional/tests/manual_configuration_without_security.ts +++ b/test/interactive_setup_functional/tests/manual_configuration_without_security.ts @@ -19,7 +19,7 @@ export default function ({ getService, getPageObject }: FtrProviderContext) { const log = getService('log'); describe('Interactive Setup Functional Tests (Manual configuration without Security)', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); let verificationCode: string; before(async function () { diff --git a/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts b/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts index 04e31b4ae8454f..23595150d55a1d 100644 --- a/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts +++ b/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts @@ -19,7 +19,7 @@ export default function ({ getService }: FtrProviderContext) { const log = getService('log'); describe('Interactive Setup Functional Tests (Manual configuration without TLS)', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); let verificationCode: string; before(async function () { diff --git a/test/interpreter_functional/config.ts b/test/interpreter_functional/config.ts index 3f9c846a514294..0f81089433b337 100644 --- a/test/interpreter_functional/config.ts +++ b/test/interpreter_functional/config.ts @@ -11,7 +11,7 @@ import fs from 'fs'; import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(path.resolve(__dirname, 'plugins')); diff --git a/test/new_visualize_flow/config.ts b/test/new_visualize_flow/config.ts index a6bd97464e2d06..381d316fa1ad0d 100644 --- a/test/new_visualize_flow/config.ts +++ b/test/new_visualize_flow/config.ts @@ -9,7 +9,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const commonConfig = await readConfigFile(require.resolve('../functional/config.js')); + const commonConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { testFiles: [require.resolve('./index.ts')], diff --git a/test/new_visualize_flow/index.ts b/test/new_visualize_flow/index.ts index 7cb55069d7d9bd..35c90edf9447d2 100644 --- a/test/new_visualize_flow/index.ts +++ b/test/new_visualize_flow/index.ts @@ -11,7 +11,6 @@ import { FtrProviderContext } from '../functional/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { describe('New Visualize Flow', function () { - this.tags('ciGroup11'); const esArchiver = getService('esArchiver'); before(async () => { await esArchiver.loadIfNeeded( diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index 4a96b2b4028986..b2dbc762ab657c 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -11,7 +11,7 @@ import path from 'path'; import fs from 'fs'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(path.resolve(__dirname, 'plugins')); diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 56264eec24477f..2d87c0575845fb 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -190,7 +190,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.osquery.savedQueries (boolean)', 'xpack.remote_clusters.ui.enabled (boolean)', /** - * NOTE: The Reporting plugin is currently disabled in functional tests (see test/functional/config.js). + * NOTE: The Reporting plugin is currently disabled in functional tests (see test/functional/config.base.js). * It will be re-enabled once #102552 is completed. */ // 'xpack.reporting.roles.allow (array)', diff --git a/test/server_integration/config.js b/test/server_integration/config.base.js similarity index 97% rename from test/server_integration/config.js rename to test/server_integration/config.base.js index 0ebb5c48033b80..71006c258c4234 100644 --- a/test/server_integration/config.js +++ b/test/server_integration/config.base.js @@ -14,7 +14,7 @@ import { export default async function ({ readConfigFile }) { const commonConfig = await readConfigFile(require.resolve('../common/config')); - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { services: { diff --git a/test/server_integration/http/platform/config.status.ts b/test/server_integration/http/platform/config.status.ts index 20ffc917f82444..8be9b24a568177 100644 --- a/test/server_integration/http/platform/config.status.ts +++ b/test/server_integration/http/platform/config.status.ts @@ -20,7 +20,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; */ // eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const httpConfig = await readConfigFile(require.resolve('../../config')); + const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); // Find all folders in __fixtures__/plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(path.resolve(__dirname, '../../__fixtures__/plugins')); @@ -52,6 +52,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...httpConfig.get('kbnTestServer.runOptions'), // Don't wait for Kibana to be completely ready so that we can test the status timeouts wait: /Kibana is now unavailable/, + alwaysUseSource: true, }, }, }; diff --git a/test/server_integration/http/platform/config.ts b/test/server_integration/http/platform/config.ts index f3cdf426e9cbbe..028ff67b43022f 100644 --- a/test/server_integration/http/platform/config.ts +++ b/test/server_integration/http/platform/config.ts @@ -10,7 +10,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; // eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const httpConfig = await readConfigFile(require.resolve('../../config')); + const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); return { testFiles: [require.resolve('./cache'), require.resolve('./headers')], diff --git a/test/server_integration/http/ssl/config.js b/test/server_integration/http/ssl/config.js index 260ba7424d676c..14d9a27a00f443 100644 --- a/test/server_integration/http/ssl/config.js +++ b/test/server_integration/http/ssl/config.js @@ -11,7 +11,7 @@ import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { createKibanaSupertestProvider } from '../../services'; export default async function ({ readConfigFile }) { - const httpConfig = await readConfigFile(require.resolve('../../config')); + const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); const certificateAuthorities = [readFileSync(CA_CERT_PATH)]; return { diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.js index aed7f235938204..47568b16bf6ba2 100644 --- a/test/server_integration/http/ssl_redirect/config.js +++ b/test/server_integration/http/ssl_redirect/config.js @@ -13,7 +13,7 @@ import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { createKibanaSupertestProvider } from '../../services'; export default async function ({ readConfigFile }) { - const httpConfig = await readConfigFile(require.resolve('../../config')); + const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); const certificateAuthorities = [readFileSync(CA_CERT_PATH)]; const redirectPort = httpConfig.get('servers.kibana.port') + 1234; diff --git a/test/server_integration/http/ssl_with_p12/config.js b/test/server_integration/http/ssl_with_p12/config.js index 5d2de9e9a9b549..51beb51b20ba4b 100644 --- a/test/server_integration/http/ssl_with_p12/config.js +++ b/test/server_integration/http/ssl_with_p12/config.js @@ -11,7 +11,7 @@ import { CA_CERT_PATH, KBN_P12_PATH, KBN_P12_PASSWORD } from '@kbn/dev-utils'; import { createKibanaSupertestProvider } from '../../services'; export default async function ({ readConfigFile }) { - const httpConfig = await readConfigFile(require.resolve('../../config')); + const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); const certificateAuthorities = [readFileSync(CA_CERT_PATH)]; return { diff --git a/test/server_integration/http/ssl_with_p12_intermediate/config.js b/test/server_integration/http/ssl_with_p12_intermediate/config.js index ffcb9da0fa047d..a6eb6c82485d99 100644 --- a/test/server_integration/http/ssl_with_p12_intermediate/config.js +++ b/test/server_integration/http/ssl_with_p12_intermediate/config.js @@ -11,7 +11,7 @@ import { CA1_CERT_PATH, CA2_CERT_PATH, EE_P12_PATH, EE_P12_PASSWORD } from '../. import { createKibanaSupertestProvider } from '../../services'; export default async function ({ readConfigFile }) { - const httpConfig = await readConfigFile(require.resolve('../../config')); + const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); const certificateAuthorities = [readFileSync(CA1_CERT_PATH), readFileSync(CA2_CERT_PATH)]; return { diff --git a/test/ui_capabilities/newsfeed_err/config.ts b/test/ui_capabilities/newsfeed_err/config.ts index e9548b41b67a00..4b6eb99a5627c8 100644 --- a/test/ui_capabilities/newsfeed_err/config.ts +++ b/test/ui_capabilities/newsfeed_err/config.ts @@ -7,22 +7,20 @@ */ import { FtrConfigProviderContext } from '@kbn/test'; -// @ts-ignore untyped module -import getFunctionalConfig from '../../functional/config'; // eslint-disable-next-line import/no-default-export export default async ({ readConfigFile }: FtrConfigProviderContext) => { - const functionalConfig = await getFunctionalConfig({ readConfigFile }); + const baseConfig = await readConfigFile(require.resolve('../../functional/config.base.js')); return { - ...functionalConfig, + ...baseConfig.getAll(), testFiles: [require.resolve('./test')], kbnTestServer: { - ...functionalConfig.kbnTestServer, + ...baseConfig.get('kbnTestServer'), serverArgs: [ - ...functionalConfig.kbnTestServer.serverArgs, + ...baseConfig.get('kbnTestServer.serverArgs'), `--newsfeed.service.pathTemplate=/api/_newsfeed-FTS-external-service-simulators/kibana/crash.json`, ], }, diff --git a/test/ui_capabilities/newsfeed_err/test.ts b/test/ui_capabilities/newsfeed_err/test.ts index 52c1c0644299a1..538d790ac57243 100644 --- a/test/ui_capabilities/newsfeed_err/test.ts +++ b/test/ui_capabilities/newsfeed_err/test.ts @@ -15,8 +15,6 @@ export default function uiCapabilitiesTests({ getService, getPageObjects }: FtrP const PageObjects = getPageObjects(['common', 'newsfeed']); describe('Newsfeed icon button handle errors', function () { - this.tags('ciGroup5'); - before(async () => { await PageObjects.newsfeed.resetPage(); }); diff --git a/test/visual_regression/config.ts b/test/visual_regression/config.ts index 3c11a4c2689bab..294848246e7c85 100644 --- a/test/visual_regression/config.ts +++ b/test/visual_regression/config.ts @@ -10,7 +10,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { ...functionalConfig.getAll(), diff --git a/test/visual_regression/tests/discover/index.ts b/test/visual_regression/tests/discover/index.ts index fe634c02400a44..9142a430f963ba 100644 --- a/test/visual_regression/tests/discover/index.ts +++ b/test/visual_regression/tests/discover/index.ts @@ -16,8 +16,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); describe('discover app', function () { - this.tags('ciGroup5'); - before(function () { return browser.setWindowSize(SCREEN_WIDTH, 1000); }); diff --git a/test/visual_regression/tests/vega/index.ts b/test/visual_regression/tests/vega/index.ts index 71f22a3058d91a..9ab4e199439a4d 100644 --- a/test/visual_regression/tests/vega/index.ts +++ b/test/visual_regression/tests/vega/index.ts @@ -16,8 +16,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); describe('vega app', function () { - this.tags('ciGroup5'); - before(function () { return browser.setWindowSize(SCREEN_WIDTH, 1000); }); diff --git a/x-pack/README.md b/x-pack/README.md index d104dffff3d284..dad469295ca0f2 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -20,7 +20,7 @@ For information on testing, see [the Elastic functional test development guide]( #### Running functional tests -The functional UI tests, the API integration tests, and the SAML API integration tests are all run against a live browser, Kibana, and Elasticsearch install. Each set of tests is specified with a unique config that describes how to start the Elasticsearch server, the Kibana server, and what tests to run against them. The sets of tests that exist today are *functional UI tests* ([specified by this config](test/functional/config.js)), *API integration tests* ([specified by this config](test/api_integration/config.ts)), and *SAML API integration tests* ([specified by this config](test/security_api_integration/saml.config.ts)). +The functional UI tests, the API integration tests, and the SAML API integration tests are all run against a live browser, Kibana, and Elasticsearch install. Each set of tests is specified with a unique config that describes how to start the Elasticsearch server, the Kibana server, and what tests to run against them. The sets of tests that exist today are *functional UI tests* ([specified by this config](test/functional/config.base.js)), *API integration tests* ([specified by this config](test/api_integration/config.ts)), and *SAML API integration tests* ([specified by this config](test/security_api_integration/saml.config.ts)). The script runs all sets of tests sequentially like so: * builds Elasticsearch and X-Pack diff --git a/x-pack/plugins/apm/dev_docs/testing.md b/x-pack/plugins/apm/dev_docs/testing.md index 6c35979add784d..e1819875f58d80 100644 --- a/x-pack/plugins/apm/dev_docs/testing.md +++ b/x-pack/plugins/apm/dev_docs/testing.md @@ -96,13 +96,13 @@ TODO: We could try moving this tests to the new e2e tests located at `x-pack/plu **Start server** ``` -node scripts/functional_tests_server --config x-pack/test/functional/config.js +node scripts/functional_tests_server --config x-pack/test/functional/config.base.js ``` **Run tests** ``` -node scripts/functional_test_runner --config x-pack/test/functional/config.js --grep='APM specs' +node scripts/functional_test_runner --config x-pack/test/functional/config.base.js --grep='APM specs' ``` APM tests are located in `x-pack/test/functional/apps/apm`. diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts b/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts index 383a6fa68cf8a3..2d9b530ed76a9e 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts @@ -19,7 +19,7 @@ const NODE_TLS_REJECT_UNAUTHORIZED = '1'; export const esArchiverLoad = (archiveName: string) => { const archivePath = path.join(ES_ARCHIVE_DIR, archiveName); execSync( - `node ../../../../scripts/es_archiver load "${archivePath}" --config ../../../test/functional/config.js`, + `node ../../../../scripts/es_archiver load "${archivePath}" --config ../../../test/functional/config.base.js`, { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; @@ -27,14 +27,14 @@ export const esArchiverLoad = (archiveName: string) => { export const esArchiverUnload = (archiveName: string) => { const archivePath = path.join(ES_ARCHIVE_DIR, archiveName); execSync( - `node ../../../../scripts/es_archiver unload "${archivePath}" --config ../../../test/functional/config.js`, + `node ../../../../scripts/es_archiver unload "${archivePath}" --config ../../../test/functional/config.base.js`, { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; export const esArchiverResetKibana = () => { execSync( - `node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.js`, + `node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.base.js`, { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; diff --git a/x-pack/plugins/apm/ftr_e2e/ftr_config.ts b/x-pack/plugins/apm/ftr_e2e/ftr_config.ts index ec2e8d05a97dd1..dd2166508d1349 100644 --- a/x-pack/plugins/apm/ftr_e2e/ftr_config.ts +++ b/x-pack/plugins/apm/ftr_e2e/ftr_config.ts @@ -13,7 +13,7 @@ async function config({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); return { diff --git a/x-pack/plugins/fleet/cypress/README.md b/x-pack/plugins/fleet/cypress/README.md index e9bb299ca905e5..94de6b38c47ec4 100644 --- a/x-pack/plugins/fleet/cypress/README.md +++ b/x-pack/plugins/fleet/cypress/README.md @@ -113,13 +113,13 @@ We use es_archiver to manage the data that our Cypress tests need. 3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/fleet` ```sh -node ../../../scripts/es_archiver save --dir ../../test/fleet_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@: +node ../../../scripts/es_archiver save --dir ../../test/fleet_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http://:@: ``` Example: ```sh -node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/fleet_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220 +node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/fleet_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http://elastic:changeme@localhost:9220 ``` Note that the command will create the folder if it does not exist. diff --git a/x-pack/plugins/graph/README.md b/x-pack/plugins/graph/README.md index a0d7eb25ff9879..b17f114a8f01f7 100644 --- a/x-pack/plugins/graph/README.md +++ b/x-pack/plugins/graph/README.md @@ -10,8 +10,8 @@ Graph shows only up in the side bar if your server is running on a platinum or t * Run type check `node scripts/type_check.js --project=x-pack/tsconfig.json` * Run linter `node scripts/eslint.js x-pack/plugins/graph` * Run functional tests (make sure to stop dev server) - * Server `cd x-pack && node ./scripts/functional_tests_server.js` - * Tests `cd x-pack && node ../scripts/functional_test_runner.js --config ./test/functional/config.js --grep=graph` + * Server `node ./scripts/functional_tests_server.js --config x-pack/test/functional/apps/graph/config.ts` + * Tests `node scripts/functional_test_runner.js --config x-pack/test/functional/apps/graph/config.ts` ## Folder structure diff --git a/x-pack/plugins/lens/readme.md b/x-pack/plugins/lens/readme.md index c85005c09754ef..84cea6feead066 100644 --- a/x-pack/plugins/lens/readme.md +++ b/x-pack/plugins/lens/readme.md @@ -14,8 +14,9 @@ Run all tests from the `x-pack` root directory - Unit tests: `yarn test:jest x-pack/plugins/lens` - Functional tests: - Run `node scripts/functional_tests_server` - - Run `node ../scripts/functional_test_runner.js --config ./test/functional/config.js --grep="lens app"` - - You may want to comment out all imports except for Lens in the config file. + - Run `node ../scripts/functional_test_runner.js --config ./test/functional/apps/lens/group1/config.ts` + - Run `node ../scripts/functional_test_runner.js --config ./test/functional/apps/lens/group2/config.ts` + - Run `node ../scripts/functional_test_runner.js --config ./test/functional/apps/lens/group3/config.ts` - API Functional tests: - Run `node scripts/functional_tests_server` - Run `node ../scripts/functional_test_runner.js --config ./test/api_integration/config.ts --grep=Lens` diff --git a/x-pack/plugins/maps/README.md b/x-pack/plugins/maps/README.md index 729cba26f72ab8..de683af8ac627e 100644 --- a/x-pack/plugins/maps/README.md +++ b/x-pack/plugins/maps/README.md @@ -10,4 +10,7 @@ Run all tests from the `x-pack` root directory - Unit tests: `yarn test:jest x-pack/plugins/maps --watch` - Functional tests: - Run `node scripts/functional_tests_server` - - Run `node ../scripts/functional_test_runner.js --config ./test/functional/config.js --grep="maps app"` \ No newline at end of file + - Run `node ../scripts/functional_test_runner.js --config ./test/functional/apps/maps/group1/config.ts` + - Run `node ../scripts/functional_test_runner.js --config ./test/functional/apps/maps/group2/config.ts` + - Run `node ../scripts/functional_test_runner.js --config ./test/functional/apps/maps/group3/config.ts` + - Run `node ../scripts/functional_test_runner.js --config ./test/functional/apps/maps/group4/config.ts` \ No newline at end of file diff --git a/x-pack/plugins/osquery/cypress/README.md b/x-pack/plugins/osquery/cypress/README.md index 72d558b6e59802..6d72c6aa82b9da 100644 --- a/x-pack/plugins/osquery/cypress/README.md +++ b/x-pack/plugins/osquery/cypress/README.md @@ -101,13 +101,13 @@ We use es_archiver to manage the data that our Cypress tests need. 3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/osquery` ```sh -node ../../../scripts/es_archiver save --dir ../../test/osquery_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@: +node ../../../scripts/es_archiver save --dir ../../test/osquery_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http://:@: ``` Example: ```sh -node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/osquery_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220 +node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/osquery_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http://elastic:changeme@localhost:9220 ``` Note that the command will create the folder if it does not exist. diff --git a/x-pack/plugins/security_solution/cypress/README.md b/x-pack/plugins/security_solution/cypress/README.md index 69a8019146ae6b..e0430ea332e997 100644 --- a/x-pack/plugins/security_solution/cypress/README.md +++ b/x-pack/plugins/security_solution/cypress/README.md @@ -203,7 +203,7 @@ yarn kbn bootstrap # load auditbeat data needed for test execution (which FTR normally does for us) cd x-pack/plugins/security_solution -node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http(s)://:@ --kibana-url http(s)://:@ +node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http(s)://:@ --kibana-url http(s)://:@ # launch the cypress test runner with overridden environment variables cd x-pack/plugins/security_solution @@ -221,7 +221,7 @@ yarn kbn bootstrap # load auditbeat data needed for test execution (which FTR normally does for us) cd x-pack/plugins/security_solution -node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http(s)://:@ --kibana-url http(s)://:@ +node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http(s)://:@ --kibana-url http(s)://:@ # launch the cypress test runner with overridden environment variables cd x-pack/plugins/security_solution @@ -344,13 +344,13 @@ We use es_archiver to manage the data that our Cypress tests need. 3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/security_solution` ```sh -node ../../../scripts/es_archiver save --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@: +node ../../../scripts/es_archiver save --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http://:@: ``` Example: ```sh -node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220 +node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http://elastic:changeme@localhost:9220 ``` Note that the command will create the folder if it does not exist. diff --git a/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts b/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts index 83ec1536baf0f5..588a1e94cf407e 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts @@ -8,7 +8,7 @@ import Path from 'path'; const ES_ARCHIVE_DIR = '../../test/security_solution_cypress/es_archives'; -const CONFIG_PATH = '../../test/functional/config.js'; +const CONFIG_PATH = '../../test/functional/config.base.js'; const ES_URL = Cypress.env('ELASTICSEARCH_URL'); const KIBANA_URL = Cypress.config().baseUrl; const CCS_ES_URL = Cypress.env('CCS_ELASTICSEARCH_URL'); diff --git a/x-pack/plugins/synthetics/e2e/config.ts b/x-pack/plugins/synthetics/e2e/config.ts index e8af8510fe5dc5..42e97eb21e90a5 100644 --- a/x-pack/plugins/synthetics/e2e/config.ts +++ b/x-pack/plugins/synthetics/e2e/config.ts @@ -19,7 +19,7 @@ async function config({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); const kibanaConfig = readKibanaConfig(); diff --git a/x-pack/plugins/synthetics/e2e/tasks/es_archiver.ts b/x-pack/plugins/synthetics/e2e/tasks/es_archiver.ts index dac5672bdf649a..bbb66b19f5a5ea 100644 --- a/x-pack/plugins/synthetics/e2e/tasks/es_archiver.ts +++ b/x-pack/plugins/synthetics/e2e/tasks/es_archiver.ts @@ -16,7 +16,7 @@ const NODE_TLS_REJECT_UNAUTHORIZED = '1'; export const esArchiverLoad = (folder: string) => { const path = Path.join(ES_ARCHIVE_DIR, folder); execSync( - `node ../../../../scripts/es_archiver load "${path}" --config ../../../test/functional/config.js`, + `node ../../../../scripts/es_archiver load "${path}" --config ../../../test/functional/config.base.js`, { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; @@ -24,14 +24,14 @@ export const esArchiverLoad = (folder: string) => { export const esArchiverUnload = (folder: string) => { const path = Path.join(ES_ARCHIVE_DIR, folder); execSync( - `node ../../../../scripts/es_archiver unload "${path}" --config ../../../test/functional/config.js`, + `node ../../../../scripts/es_archiver unload "${path}" --config ../../../test/functional/config.base.js`, { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; export const esArchiverResetKibana = () => { execSync( - `node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.js`, + `node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.base.js`, { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js deleted file mode 100644 index ee99785aa8fad4..00000000000000 --- a/x-pack/scripts/functional_tests.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -require('../../src/setup_node_env'); -require('@kbn/test').runTestsCli([ - require.resolve('../test/functional/config.ccs.ts'), - require.resolve('../test/functional/config.js'), - require.resolve('../test/functional_basic/config.ts'), - require.resolve('../test/security_solution_endpoint/config.ts'), - require.resolve('../test/plugin_functional/config.ts'), - require.resolve('../test/functional_with_es_ssl/config.ts'), - require.resolve('../test/functional/config_security_basic.ts'), - require.resolve('../test/reporting_functional/reporting_and_security.config.ts'), - require.resolve('../test/reporting_functional/reporting_without_security.config.ts'), - require.resolve('../test/reporting_functional/reporting_and_deprecated_security.config.ts'), - require.resolve('../test/security_functional/login_selector.config.ts'), - require.resolve('../test/security_functional/oidc.config.ts'), - require.resolve('../test/security_functional/saml.config.ts'), - require.resolve('../test/functional_embedded/config.ts'), - require.resolve('../test/functional_cors/config.ts'), - require.resolve('../test/functional_enterprise_search/without_host_configured.config.ts'), - require.resolve('../test/saved_object_tagging/functional/config.ts'), - require.resolve('../test/usage_collection/config.ts'), - require.resolve('../test/fleet_functional/config.ts'), - require.resolve('../test/functional_synthetics/config.js'), - require.resolve('../test/api_integration/config_security_basic.ts'), - require.resolve('../test/api_integration/config_security_trial.ts'), - require.resolve('../test/api_integration/config.ts'), - require.resolve('../test/api_integration_basic/config.ts'), - require.resolve('../test/alerting_api_integration/basic/config.ts'), - require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), - require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), - require.resolve('../test/cases_api_integration/security_and_spaces/config_basic.ts'), - require.resolve('../test/cases_api_integration/security_and_spaces/config_trial.ts'), - require.resolve('../test/cases_api_integration/spaces_only/config.ts'), - require.resolve('../test/apm_api_integration/basic/config.ts'), - require.resolve('../test/apm_api_integration/trial/config.ts'), - require.resolve('../test/apm_api_integration/rules/config.ts'), - require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'), - require.resolve('../test/detection_engine_api_integration/basic/config.ts'), - require.resolve('../test/lists_api_integration/security_and_spaces/config.ts'), - require.resolve('../test/plugin_api_integration/config.ts'), - require.resolve('../test/rule_registry/security_and_spaces/config_basic.ts'), - require.resolve('../test/rule_registry/security_and_spaces/config_trial.ts'), - require.resolve('../test/rule_registry/spaces_only/config_basic.ts'), - require.resolve('../test/rule_registry/spaces_only/config_trial.ts'), - require.resolve('../test/security_api_integration/saml.config.ts'), - require.resolve('../test/security_api_integration/session_idle.config.ts'), - require.resolve('../test/security_api_integration/session_invalidate.config.ts'), - require.resolve('../test/security_api_integration/session_lifespan.config.ts'), - require.resolve('../test/security_api_integration/login_selector.config.ts'), - require.resolve('../test/security_api_integration/audit.config.ts'), - require.resolve('../test/security_api_integration/http_bearer.config.ts'), - require.resolve('../test/security_api_integration/http_no_auth_providers.config.ts'), - require.resolve('../test/security_api_integration/kerberos.config.ts'), - require.resolve('../test/security_api_integration/kerberos_anonymous_access.config.ts'), - require.resolve('../test/security_api_integration/pki.config.ts'), - require.resolve('../test/security_api_integration/oidc.config.ts'), - require.resolve('../test/security_api_integration/oidc_implicit_flow.config.ts'), - require.resolve('../test/security_api_integration/token.config.ts'), - require.resolve('../test/security_api_integration/anonymous.config.ts'), - require.resolve('../test/security_api_integration/anonymous_es_anonymous.config.ts'), - require.resolve('../test/observability_api_integration/basic/config.ts'), - require.resolve('../test/observability_api_integration/trial/config.ts'), - require.resolve('../test/observability_functional/with_rac_write.config.ts'), - require.resolve('../test/encrypted_saved_objects_api_integration/config.ts'), - require.resolve('../test/spaces_api_integration/spaces_only/config.ts'), - require.resolve('../test/spaces_api_integration/security_and_spaces/config_trial.ts'), - require.resolve('../test/spaces_api_integration/security_and_spaces/config_basic.ts'), - require.resolve('../test/saved_object_api_integration/security_and_spaces/config_trial.ts'), - require.resolve('../test/saved_object_api_integration/security_and_spaces/config_basic.ts'), - require.resolve('../test/saved_object_api_integration/spaces_only/config.ts'), - // TODO: Enable once RBAC timeline search strategy - // tests updated - // require.resolve('../test/timeline/security_and_spaces/config_basic.ts'), - require.resolve('../test/timeline/security_and_spaces/config_trial.ts'), - require.resolve('../test/ui_capabilities/security_and_spaces/config.ts'), - require.resolve('../test/ui_capabilities/spaces_only/config.ts'), - require.resolve('../test/upgrade_assistant_integration/config.js'), - require.resolve('../test/licensing_plugin/config.ts'), - require.resolve('../test/licensing_plugin/config.public.ts'), - require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'), - require.resolve('../test/reporting_api_integration/reporting_and_security.config.ts'), - require.resolve('../test/reporting_api_integration/reporting_without_security.config.ts'), - require.resolve('../test/security_solution_endpoint_api_int/config.ts'), - require.resolve('../test/fleet_api_integration/config.ts'), - require.resolve('../test/search_sessions_integration/config.ts'), - require.resolve('../test/saved_object_tagging/api_integration/security_and_spaces/config.ts'), - require.resolve('../test/saved_object_tagging/api_integration/tagging_api/config.ts'), - require.resolve('../test/examples/config.ts'), - require.resolve('../test/functional_execution_context/config.ts'), -]); diff --git a/x-pack/scripts/functional_tests_server.js b/x-pack/scripts/functional_tests_server.js index 946f7ea3836a61..329fea019221b5 100755 --- a/x-pack/scripts/functional_tests_server.js +++ b/x-pack/scripts/functional_tests_server.js @@ -8,4 +8,4 @@ process.env.ALLOW_PERFORMANCE_HOOKS_IN_TASK_MANAGER = true; require('../../src/setup_node_env'); -require('@kbn/test').startServersCli(require.resolve('../test/functional/config.js')); +require('@kbn/test').startServersCli(require.resolve('../test/functional/config.base.js')); diff --git a/x-pack/test/accessibility/apps/advanced_settings.ts b/x-pack/test/accessibility/apps/advanced_settings.ts index 6f2dc78a7b35b8..6c931f0a0e5a1f 100644 --- a/x-pack/test/accessibility/apps/advanced_settings.ts +++ b/x-pack/test/accessibility/apps/advanced_settings.ts @@ -13,7 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); - describe('Stack Management -Advanced Settings', () => { + describe('Stack Management -Advanced Settings Accessibility', () => { // click on Management > Advanced settings it('click on advanced settings ', async () => { await PageObjects.common.navigateToUrl('management', 'kibana/settings', { diff --git a/x-pack/test/accessibility/apps/canvas.ts b/x-pack/test/accessibility/apps/canvas.ts index 609c8bf5bb1ae4..d9508e75bdf276 100644 --- a/x-pack/test/accessibility/apps/canvas.ts +++ b/x-pack/test/accessibility/apps/canvas.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const { common } = getPageObjects(['common']); - describe('Canvas', () => { + describe('Canvas Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/canvas/default'); await common.navigateToApp('canvas'); diff --git a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts index 5624a5f25db2f6..20b72e142f5c76 100644 --- a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts +++ b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts @@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PANEL_TITLE = 'Visualization PieChart'; - describe('Dashboard Edit Panel', () => { + describe('Dashboard Edit Panel Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/drilldowns'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); diff --git a/x-pack/test/accessibility/apps/enterprise_search.ts b/x-pack/test/accessibility/apps/enterprise_search.ts index aa6910842b5ebe..0a1a5d68d9621d 100644 --- a/x-pack/test/accessibility/apps/enterprise_search.ts +++ b/x-pack/test/accessibility/apps/enterprise_search.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const { common } = getPageObjects(['common']); - describe('Enterprise Search', () => { + describe('Enterprise Search Accessibility', () => { // NOTE: These accessibility tests currently only run against Enterprise Search in Kibana // without a sidecar Enterprise Search service/host configured, and as such only test // the basic setup guides and not the full application(s) diff --git a/x-pack/test/accessibility/apps/grok_debugger.ts b/x-pack/test/accessibility/apps/grok_debugger.ts index ecb62ffd53177d..4f40696bb0eb6a 100644 --- a/x-pack/test/accessibility/apps/grok_debugger.ts +++ b/x-pack/test/accessibility/apps/grok_debugger.ts @@ -13,7 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const grokDebugger = getService('grokDebugger'); // this test is failing as there is a violation https://github.com/elastic/kibana/issues/62102 - describe.skip('Dev tools grok debugger', () => { + describe.skip('Dev tools grok debugger Accessibility', () => { before(async () => { await PageObjects.common.navigateToApp('grokDebugger'); await grokDebugger.assertExists(); diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts index 61297859c29f8f..544a32843f7f36 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/home.ts @@ -13,7 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); - describe('Kibana Home', () => { + describe('Kibana Home Accessibility', () => { before(async () => { await common.navigateToApp('home'); }); diff --git a/x-pack/test/accessibility/apps/index_lifecycle_management.ts b/x-pack/test/accessibility/apps/index_lifecycle_management.ts index 6cec8d1cb891ab..fc3ec1ff5cf818 100644 --- a/x-pack/test/accessibility/apps/index_lifecycle_management.ts +++ b/x-pack/test/accessibility/apps/index_lifecycle_management.ts @@ -63,14 +63,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { throw new Error(`Could not find ${policyName} in policy table`); }; - describe('Index Lifecycle Management', async () => { + describe('Index Lifecycle Management Accessibility', async () => { before(async () => { await esClient.snapshot.createRepository({ name: REPO_NAME, body: { type: 'fs', settings: { - // use one of the values defined in path.repo in test/functional/config.js + // use one of the values defined in path.repo in test/functional/config.base.js location: '/tmp/', }, }, diff --git a/x-pack/test/accessibility/apps/ingest_node_pipelines.ts b/x-pack/test/accessibility/apps/ingest_node_pipelines.ts index a09a07bb012677..4bbd9cde06d2d5 100644 --- a/x-pack/test/accessibility/apps/ingest_node_pipelines.ts +++ b/x-pack/test/accessibility/apps/ingest_node_pipelines.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: any) { const log = getService('log'); const a11y = getService('a11y'); /* this is the wrapping service around axe */ - describe('Ingest Pipelines', async () => { + describe('Ingest Pipelines Accessibility', async () => { before(async () => { await putSamplePipeline(esClient); await common.navigateToApp('ingestPipelines'); diff --git a/x-pack/test/accessibility/apps/kibana_overview.ts b/x-pack/test/accessibility/apps/kibana_overview.ts index 9d21f08a900cca..19af9c2828d35c 100644 --- a/x-pack/test/accessibility/apps/kibana_overview.ts +++ b/x-pack/test/accessibility/apps/kibana_overview.ts @@ -11,7 +11,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'home']); const a11y = getService('a11y'); - describe('Kibana overview', () => { + describe('Kibana overview Accessibility', () => { const esArchiver = getService('esArchiver'); before(async () => { diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/lens.ts index 8a46d662a61cf0..18459b56c05422 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/lens.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); const kibanaServer = getService('kibanaServer'); - describe('Lens', () => { + describe('Lens Accessibility', () => { const lensChartName = 'MyLensChart'; before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional'); diff --git a/x-pack/test/accessibility/apps/license_management.ts b/x-pack/test/accessibility/apps/license_management.ts index 891a682e653ba4..7693ebb197ff1a 100644 --- a/x-pack/test/accessibility/apps/license_management.ts +++ b/x-pack/test/accessibility/apps/license_management.ts @@ -12,7 +12,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const testSubjects = getService('testSubjects'); - describe('License Management page a11y tests', () => { + describe('License Management page Accessibility', () => { before(async () => { await PageObjects.common.navigateToApp('licenseManagement'); }); diff --git a/x-pack/test/accessibility/apps/login_page.ts b/x-pack/test/accessibility/apps/login_page.ts index 154517d09502ee..6463e63fb2e49c 100644 --- a/x-pack/test/accessibility/apps/login_page.ts +++ b/x-pack/test/accessibility/apps/login_page.ts @@ -14,8 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'security']); - // Failing: See https://github.com/elastic/kibana/issues/96372 - describe('Security', () => { + describe('Security Accessibility', () => { describe('Login Page', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); diff --git a/x-pack/test/accessibility/apps/maps.ts b/x-pack/test/accessibility/apps/maps.ts index c5b824c330829d..0e4142e6ade60a 100644 --- a/x-pack/test/accessibility/apps/maps.ts +++ b/x-pack/test/accessibility/apps/maps.ts @@ -13,7 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'settings', 'header', 'home', 'maps']); - describe('Maps app meets ally validations', () => { + describe('Maps app Accessibility', () => { before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index fd05d2af07747c..a783310d017067 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -5,15 +5,13 @@ * 2.0. */ -import path from 'path'; - import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); const ml = getService('ml'); - describe('ml', () => { + describe('ml Accessibility', () => { const esArchiver = getService('esArchiver'); before(async () => { @@ -79,16 +77,8 @@ export default function ({ getService }: FtrProviderContext) { const dfaJobType = 'outlier_detection'; const dfaJobId = `ihp_ally_${Date.now()}`; - const uploadFilePath = path.join( - __dirname, - '..', - '..', - 'functional', - 'apps', - 'ml', - 'data_visualizer', - 'files_to_import', - 'artificial_server_log' + const uploadFilePath = require.resolve( + '../../functional/apps/ml/data_visualizer/files_to_import/artificial_server_log' ); before(async () => { diff --git a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts b/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts index 9532e8e365655c..8f8bc67304c0d9 100644 --- a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts +++ b/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'timePicker', 'dashboard']); const a11y = getService('a11y'); /* this is the wrapping service around axe */ - describe('machine learning embeddables anomaly charts', function () { + describe('machine learning embeddables anomaly charts Accessibility', function () { before(async () => { await ml.securityCommon.createMlRoles(); await ml.securityCommon.createMlUsers(); diff --git a/x-pack/test/accessibility/apps/painless_lab.ts b/x-pack/test/accessibility/apps/painless_lab.ts index c25930941b5eb8..a0a4712dbe4e38 100644 --- a/x-pack/test/accessibility/apps/painless_lab.ts +++ b/x-pack/test/accessibility/apps/painless_lab.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const retry = getService('retry'); - describe('Accessibility Painless Lab Editor', () => { + describe('Accessibility Painless Lab Editor Accessibility', () => { before(async () => { await PageObjects.common.navigateToApp('painlessLab'); }); diff --git a/x-pack/test/accessibility/apps/remote_clusters.ts b/x-pack/test/accessibility/apps/remote_clusters.ts index 67c85eda60a4a7..deb0e4a090b8c9 100644 --- a/x-pack/test/accessibility/apps/remote_clusters.ts +++ b/x-pack/test/accessibility/apps/remote_clusters.ts @@ -79,7 +79,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const retry = getService('retry'); - describe('Remote Clusters', () => { + describe('Remote Clusters Accessibility', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('remoteClusters'); }); diff --git a/x-pack/test/accessibility/apps/reporting.ts b/x-pack/test/accessibility/apps/reporting.ts index c6a6571cc0ff6a..f1ac0770c95876 100644 --- a/x-pack/test/accessibility/apps/reporting.ts +++ b/x-pack/test/accessibility/apps/reporting.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const security = getService('security'); const log = getService('log'); - describe('Reporting', () => { + describe('Reporting Accessibility', () => { const createReportingUser = async () => { await security.user.create(reporting.REPORTING_USER_USERNAME, { password: reporting.REPORTING_USER_PASSWORD, diff --git a/x-pack/test/accessibility/apps/roles.ts b/x-pack/test/accessibility/apps/roles.ts index 3c40e664d7da2d..5369dced427fad 100644 --- a/x-pack/test/accessibility/apps/roles.ts +++ b/x-pack/test/accessibility/apps/roles.ts @@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); - describe('Kibana roles page a11y tests', () => { + describe('Kibana roles page Accessibility', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.uiSettings.update({ diff --git a/x-pack/test/accessibility/apps/search_profiler.ts b/x-pack/test/accessibility/apps/search_profiler.ts index 47909662fb1327..30043f8f4157fa 100644 --- a/x-pack/test/accessibility/apps/search_profiler.ts +++ b/x-pack/test/accessibility/apps/search_profiler.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const esArchiver = getService('esArchiver'); - describe('Accessibility Search Profiler Editor', () => { + describe('Search Profiler Editor Accessibility', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await PageObjects.common.navigateToApp('searchProfiler'); diff --git a/x-pack/test/accessibility/apps/search_sessions.ts b/x-pack/test/accessibility/apps/search_sessions.ts index 30bef9086a4b69..42a2f387612ac8 100644 --- a/x-pack/test/accessibility/apps/search_sessions.ts +++ b/x-pack/test/accessibility/apps/search_sessions.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); const esArchiver = getService('esArchiver'); - describe('Search sessions a11y tests', () => { + describe('Search sessions Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/data/search_sessions'); await PageObjects.searchSessionsManagement.goTo(); diff --git a/x-pack/test/accessibility/apps/security_solution.ts b/x-pack/test/accessibility/apps/security_solution.ts index 8014e03152b17f..ef930f093eb4a1 100644 --- a/x-pack/test/accessibility/apps/security_solution.ts +++ b/x-pack/test/accessibility/apps/security_solution.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); // FLAKY: https://github.com/elastic/kibana/issues/95707 - describe.skip('Security Solution', () => { + describe.skip('Security Solution Accessibility', () => { before(async () => { await security.testUser.setRoles(['superuser'], { skipBrowserRefresh: true }); await common.navigateToApp('security'); diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/spaces.ts index 567f958f5f8a49..38b34054911f61 100644 --- a/x-pack/test/accessibility/apps/spaces.ts +++ b/x-pack/test/accessibility/apps/spaces.ts @@ -18,7 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const toasts = getService('toasts'); - describe('Kibana spaces page meets a11y validations', () => { + describe('Kibana Spaces Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); await PageObjects.common.navigateToApp('home'); diff --git a/x-pack/test/accessibility/apps/tags.ts b/x-pack/test/accessibility/apps/tags.ts index 8174c8fa8c06b4..da51f2f0535e2a 100644 --- a/x-pack/test/accessibility/apps/tags.ts +++ b/x-pack/test/accessibility/apps/tags.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const toasts = getService('toasts'); - describe('Kibana tags page meets a11y validations', () => { + describe('Kibana Tags Page Accessibility', () => { before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, diff --git a/x-pack/test/accessibility/apps/transform.ts b/x-pack/test/accessibility/apps/transform.ts index 59f19471490b89..fa54ea4ad67667 100644 --- a/x-pack/test/accessibility/apps/transform.ts +++ b/x-pack/test/accessibility/apps/transform.ts @@ -11,7 +11,7 @@ export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); const transform = getService('transform'); - describe('transform', () => { + describe('transform Accessibility', () => { const esArchiver = getService('esArchiver'); before(async () => { diff --git a/x-pack/test/accessibility/apps/upgrade_assistant.ts b/x-pack/test/accessibility/apps/upgrade_assistant.ts index 1f7fd2a654bcad..fffb6e684ba4af 100644 --- a/x-pack/test/accessibility/apps/upgrade_assistant.ts +++ b/x-pack/test/accessibility/apps/upgrade_assistant.ts @@ -53,7 +53,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const es = getService('es'); const log = getService('log'); - describe.skip('Upgrade Assistant', () => { + describe.skip('Upgrade Assistant Accessibility', () => { before(async () => { await PageObjects.upgradeAssistant.navigateToPage(); diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index 41664c5920b825..49243c37fe730b 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/uptime.ts @@ -19,7 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const es = getService('es'); const toasts = getService('toasts'); - describe('uptime', () => { + describe('uptime Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { diff --git a/x-pack/test/accessibility/apps/users.ts b/x-pack/test/accessibility/apps/users.ts index 8682cc8f0a884f..5833a19580c24d 100644 --- a/x-pack/test/accessibility/apps/users.ts +++ b/x-pack/test/accessibility/apps/users.ts @@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); const retry = getService('retry'); - describe('Kibana users page a11y tests', () => { + describe('Kibana users Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); await PageObjects.security.clickElasticsearchUsers(); diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index e85b8a9ef17d81..30c130df23a15a 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -10,7 +10,7 @@ import { services } from './services'; import { pageObjects } from './page_objects'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { ...functionalConfig.getAll(), diff --git a/x-pack/test/alerting_api_integration/basic/tests/index.ts b/x-pack/test/alerting_api_integration/basic/tests/index.ts index 6ddb09b1c666e4..ba6ebbe6a944e3 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/index.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/index.ts @@ -13,8 +13,6 @@ export default function alertingApiIntegrationTests({ getService, }: FtrProviderContext) { describe('alerting api integration basic license', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./actions')); loadTestFile(require.resolve('./alerts')); }); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 14039ad3360a0e..ffdf0c09ad2166 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -25,6 +25,7 @@ interface CreateTestConfigOptions { customizeLocalHostSsl?: boolean; rejectUnauthorized?: boolean; // legacy emailDomainsAllowed?: string[]; + testFiles?: string[]; } // test.not-enabled is specifically not enabled @@ -64,6 +65,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) customizeLocalHostSsl = false, rejectUnauthorized = true, // legacy emailDomainsAllowed = undefined, + testFiles = undefined, } = options; return async ({ readConfigFile }: FtrConfigProviderContext) => { @@ -139,7 +141,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) : []; return { - testFiles: [require.resolve(`../${name}/tests/`)], + testFiles: testFiles ? testFiles : [require.resolve(`../${name}/tests/`)], servers, services, junit: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/config.ts similarity index 82% rename from x-pack/test/alerting_api_integration/security_and_spaces/config.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/config.ts index 314f65c167048a..9b90a4c18fdf0d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/config.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createTestConfig } from '../common/config'; +import { createTestConfig } from '../../common/config'; // eslint-disable-next-line import/no-default-export export default createTestConfig('security_and_spaces', { @@ -14,4 +14,5 @@ export default createTestConfig('security_and_spaces', { ssl: true, enableActionsProxy: true, publicBaseUrl: true, + testFiles: [require.resolve('./tests')], }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts index 57ba6e38635769..e601c6ee15ec73 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios } from '../../../scenarios'; import { checkAAD, getTestRuleData, @@ -15,8 +15,8 @@ import { ObjectRemover, getProducerUnauthorizedErrorMessage, TaskManagerDoc, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/delete.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/delete.ts index 56c50f035b10e4..2b6086cf38d926 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/delete.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios } from '../../../scenarios'; import { getUrlPrefix, getTestRuleData, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, ObjectRemover, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createDeleteTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts index 8a4266eb8dc8a5..864de743ea3437 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createDisableAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts index 6d667eff24072f..0aba468174cffd 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -17,7 +17,7 @@ import { getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, TaskManagerDoc, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createEnableAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/execution_status.ts similarity index 95% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/execution_status.ts index 4e61fd65931136..122353f444536a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/execution_status.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; import { RuleExecutionStatusErrorReasons } from '@kbn/alerting-plugin/common'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function executionStatusAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts index 84f0d7709d01af..20a5e82d303fe7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts @@ -9,9 +9,9 @@ import expect from '@kbn/expect'; import { SuperTest, Test } from 'supertest'; import { chunk, omit } from 'lodash'; import uuid from 'uuid'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; const findTestUtils = ( describeType: 'internal' | 'public', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts index 180a3cf36e27fd..48559aa35ac3ce 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts @@ -7,15 +7,15 @@ import expect from '@kbn/expect'; import { SuperTest, Test } from 'supertest'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios } from '../../../scenarios'; import { getUrlPrefix, getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; const getTestUtils = ( describeType: 'internal' | 'public', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_state.ts similarity index 97% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_state.ts index 3bdfe49464fcfe..e3da329c1cbafa 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_state.ts @@ -12,9 +12,9 @@ import { getTestRuleData, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { UserAtSpaceScenarios } from '../../scenarios'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; // eslint-disable-next-line import/no-default-export export default function createGetAlertStateTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts similarity index 97% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts index eb4e592a91d8af..f0ba9dc4519375 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts @@ -13,9 +13,9 @@ import { getTestRuleData, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { UserAtSpaceScenarios } from '../../scenarios'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; // eslint-disable-next-line import/no-default-export export default function createGetAlertSummaryTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts new file mode 100644 index 00000000000000..6753b6383872db --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { setupSpacesAndUsers, tearDown } from '../../../setup'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Alerts - Group 1', () => { + describe('alerts', () => { + before(async () => { + await setupSpacesAndUsers(getService); + }); + + after(async () => { + await tearDown(getService); + }); + + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./disable')); + loadTestFile(require.resolve('./enable')); + loadTestFile(require.resolve('./execution_status')); + loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./get_alert_state')); + loadTestFile(require.resolve('./get_alert_summary')); + loadTestFile(require.resolve('./rule_types')); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/rule_types.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/rule_types.ts index 0c527ac1449f84..e9d8175f9066e8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/rule_types.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; import { omit } from 'lodash'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix } from '../../../common/lib/space_test_utils'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix } from '../../../../common/lib/space_test_utils'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function listAlertTypes({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/index.ts new file mode 100644 index 00000000000000..795af26627dfb9 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('alerting api integration security and spaces enabled', function () { + describe('', function () { + loadTestFile(require.resolve('./alerting')); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts new file mode 100644 index 00000000000000..9b90a4c18fdf0d --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + disabledPlugins: [], + license: 'trial', + ssl: true, + enableActionsProxy: true, + publicBaseUrl: true, + testFiles: [require.resolve('./tests')], +}); 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/group2/tests/actions/builtin_action_types/email.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/email.ts index 6fb2315956b690..4d9282d4fdeea2 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/group2/tests/actions/builtin_action_types/email.ts @@ -7,11 +7,11 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { ExternalServiceSimulator, getExternalServiceSimulatorPath, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function emailTest({ getService }: FtrProviderContext) { 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/group2/tests/actions/builtin_action_types/es_index.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/es_index.ts index edf352936e979a..fed3acba1147ef 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/group2/tests/actions/builtin_action_types/es_index.ts @@ -7,7 +7,7 @@ import type { Client } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; const ES_TEST_INDEX_NAME = 'functional-test-actions-index'; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/es_index_preconfigured.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/es_index_preconfigured.ts index caa7d576880371..a447921ac50418 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/es_index_preconfigured.ts @@ -7,7 +7,7 @@ import type { Client } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; // from: x-pack/test/alerting_api_integration/common/config.ts const ACTION_ID = 'preconfigured-es-index-action'; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/jira.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/jira.ts index 33185a20c9249b..5a6e0967736a25 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/jira.ts @@ -8,13 +8,13 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function jiraTest({ getService }: FtrProviderContext) { 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/group2/tests/actions/builtin_action_types/pagerduty.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/pagerduty.ts index 05dba492361978..731eab7dad0f35 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/group2/tests/actions/builtin_action_types/pagerduty.ts @@ -8,13 +8,13 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function pagerdutyTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/resilient.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/resilient.ts index e2a92701b62cb3..fec22ab72e1ef9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/resilient.ts @@ -8,13 +8,13 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function resilientTest({ getService }: FtrProviderContext) { 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/group2/tests/actions/builtin_action_types/server_log.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/server_log.ts index fb7bac7d81e9c2..b8b12d6aac764c 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/group2/tests/actions/builtin_action_types/server_log.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function serverLogTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itom.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_itom.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itom.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_itom.ts index c685fff8abfc67..0771e4e2937260 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itom.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_itom.ts @@ -11,9 +11,9 @@ import { asyncForEach } from '@kbn/std'; import getPort from 'get-port'; import http from 'http'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getServiceNowServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { getServiceNowServer } from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function serviceNowITOMTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_itsm.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_itsm.ts index 0f81753bbc7310..368e1b104a87eb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_itsm.ts @@ -11,9 +11,9 @@ import { asyncForEach } from '@kbn/std'; import getPort from 'get-port'; import http from 'http'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getServiceNowServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { getServiceNowServer } from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function serviceNowITSMTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_sir.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_sir.ts index 0f5640f7edd3ef..f08ca542e4617f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_sir.ts @@ -11,9 +11,9 @@ import { asyncForEach } from '@kbn/std'; import getPort from 'get-port'; import http from 'http'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getServiceNowServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { getServiceNowServer } from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function serviceNowSIRTest({ getService }: FtrProviderContext) { 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/group2/tests/actions/builtin_action_types/slack.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/slack.ts index 66b988fb9b4eb0..a05c622d8a1cfe 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/group2/tests/actions/builtin_action_types/slack.ts @@ -9,10 +9,10 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; import http from 'http'; import getPort from 'get-port'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getSlackServer } from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/swimlane.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/swimlane.ts index a55e8e30d419ad..4119a409d7a4bc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/swimlane.ts @@ -10,9 +10,9 @@ import expect from '@kbn/expect'; import getPort from 'get-port'; import http from 'http'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getSwimlaneServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { getSwimlaneServer } from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function swimlaneTest({ getService }: FtrProviderContext) { 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/group2/tests/actions/builtin_action_types/webhook.ts similarity index 97% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/webhook.ts index 44a74a7a315718..c484dfad69539d 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/group2/tests/actions/builtin_action_types/webhook.ts @@ -10,13 +10,13 @@ import http from 'http'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; import getPort from 'get-port'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, getWebhookServer, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; const defaultValues: Record = { headers: null, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/xmatters.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/xmatters.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/xmatters.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/xmatters.ts index 7ce357bc62e367..e33597057cfe1d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/xmatters.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/xmatters.ts @@ -8,13 +8,13 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function xmattersTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts new file mode 100644 index 00000000000000..506d0016af4f64 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../../../../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + disabledPlugins: [], + license: 'trial', + ssl: true, + enableActionsProxy: true, + publicBaseUrl: true, + testFiles: [require.resolve('.')], +}); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types.ts similarity index 91% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types.ts index ec237198809261..feacbaa48be422 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix } from '../../../common/lib/space_test_utils'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix } from '../../../../common/lib/space_test_utils'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function listActionTypesTests({ getService }: FtrProviderContext) { 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/group2/tests/actions/create.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts index 06e80171771380..15d43f9782d94c 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/group2/tests/actions/create.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createActionTests({ getService }: FtrProviderContext) { 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/group2/tests/actions/delete.ts similarity index 97% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts index a0aae0a1bd64de..b0cfffdd2f464a 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/group2/tests/actions/delete.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } 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 deleteActionTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts index c8433ac60575f9..35361920cc729f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts @@ -7,15 +7,15 @@ import expect from '@kbn/expect'; import { IValidatedEvent, nanosToMillis } from '@kbn/event-log-plugin/server'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios } from '../../../scenarios'; import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix, ObjectRemover, getEventLog, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { 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/group2/tests/actions/get.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get.ts index 9842b13a9745d2..f58481bb944125 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/group2/tests/actions/get.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } 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 getActionTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts index ab6181369755fd..103ae5abd30714 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, 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) { 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/group2/tests/actions/index.ts similarity index 93% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts index 93d4bbb4065ed2..6d1ecdbee566c8 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/group2/tests/actions/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { setupSpacesAndUsers, tearDown } from '..'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { setupSpacesAndUsers, tearDown } from '../../../setup'; // eslint-disable-next-line import/no-default-export export default function actionsTests({ loadTestFile, getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/manual/pr_40694.js b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/manual/pr_40694.js similarity index 100% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/manual/pr_40694.js rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/manual/pr_40694.js 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/group2/tests/actions/update.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts index 92bb7084ec5343..6a9181d5ba5ddc 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/group2/tests/actions/update.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function updateActionTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts index 407d6467296e44..5dfbdfa9707c27 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts @@ -11,8 +11,8 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IValidatedEvent, nanosToMillis } from '@kbn/event-log-plugin/server'; import { TaskRunning, TaskRunningStage } from '@kbn/task-manager-plugin/server/task_running'; import { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server'; -import { UserAtSpaceScenarios, Superuser } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios, Superuser } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ESTestIndexTool, ES_TEST_INDEX_NAME, @@ -23,7 +23,7 @@ import { getConsumerUnauthorizedErrorMessage, TaskManagerUtils, getEventLog, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts similarity index 92% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts index 4a572002a4366b..424c734b5c6a29 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts @@ -6,10 +6,10 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { validateEvent } from '../../../spaces_only/tests/alerting/event_log'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { validateEvent } from '../../../../spaces_only/tests/alerting/event_log'; // eslint-disable-next-line import/no-default-export export default function eventLogTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/excluded.ts similarity index 94% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/excluded.ts index eae80da85dc599..d09edae045e6f4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/excluded.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios } from '../../../scenarios'; import { getTestRuleData, getUrlPrefix, @@ -14,8 +14,8 @@ import { getEventLog, AlertUtils, ES_TEST_INDEX_NAME, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/health.ts similarity index 97% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/health.ts index d51cf8cc96af90..0663696dbee14c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/health.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getUrlPrefix, getTestRuleData, @@ -15,7 +15,7 @@ import { AlertUtils, ESTestIndexTool, ES_TEST_INDEX_NAME, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createFindTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/index.ts new file mode 100644 index 00000000000000..72890c2bbd90ac --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { setupSpacesAndUsers, tearDown } from '../../../setup'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Alerts', () => { + describe('legacy alerts', function () { + before(async () => { + await setupSpacesAndUsers(getService); + }); + + after(async () => { + await tearDown(getService); + }); + + loadTestFile(require.resolve('./rbac_legacy')); + }); + + describe('alerts', () => { + before(async () => { + await setupSpacesAndUsers(getService); + }); + + after(async () => { + await tearDown(getService); + }); + + loadTestFile(require.resolve('./mute_all')); + loadTestFile(require.resolve('./mute_instance')); + loadTestFile(require.resolve('./unmute_all')); + loadTestFile(require.resolve('./unmute_instance')); + loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./update_api_key')); + loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./event_log')); + loadTestFile(require.resolve('./mustache_templates')); + loadTestFile(require.resolve('./health')); + loadTestFile(require.resolve('./excluded')); + loadTestFile(require.resolve('./snooze')); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mustache_templates.ts similarity index 92% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mustache_templates.ts index 7e3a7599a73e06..2426c154aa4435 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mustache_templates.ts @@ -18,11 +18,11 @@ import axios from 'axios'; import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { getSlackServer } from '../../../common/fixtures/plugins/actions_simulators/server/plugin'; -import { getHttpProxyServer } from '../../../common/lib/get_proxy_server'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; // eslint-disable-next-line import/no-default-export export default function executionStatusAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts index 1cac93cb52b78c..5a4c792463b626 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createMuteAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_instance.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_instance.ts index 3948f910423a9d..63a285e0f4cb85 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_instance.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createMuteAlertInstanceTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts index 4f324cae689cd2..4f0f53383e2063 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts @@ -7,10 +7,10 @@ import expect from '@kbn/expect'; import { SavedObjectsUtils } from '@kbn/core/server/saved_objects'; -import { UserAtSpaceScenarios, Superuser } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { ESTestIndexTool, getUrlPrefix, ObjectRemover, AlertUtils } from '../../../common/lib'; -import { setupSpacesAndUsers } from '..'; +import { UserAtSpaceScenarios, Superuser } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ESTestIndexTool, getUrlPrefix, ObjectRemover, AlertUtils } from '../../../../common/lib'; +import { setupSpacesAndUsers } from '../../../setup'; // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/snooze.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts index 929b95535e1952..553e090498f00e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/snooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; const FUTURE_SNOOZE_TIME = '9999-12-31T06:00:00.000Z'; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts index e97e7e73abe440..dde198f54f7714 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createUnmuteAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_instance.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_instance.ts index 17ee25e822a6da..1aa84f64a7e795 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_instance.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createMuteAlertInstanceTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unsnooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unsnooze.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts index ed37a19d807078..c868654235c21c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unsnooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createUnsnoozeRuleTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts index b2a1ae223f62c9..c49fa62c606b68 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Response as SupertestResponse } from 'supertest'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios } from '../../../scenarios'; import { checkAAD, getUrlPrefix, @@ -16,8 +16,8 @@ import { ensureDatetimeIsWithinRange, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createUpdateTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update_api_key.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update_api_key.ts index 1c25ec550c41ef..a0d1eb4dd0756f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update_api_key.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createUpdateApiKeyTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/index.ts new file mode 100644 index 00000000000000..c4b5ab80c34161 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('alerting api integration security and spaces enabled - Group 2', function () { + loadTestFile(require.resolve('./telemetry')); + loadTestFile(require.resolve('./actions')); + loadTestFile(require.resolve('./alerting')); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/actions_telemetry.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/actions_telemetry.ts index 1d8d0091b67de1..b187b9e9f97597 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/actions_telemetry.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { Spaces, Superuser } from '../../scenarios'; +import { Spaces, Superuser } from '../../../scenarios'; import { getUrlPrefix, getEventLog, getTestRuleData, ObjectRemover, TaskManagerDoc, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createActionsTelemetryTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_telemetry.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_telemetry.ts index afc39bf1c6b74f..811eeecfc03754 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_telemetry.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { Spaces, Superuser } from '../../scenarios'; +import { Spaces, Superuser } from '../../../scenarios'; import { getUrlPrefix, getEventLog, @@ -14,8 +14,8 @@ import { ObjectRemover, TaskManagerDoc, ESTestIndexTool, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAlertingTelemetryTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/config.ts new file mode 100644 index 00000000000000..506d0016af4f64 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../../../../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + disabledPlugins: [], + license: 'trial', + ssl: true, + enableActionsProxy: true, + publicBaseUrl: true, + testFiles: [require.resolve('.')], +}); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/index.ts similarity index 84% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/index.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/index.ts index 9e73fafc9f7bd5..6077997b661ae9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { setupSpacesAndUsers, tearDown } from '..'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { setupSpacesAndUsers, tearDown } from '../../../setup'; // eslint-disable-next-line import/no-default-export export default function actionsTests({ loadTestFile, getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/setup.ts similarity index 73% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/setup.ts index 1196e3716347d4..69ca58e1edc125 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/setup.ts @@ -6,8 +6,8 @@ */ import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { isCustomRoleSpecification } from '../../common/types'; -import { Spaces, Users } from '../scenarios'; +import { isCustomRoleSpecification } from '../common/types'; +import { Spaces, Users } from './scenarios'; export async function setupSpacesAndUsers(getService: FtrProviderContext['getService']) { const securityService = getService('security'); @@ -54,18 +54,3 @@ export async function tearDown(getService: FtrProviderContext['getService']) { await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); } - -// eslint-disable-next-line import/no-default-export -export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { - describe('alerting api integration security and spaces enabled', function () { - describe('', function () { - this.tags('ciGroup17'); - loadTestFile(require.resolve('./telemetry')); - loadTestFile(require.resolve('./actions')); - }); - - describe('', function () { - loadTestFile(require.resolve('./alerting')); - }); - }); -} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts deleted file mode 100644 index 9dd7326aa4663c..00000000000000 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { setupSpacesAndUsers, tearDown } from '..'; - -// eslint-disable-next-line import/no-default-export -export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { - describe('Alerts', () => { - describe('legacy alerts', function () { - this.tags('ciGroup17'); - - before(async () => { - await setupSpacesAndUsers(getService); - }); - - after(async () => { - await tearDown(getService); - }); - - loadTestFile(require.resolve('./rbac_legacy')); - }); - - describe('alerts', () => { - before(async () => { - await setupSpacesAndUsers(getService); - }); - - after(async () => { - await tearDown(getService); - }); - - describe('', function () { - this.tags('ciGroup17'); - loadTestFile(require.resolve('./find')); - loadTestFile(require.resolve('./create')); - loadTestFile(require.resolve('./delete')); - loadTestFile(require.resolve('./disable')); - loadTestFile(require.resolve('./enable')); - loadTestFile(require.resolve('./execution_status')); - loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./get_alert_state')); - loadTestFile(require.resolve('./get_alert_summary')); - loadTestFile(require.resolve('./rule_types')); - }); - - describe('', function () { - this.tags('ciGroup30'); - loadTestFile(require.resolve('./mute_all')); - loadTestFile(require.resolve('./mute_instance')); - loadTestFile(require.resolve('./unmute_all')); - loadTestFile(require.resolve('./unmute_instance')); - loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./update_api_key')); - loadTestFile(require.resolve('./alerts')); - loadTestFile(require.resolve('./event_log')); - loadTestFile(require.resolve('./mustache_templates')); - loadTestFile(require.resolve('./health')); - loadTestFile(require.resolve('./excluded')); - loadTestFile(require.resolve('./snooze')); - }); - }); - }); -} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts index 88e5e0740789f5..424deaaf838156 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts @@ -11,8 +11,6 @@ import { Spaces } from '../scenarios'; // eslint-disable-next-line import/no-default-export export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('alerting api integration spaces only', function () { - this.tags('ciGroup12'); - loadTestFile(require.resolve('./actions')); loadTestFile(require.resolve('./alerting')); loadTestFile(require.resolve('./action_task_params')); diff --git a/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts index bb581a7f61cdb6..757f2793b239f4 100644 --- a/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts @@ -11,8 +11,6 @@ import { Spaces } from '../scenarios'; // eslint-disable-next-line import/no-default-export export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('alerting api integration spaces only legacy configuration', function () { - this.tags('ciGroup12'); - loadTestFile(require.resolve('./actions/builtin_action_types/webhook')); }); } diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index ec964f97922ad1..645cc815606821 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('apis', function () { - this.tags('ciGroup18'); - loadTestFile(require.resolve('./search')); loadTestFile(require.resolve('./es')); loadTestFile(require.resolve('./security')); diff --git a/x-pack/test/api_integration/apis/security/index.ts b/x-pack/test/api_integration/apis/security/index.ts index eb81d8245dbff3..b595894f157fcf 100644 --- a/x-pack/test/api_integration/apis/security/index.ts +++ b/x-pack/test/api_integration/apis/security/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security', function () { - this.tags('ciGroup18'); - // Updates here should be mirrored in `./security_basic.ts` if tests // should also run under a basic license. diff --git a/x-pack/test/api_integration/apis/security/security_basic.ts b/x-pack/test/api_integration/apis/security/security_basic.ts index 3dcb347126a973..1e7d8ed1ad4b28 100644 --- a/x-pack/test/api_integration/apis/security/security_basic.ts +++ b/x-pack/test/api_integration/apis/security/security_basic.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security (basic license)', function () { - this.tags('ciGroup6'); - // Updates here should be mirrored in `./index.js` if tests // should also run under a trial/platinum license. diff --git a/x-pack/test/api_integration/apis/security/security_trial.ts b/x-pack/test/api_integration/apis/security/security_trial.ts index be4ad252d408f6..3786240b4e4333 100644 --- a/x-pack/test/api_integration/apis/security/security_trial.ts +++ b/x-pack/test/api_integration/apis/security/security_trial.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security (trial license)', function () { - this.tags('ciGroup6'); - // THIS TEST NEEDS TO BE LAST. IT IS DESTRUCTIVE! IT REMOVES TRIAL LICENSE!!! loadTestFile(require.resolve('./license_downgrade')); }); diff --git a/x-pack/test/api_integration/apis/spaces/index.ts b/x-pack/test/api_integration/apis/spaces/index.ts index 3ca0040e39ec9a..1ec1faac535cec 100644 --- a/x-pack/test/api_integration/apis/spaces/index.ts +++ b/x-pack/test/api_integration/apis/spaces/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('spaces', function () { - this.tags('ciGroup18'); - loadTestFile(require.resolve('./get_active_space')); loadTestFile(require.resolve('./saved_objects')); loadTestFile(require.resolve('./space_attributes')); diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index 4e47947aa29dcb..8cc5fb6f57d42a 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -10,7 +10,7 @@ import { services } from './services'; export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProviderContext) { const xPackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); return { diff --git a/x-pack/test/api_integration_basic/apis/index.ts b/x-pack/test/api_integration_basic/apis/index.ts index 9490d4c277675b..31176c55ac3ca5 100644 --- a/x-pack/test/api_integration_basic/apis/index.ts +++ b/x-pack/test/api_integration_basic/apis/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('apis', function () { - this.tags('ciGroup11'); - loadTestFile(require.resolve('./transform')); loadTestFile(require.resolve('./security_solution')); }); diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 59764cf021c4a6..e1312f589862c2 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -14,8 +14,6 @@ export default function apmApiIntegrationTests({ getService, loadTestFile }: Ftr const registry = getService('registry'); describe('APM API tests', function () { - this.tags('ciGroup1'); - const tests = glob.sync('**/*.spec.ts', { cwd }); tests.forEach((test) => { describe(test, function () { diff --git a/x-pack/test/banners_functional/config.ts b/x-pack/test/banners_functional/config.ts index 03f91dfbc34e20..83d0c4656572c0 100644 --- a/x-pack/test/banners_functional/config.ts +++ b/x-pack/test/banners_functional/config.ts @@ -9,7 +9,9 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { services, pageObjects } from './ftr_provider_context'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const kibanaFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { testFiles: [require.resolve('./tests')], diff --git a/x-pack/test/banners_functional/tests/index.ts b/x-pack/test/banners_functional/tests/index.ts index 301c872c746e11..8a26cb66ad5697 100644 --- a/x-pack/test/banners_functional/tests/index.ts +++ b/x-pack/test/banners_functional/tests/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('banners - functional tests', function () { - this.tags('ciGroup2'); - loadTestFile(require.resolve('./global')); loadTestFile(require.resolve('./spaces')); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/index.ts index b618cf5b4df682..714d7e0c6b2193 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/index.ts @@ -11,9 +11,6 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases security and spaces enabled: basic', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup27'); - before(async () => { await createSpacesAndUsers(getService); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts index 3c1ee842962700..e768c92d8c8af8 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts @@ -19,23 +19,15 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { await deleteSpacesAndUsers(getService); }); - describe('', function () { - this.tags('ciGroup13'); + // Trial + loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); + loadTestFile(require.resolve('./configure')); - // Trial - loadTestFile(require.resolve('./cases/push_case')); - loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); - loadTestFile(require.resolve('./configure')); - }); - - describe('', function () { - this.tags('ciGroup25'); + // Common + loadTestFile(require.resolve('../common')); - // Common - loadTestFile(require.resolve('../common')); - - // NOTE: These need to be at the end because they could delete the .kibana index and inadvertently remove the users and spaces - loadTestFile(require.resolve('../common/migrations')); - }); + // NOTE: These need to be at the end because they could delete the .kibana index and inadvertently remove the users and spaces + loadTestFile(require.resolve('../common/migrations')); }); }; diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts index 346640aa6b9de1..cbf0f010048efd 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts @@ -11,9 +11,6 @@ import { createSpaces, deleteSpaces } from '../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases spaces only enabled: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpaces(getService); }); diff --git a/x-pack/test/cloud_integration/config.ts b/x-pack/test/cloud_integration/config.ts index 102d276b345841..0074565875845b 100644 --- a/x-pack/test/cloud_integration/config.ts +++ b/x-pack/test/cloud_integration/config.ts @@ -25,7 +25,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); const kibanaPort = kibanaFunctionalConfig.get('servers.kibana.port'); diff --git a/x-pack/test/detection_engine_api_integration/basic/config.ts b/x-pack/test/detection_engine_api_integration/basic/config.ts index 0c5c7d1649f84d..26fdc62e0ec520 100644 --- a/x-pack/test/detection_engine_api_integration/basic/config.ts +++ b/x-pack/test/detection_engine_api_integration/basic/config.ts @@ -8,7 +8,10 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export -export default createTestConfig('basic', { - license: 'basic', - ssl: true, -}); +export default createTestConfig( + { + license: 'basic', + ssl: true, + }, + [require.resolve('./tests')] +); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts index 1a5ea8de935b44..349d33c89fdf36 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('detection engine api basic license', function () { - this.tags('ciGroup1'); - loadTestFile(require.resolve('./add_prepackaged_rules')); loadTestFile(require.resolve('./create_rules')); loadTestFile(require.resolve('./create_rules_bulk')); diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index 96a83d2c3a427a..a95cb937d4cd90 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -31,7 +31,7 @@ const enabledActionTypes = [ 'test.rate-limit', ]; -export function createTestConfig(name: string, options: CreateTestConfigOptions) { +export function createTestConfig(options: CreateTestConfigOptions, testFiles?: string[]) { const { license = 'trial', ssl = false } = options; return async ({ readConfigFile }: FtrConfigProviderContext) => { @@ -47,7 +47,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) }; return { - testFiles: [require.resolve(`../${name}/tests/`)], + testFiles, servers, services, junit: { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/README.md new file mode 100644 index 00000000000000..ca10827803c650 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/README.md @@ -0,0 +1,33 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations + +# Rule Exception List Tests + +These tests are currently in group7-9. + +These are tests for rule exception lists where we test each data type + +- date +- double +- float +- integer +- ip +- keyword +- long +- text + +Against the operator types of: + +- "is" +- "is not" +- "is one of" +- "is not one of" +- "exists" +- "does not exist" +- "is in list" +- "is not in list" diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/config.base.ts similarity index 87% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/config.base.ts index 78203525b887ab..ed72e1faa682c8 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/config.base.ts @@ -8,7 +8,7 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export -export default createTestConfig('security_and_spaces', { +export default createTestConfig({ license: 'trial', ssl: true, }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_actions.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_actions.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_prepackaged_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_prepackaged_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/aliases.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/aliases.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/check_privileges.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/check_privileges.ts diff --git a/x-pack/test/functional_embedded/config.firefox.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/config.ts similarity index 54% rename from x-pack/test/functional_embedded/config.firefox.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/config.ts index 49359d37673dec..2430b8f2148d9a 100644 --- a/x-pack/test/functional_embedded/config.firefox.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/config.ts @@ -7,23 +7,12 @@ import { FtrConfigProviderContext } from '@kbn/test'; +// eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const chromeConfig = await readConfigFile(require.resolve('./config')); + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); return { - ...chromeConfig.getAll(), - - browser: { - type: 'firefox', - acceptInsecureCerts: true, - }, - - suiteTags: { - exclude: ['skipFirefox'], - }, - - junit: { - reportName: 'Firefox Kibana Embedded in iframe with X-Pack Security', - }, + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], }; } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_index.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_index.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_ml.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_ml.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_signals_migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_signals_migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_threat_matching.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_threat_matching.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_signals_migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_signals_migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/finalize_signals_migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/finalize_signals_migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/generating_signals.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/generating_signals.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_prepackaged_rules_status.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_prepackaged_rules_status.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_rule_execution_events.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_rule_execution_events.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_rule_execution_events.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_rule_execution_events.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_signals_migration_status.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_signals_migration_status.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/ignore_fields.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/ignore_fields.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/ignore_fields.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/ignore_fields.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/import_export_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_export_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/import_export_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/import_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/import_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts new file mode 100644 index 00000000000000..f7a96c2f496d82 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 1', function () { + // !!NOTE: For new routes that do any updates on a rule, please ensure that you are including the legacy + // action migration code. We are monitoring legacy action telemetry to clean up once we see their + // existence being near 0. + + loadTestFile(require.resolve('./aliases')); + loadTestFile(require.resolve('./add_actions')); + loadTestFile(require.resolve('./update_actions')); + loadTestFile(require.resolve('./add_prepackaged_rules')); + loadTestFile(require.resolve('./check_privileges')); + loadTestFile(require.resolve('./create_index')); + loadTestFile(require.resolve('./create_rules')); + loadTestFile(require.resolve('./preview_rules')); + loadTestFile(require.resolve('./create_rules_bulk')); + loadTestFile(require.resolve('./create_ml')); + loadTestFile(require.resolve('./create_threat_matching')); + loadTestFile(require.resolve('./delete_rules')); + loadTestFile(require.resolve('./delete_rules_bulk')); + loadTestFile(require.resolve('./export_rules')); + loadTestFile(require.resolve('./find_rules')); + loadTestFile(require.resolve('./generating_signals')); + loadTestFile(require.resolve('./get_prepackaged_rules_status')); + loadTestFile(require.resolve('./get_rule_execution_events')); + loadTestFile(require.resolve('./import_rules')); + loadTestFile(require.resolve('./import_export_rules')); + loadTestFile(require.resolve('./legacy_actions_migrations')); + loadTestFile(require.resolve('./read_rules')); + loadTestFile(require.resolve('./resolve_read_rules')); + loadTestFile(require.resolve('./update_rules')); + loadTestFile(require.resolve('./update_rules_bulk')); + loadTestFile(require.resolve('./patch_rules_bulk')); + loadTestFile(require.resolve('./perform_bulk_action')); + loadTestFile(require.resolve('./patch_rules')); + loadTestFile(require.resolve('./read_privileges')); + loadTestFile(require.resolve('./open_close_signals')); + loadTestFile(require.resolve('./get_signals_migration_status')); + loadTestFile(require.resolve('./create_signals_migrations')); + loadTestFile(require.resolve('./finalize_signals_migrations')); + loadTestFile(require.resolve('./delete_signals_migrations')); + loadTestFile(require.resolve('./timestamps')); + loadTestFile(require.resolve('./runtime')); + loadTestFile(require.resolve('./throttle')); + loadTestFile(require.resolve('./ignore_fields')); + loadTestFile(require.resolve('./migrations')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/legacy_actions_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/legacy_actions_migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/legacy_actions_migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/legacy_actions_migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/open_close_signals.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/open_close_signals.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/patch_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/patch_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/patch_rules_bulk.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/patch_rules_bulk.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/preview_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/preview_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/preview_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/preview_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_privileges.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/read_privileges.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_privileges.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/read_privileges.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/read_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/read_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/resolve_read_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/resolve_read_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/runtime.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/runtime.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/runtime.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/runtime.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/template_data/execution_events.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/template_data/execution_events.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/template_data/execution_events.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/template_data/execution_events.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/throttle.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/throttle.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/throttle.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/throttle.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/timestamps.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/timestamps.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/timestamps.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/timestamps.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_actions.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_actions.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules_bulk.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules_bulk.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/config.ts new file mode 100644 index 00000000000000..2430b8f2148d9a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_endpoint_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/create_endpoint_exceptions.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_endpoint_exceptions.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group2/create_endpoint_exceptions.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/index.ts new file mode 100644 index 00000000000000..f477b23e801f36 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 2', function () { + loadTestFile(require.resolve('./create_endpoint_exceptions')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/config.ts new file mode 100644 index 00000000000000..2430b8f2148d9a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/index.ts new file mode 100644 index 00000000000000..10d4e9c2d6635c --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 3', function () { + loadTestFile(require.resolve('./create_exceptions')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/config.ts new file mode 100644 index 00000000000000..2430b8f2148d9a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/index.ts new file mode 100644 index 00000000000000..9394e81017aba2 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 4', function () { + loadTestFile(require.resolve('./telemetry')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/README.md similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/README.md diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/index.ts similarity index 51% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/index.ts index d2050179abd0ef..4e180f73dc7424 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/index.ts @@ -10,15 +10,12 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('Detection rule type telemetry', function () { - describe('', function () { - this.tags('ciGroup28'); - loadTestFile(require.resolve('./usage_collector/all_types')); - loadTestFile(require.resolve('./usage_collector/detection_rules')); - loadTestFile(require.resolve('./usage_collector/detection_rule_status')); + loadTestFile(require.resolve('./usage_collector/all_types')); + loadTestFile(require.resolve('./usage_collector/detection_rules')); + loadTestFile(require.resolve('./usage_collector/detection_rule_status')); - loadTestFile(require.resolve('./task_based/all_types')); - loadTestFile(require.resolve('./task_based/detection_rules')); - loadTestFile(require.resolve('./task_based/security_lists')); - }); + loadTestFile(require.resolve('./task_based/all_types')); + loadTestFile(require.resolve('./task_based/detection_rules')); + loadTestFile(require.resolve('./task_based/security_lists')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/all_types.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/all_types.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/all_types.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/all_types.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/detection_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/detection_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/detection_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/security_lists.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/security_lists.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/security_lists.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/security_lists.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/all_types.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/all_types.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/all_types.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/all_types.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rule_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rule_status.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rule_status.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rule_status.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/config.ts new file mode 100644 index 00000000000000..2430b8f2148d9a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/index.ts new file mode 100644 index 00000000000000..ac107392d4b5c3 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 5', function () { + loadTestFile(require.resolve('./keyword_family')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/README.md similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/README.md rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/README.md diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/const_keyword.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/const_keyword.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/const_keyword.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/const_keyword.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/index.ts similarity index 68% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/index.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/index.ts index 4855524d650ef4..1ecb06fbed4e51 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/index.ts @@ -10,12 +10,8 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('Detection keyword family data types', function () { - describe('', function () { - this.tags('ciGroup11'); - - loadTestFile(require.resolve('./keyword')); - loadTestFile(require.resolve('./const_keyword')); - loadTestFile(require.resolve('./keyword_mixed_with_const')); - }); + loadTestFile(require.resolve('./keyword')); + loadTestFile(require.resolve('./const_keyword')); + loadTestFile(require.resolve('./keyword_mixed_with_const')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/keyword.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/keyword.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword_mixed_with_const.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/keyword_mixed_with_const.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword_mixed_with_const.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/keyword_mixed_with_const.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/alerts/alerts_compatibility.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/alerts/alerts_compatibility.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/alerts/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/index.ts similarity index 79% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/alerts/index.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/index.ts index b06d6cb26e33b1..320650a4c79e33 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/alerts/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/index.ts @@ -10,10 +10,6 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('Detection engine signals/alerts compatibility', function () { - describe('', function () { - this.tags('ciGroup11'); - - loadTestFile(require.resolve('./alerts_compatibility')); - }); + loadTestFile(require.resolve('./alerts_compatibility')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/config.ts new file mode 100644 index 00000000000000..2430b8f2148d9a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/index.ts new file mode 100644 index 00000000000000..92c235e95c0e4b --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 6', function () { + loadTestFile(require.resolve('./alerts')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/config.ts new file mode 100644 index 00000000000000..2430b8f2148d9a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/date.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/date.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/date.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/date.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/double.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/double.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/float.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/float.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/index.ts new file mode 100644 index 00000000000000..6b2cf915cc51b4 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Detection exceptions data types and operators', function () { + loadTestFile(require.resolve('./date')); + loadTestFile(require.resolve('./double')); + loadTestFile(require.resolve('./float')); + loadTestFile(require.resolve('./integer')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/integer.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/integer.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/index.ts new file mode 100644 index 00000000000000..96f53c47441ed3 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 7', function () { + loadTestFile(require.resolve('./exception_operators_data_types')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/config.ts new file mode 100644 index 00000000000000..2430b8f2148d9a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/index.ts new file mode 100644 index 00000000000000..cf87276aac3ae1 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Detection exceptions data types and operators', function () { + loadTestFile(require.resolve('./keyword')); + loadTestFile(require.resolve('./keyword_array')); + loadTestFile(require.resolve('./long')); + loadTestFile(require.resolve('./text')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/keyword.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/keyword.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/keyword_array.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/keyword_array.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/long.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/long.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/text.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/text.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/index.ts new file mode 100644 index 00000000000000..7182e411a13326 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 8', function () { + loadTestFile(require.resolve('./exception_operators_data_types')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/config.ts new file mode 100644 index 00000000000000..2430b8f2148d9a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/index.ts new file mode 100644 index 00000000000000..22864980e26537 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Detection exceptions data types and operators', function () { + loadTestFile(require.resolve('./text_array')); + loadTestFile(require.resolve('./ip')); + loadTestFile(require.resolve('./ip_array')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/ip.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/ip.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/ip_array.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/ip_array.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/text_array.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/text_array.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/index.ts new file mode 100644 index 00000000000000..ad5e427c69df8a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 9', function () { + loadTestFile(require.resolve('./exception_operators_data_types')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/README.md deleted file mode 100644 index d6beb912d70075..00000000000000 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/README.md +++ /dev/null @@ -1,21 +0,0 @@ -These are tests for rule exception lists where we test each data type -* date -* double -* float -* integer -* ip -* keyword -* long -* text - -Against the operator types of: -* "is" -* "is not" -* "is one of" -* "is not one of" -* "exists" -* "does not exist" -* "is in list" -* "is not in list" - -If you add a test here, ensure you add it to the ./index.ts" file as well \ No newline at end of file diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts deleted file mode 100644 index ded3df8b6716cb..00000000000000 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default ({ loadTestFile }: FtrProviderContext): void => { - describe('Detection exceptions data types and operators', function () { - describe('', function () { - this.tags('ciGroup23'); - - loadTestFile(require.resolve('./date')); - loadTestFile(require.resolve('./double')); - loadTestFile(require.resolve('./float')); - loadTestFile(require.resolve('./integer')); - }); - - describe('', function () { - this.tags('ciGroup24'); - - loadTestFile(require.resolve('./keyword')); - loadTestFile(require.resolve('./keyword_array')); - loadTestFile(require.resolve('./long')); - loadTestFile(require.resolve('./text')); - loadTestFile(require.resolve('./text_array')); - }); - - describe('', function () { - this.tags('ciGroup16'); - - loadTestFile(require.resolve('./ip')); - }); - - describe('', function () { - this.tags('ciGroup21'); - - loadTestFile(require.resolve('./ip_array')); - }); - }); -}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts deleted file mode 100644 index 9a61569ada3b03..00000000000000 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default ({ loadTestFile }: FtrProviderContext): void => { - describe('detection engine api security and spaces enabled', function () { - describe('', function () { - this.tags('ciGroup11'); - - // !!NOTE: For new routes that do any updates on a rule, please ensure that you are including the legacy - // action migration code. We are monitoring legacy action telemetry to clean up once we see their - // existence being near 0. - loadTestFile(require.resolve('./aliases')); - loadTestFile(require.resolve('./add_actions')); - loadTestFile(require.resolve('./update_actions')); - loadTestFile(require.resolve('./add_prepackaged_rules')); - loadTestFile(require.resolve('./check_privileges')); - loadTestFile(require.resolve('./create_index')); - loadTestFile(require.resolve('./create_rules')); - loadTestFile(require.resolve('./preview_rules')); - loadTestFile(require.resolve('./create_rules_bulk')); - loadTestFile(require.resolve('./create_ml')); - loadTestFile(require.resolve('./create_threat_matching')); - loadTestFile(require.resolve('./delete_rules')); - loadTestFile(require.resolve('./delete_rules_bulk')); - loadTestFile(require.resolve('./export_rules')); - loadTestFile(require.resolve('./find_rules')); - loadTestFile(require.resolve('./generating_signals')); - loadTestFile(require.resolve('./get_prepackaged_rules_status')); - loadTestFile(require.resolve('./get_rule_execution_events')); - loadTestFile(require.resolve('./import_rules')); - loadTestFile(require.resolve('./import_export_rules')); - loadTestFile(require.resolve('./legacy_actions_migrations')); - loadTestFile(require.resolve('./read_rules')); - loadTestFile(require.resolve('./resolve_read_rules')); - loadTestFile(require.resolve('./update_rules')); - loadTestFile(require.resolve('./update_rules_bulk')); - loadTestFile(require.resolve('./patch_rules_bulk')); - loadTestFile(require.resolve('./perform_bulk_action')); - loadTestFile(require.resolve('./patch_rules')); - loadTestFile(require.resolve('./read_privileges')); - loadTestFile(require.resolve('./open_close_signals')); - loadTestFile(require.resolve('./get_signals_migration_status')); - loadTestFile(require.resolve('./create_signals_migrations')); - loadTestFile(require.resolve('./finalize_signals_migrations')); - loadTestFile(require.resolve('./delete_signals_migrations')); - loadTestFile(require.resolve('./timestamps')); - loadTestFile(require.resolve('./runtime')); - loadTestFile(require.resolve('./throttle')); - loadTestFile(require.resolve('./ignore_fields')); - loadTestFile(require.resolve('./migrations')); - }); - - describe('', function () { - this.tags('ciGroup26'); - - loadTestFile(require.resolve('./create_endpoint_exceptions')); - }); - - describe('', function () { - this.tags('ciGroup14'); - - loadTestFile(require.resolve('./create_exceptions')); - }); - - // That split here enable us on using a different ciGroup to run the tests - // listed on ./exception_operators_data_types/index - describe('', function () { - loadTestFile(require.resolve('./exception_operators_data_types')); - }); - - // That split here enable us on using a different ciGroup to run the tests - // listed on ./keyword_family/index - describe('', function () { - loadTestFile(require.resolve('./keyword_family')); - }); - - describe('', function () { - loadTestFile(require.resolve('./alerts')); - }); - - describe('', function () { - loadTestFile(require.resolve('./telemetry')); - }); - }); -}; diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts index de87d627ac4860..0313187ac09a7b 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('encryptedSavedObjects', function encryptedSavedObjectsSuite() { - this.tags('ciGroup13'); loadTestFile(require.resolve('./encrypted_saved_objects_api')); loadTestFile(require.resolve('./encrypted_saved_objects_decryption')); }); diff --git a/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts b/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts index 1ca035df18fd58..10a4e18f8c1330 100644 --- a/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts +++ b/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function endpointAPIIntegrationTests({ loadTestFile }: FtrProviderContext) { // Failing ES snapshot promotion: https://github.com/elastic/kibana/issues/70535 describe.skip('Endpoint plugin', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./metadata')); }); } diff --git a/x-pack/test/examples/config.ts b/x-pack/test/examples/config.ts index 6eb1e86dd08268..16db620e765983 100644 --- a/x-pack/test/examples/config.ts +++ b/x-pack/test/examples/config.ts @@ -12,7 +12,9 @@ import fs from 'fs'; import { KIBANA_ROOT } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); // Find all folders in /examples and /x-pack/examples since we treat all them as plugin folder const examplesFiles = fs.readdirSync(resolve(KIBANA_ROOT, 'examples')); diff --git a/x-pack/test/examples/embedded_lens/index.ts b/x-pack/test/examples/embedded_lens/index.ts index debab4773f9eb4..b418e69584a9af 100644 --- a/x-pack/test/examples/embedded_lens/index.ts +++ b/x-pack/test/examples/embedded_lens/index.ts @@ -13,6 +13,8 @@ export default function ({ getService, loadTestFile }: PluginFunctionalProviderC const kibanaServer = getService('kibanaServer'); describe('embedded Lens examples', function () { + this.tags('skipFirefox'); + before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.load( @@ -30,10 +32,6 @@ export default function ({ getService, loadTestFile }: PluginFunctionalProviderC ); }); - describe('', function () { - this.tags(['ciGroup4', 'skipFirefox']); - - loadTestFile(require.resolve('./embedded_example')); - }); + loadTestFile(require.resolve('./embedded_example')); }); } diff --git a/x-pack/test/examples/reporting_examples/index.ts b/x-pack/test/examples/reporting_examples/index.ts index e4e5c93e5eee21..2f1b00597a9d86 100644 --- a/x-pack/test/examples/reporting_examples/index.ts +++ b/x-pack/test/examples/reporting_examples/index.ts @@ -10,8 +10,6 @@ import { PluginFunctionalProviderContext } from '../../../../test/plugin_functio // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile }: PluginFunctionalProviderContext) { describe('reporting examples', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./capture_test')); }); } diff --git a/x-pack/test/examples/screenshotting/index.ts b/x-pack/test/examples/screenshotting/index.ts index e9ecd2ecde346c..c64d84c7fcf3d6 100644 --- a/x-pack/test/examples/screenshotting/index.ts +++ b/x-pack/test/examples/screenshotting/index.ts @@ -22,8 +22,6 @@ export default function ({ // FAILING: https://github.com/elastic/kibana/issues/131190 describe.skip('Screenshotting Example', function () { - this.tags('ciGroup13'); - before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json'); diff --git a/x-pack/test/examples/search_examples/index.ts b/x-pack/test/examples/search_examples/index.ts index c0f8ab22c8f8fb..cee873dfed53a9 100644 --- a/x-pack/test/examples/search_examples/index.ts +++ b/x-pack/test/examples/search_examples/index.ts @@ -13,7 +13,6 @@ export default function ({ getService, loadTestFile }: PluginFunctionalProviderC const kibanaServer = getService('kibanaServer'); describe('search examples', function () { - this.tags('ciGroup13'); before(async () => { await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); diff --git a/x-pack/test/fleet_api_integration/apis/index.js b/x-pack/test/fleet_api_integration/apis/index.js index 9f68eb1c7f81f0..1c528e719e2e82 100644 --- a/x-pack/test/fleet_api_integration/apis/index.js +++ b/x-pack/test/fleet_api_integration/apis/index.js @@ -9,8 +9,6 @@ import { setupTestUsers } from './test_users'; export default function ({ loadTestFile, getService }) { describe('Fleet Endpoints', function () { - this.tags('ciGroup29'); - before(async () => { await setupTestUsers(getService('security')); }); diff --git a/x-pack/test/fleet_cypress/config.ts b/x-pack/test/fleet_cypress/config.ts index d2076fa940412e..52198f4f035e0f 100644 --- a/x-pack/test/fleet_cypress/config.ts +++ b/x-pack/test/fleet_cypress/config.ts @@ -14,7 +14,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); return { diff --git a/x-pack/test/fleet_functional/apps/fleet/agents_page.ts b/x-pack/test/fleet_functional/apps/fleet/agents_page.ts index 515eaa65f5310a..cff1273e0b9013 100644 --- a/x-pack/test/fleet_functional/apps/fleet/agents_page.ts +++ b/x-pack/test/fleet_functional/apps/fleet/agents_page.ts @@ -11,8 +11,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const { agentsPage } = getPageObjects(['agentsPage']); describe('When in the Fleet application', function () { - this.tags(['ciGroup7']); - describe('and on the agents page', () => { before(async () => { await agentsPage.navigateToAgentsPage(); diff --git a/x-pack/test/fleet_functional/apps/fleet/index.ts b/x-pack/test/fleet_functional/apps/fleet/index.ts index ec16e2d8571831..965d4c77761976 100644 --- a/x-pack/test/fleet_functional/apps/fleet/index.ts +++ b/x-pack/test/fleet_functional/apps/fleet/index.ts @@ -11,7 +11,6 @@ export default function (providerContext: FtrProviderContext) { const { loadTestFile } = providerContext; describe('endpoint', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./agents_page')); }); } diff --git a/x-pack/test/fleet_functional/apps/home/index.ts b/x-pack/test/fleet_functional/apps/home/index.ts index cd14bfdff557d0..727213b96349e0 100644 --- a/x-pack/test/fleet_functional/apps/home/index.ts +++ b/x-pack/test/fleet_functional/apps/home/index.ts @@ -11,7 +11,6 @@ export default function (providerContext: FtrProviderContext) { const { loadTestFile } = providerContext; describe('home onboarding', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./welcome')); }); } diff --git a/x-pack/test/fleet_functional/config.ts b/x-pack/test/fleet_functional/config.ts index 60db783280aec9..5efc39b02acd6e 100644 --- a/x-pack/test/fleet_functional/config.ts +++ b/x-pack/test/fleet_functional/config.ts @@ -11,7 +11,9 @@ import { pageObjects } from './page_objects'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { ...xpackFunctionalConfig.getAll(), diff --git a/x-pack/test/functional/apps/advanced_settings/config.ts b/x-pack/test/functional/apps/advanced_settings/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/advanced_settings/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/advanced_settings/index.ts b/x-pack/test/functional/apps/advanced_settings/index.ts index d962f648db395b..f121b031466eae 100644 --- a/x-pack/test/functional/apps/advanced_settings/index.ts +++ b/x-pack/test/functional/apps/advanced_settings/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function advancedSettingsApp({ loadTestFile }: FtrProviderContext) { describe('Advanced Settings', function canvasAppTestSuite() { - this.tags(['ciGroup2', 'skipFirefox']); // CI requires tags ヽ(゜Q。)ノ? + this.tags(['skipFirefox']); // CI requires tags ヽ(゜Q。)ノ? loadTestFile(require.resolve('./feature_controls/advanced_settings_security')); loadTestFile(require.resolve('./feature_controls/advanced_settings_spaces')); }); diff --git a/x-pack/test/functional/apps/api_keys/config.ts b/x-pack/test/functional/apps/api_keys/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/api_keys/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/api_keys/index.ts b/x-pack/test/functional/apps/api_keys/index.ts index 7b9afd201e3d95..2f9d7206d374ae 100644 --- a/x-pack/test/functional/apps/api_keys/index.ts +++ b/x-pack/test/functional/apps/api_keys/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('API Keys app', function () { - this.tags(['ciGroup7']); loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./feature_controls')); }); diff --git a/x-pack/test/functional/apps/apm/config.ts b/x-pack/test/functional/apps/apm/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/apm/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/apm/index.ts b/x-pack/test/functional/apps/apm/index.ts index 20c2bc264a74f1..61aca7ca3f9de9 100644 --- a/x-pack/test/functional/apps/apm/index.ts +++ b/x-pack/test/functional/apps/apm/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('APM specs', function () { - this.tags('ciGroup10'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./correlations')); }); diff --git a/x-pack/test/functional/apps/canvas/config.ts b/x-pack/test/functional/apps/canvas/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/canvas/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/canvas/index.js b/x-pack/test/functional/apps/canvas/index.js index 784aeb66557684..2c6a46b75e5105 100644 --- a/x-pack/test/functional/apps/canvas/index.js +++ b/x-pack/test/functional/apps/canvas/index.js @@ -27,7 +27,6 @@ export default function canvasApp({ loadTestFile, getService }) { await security.testUser.restoreDefaults(); }); - this.tags('ciGroup2'); loadTestFile(require.resolve('./smoke_test')); loadTestFile(require.resolve('./expression')); loadTestFile(require.resolve('./filters')); diff --git a/x-pack/test/functional/apps/cross_cluster_replication/config.ts b/x-pack/test/functional/apps/cross_cluster_replication/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/cross_cluster_replication/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/cross_cluster_replication/index.ts b/x-pack/test/functional/apps/cross_cluster_replication/index.ts index 1ab1ab71833941..5c6539b5e73f7a 100644 --- a/x-pack/test/functional/apps/cross_cluster_replication/index.ts +++ b/x-pack/test/functional/apps/cross_cluster_replication/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Cross Cluster Replication app', function () { - this.tags(['ciGroup4', 'skipCloud']); + this.tags('skipCloud'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); diff --git a/x-pack/test/functional/apps/dashboard/README.md b/x-pack/test/functional/apps/dashboard/README.md new file mode 100644 index 00000000000000..5e87a8b210bdd4 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/README.md @@ -0,0 +1,7 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations \ No newline at end of file diff --git a/x-pack/test/functional/apps/dashboard/group1/config.ts b/x-pack/test/functional/apps/dashboard/group1/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/group1/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts b/x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_dashboard_drilldown.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts rename to x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_dashboard_drilldown.ts index 9fd6beeb8ff22d..c540d5673d89e8 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_dashboard_drilldown.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const DRILLDOWN_TO_PIE_CHART_NAME = 'Go to pie chart dashboard'; const DRILLDOWN_TO_AREA_CHART_NAME = 'Go to area chart dashboard'; diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts b/x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_url_drilldown.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts rename to x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_url_drilldown.ts index 5ed118c9b753a1..ca057b7421b7c6 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_url_drilldown.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const DRILLDOWN_TO_DISCOVER_URL = 'Go to discover'; diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts b/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_chart_action.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts rename to x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_chart_action.ts index b9272a0a6c3bdc..2e2c1f7ecca1d6 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts +++ b/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_chart_action.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const ACTION_ID = 'ACTION_EXPLORE_DATA_CHART'; const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_panel_action.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts rename to x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_panel_action.ts index c78c716364c4bb..c302d9a195397a 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts +++ b/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_panel_action.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const ACTION_ID = 'ACTION_EXPLORE_DATA'; const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/group1/drilldowns/index.ts similarity index 95% rename from x-pack/test/functional/apps/dashboard/drilldowns/index.ts rename to x-pack/test/functional/apps/dashboard/group1/drilldowns/index.ts index fac0c355ce4d0b..eaa1189ab1007b 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts +++ b/x-pack/test/functional/apps/dashboard/group1/drilldowns/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts rename to x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts index efac6d6739fcbe..47a895472d9925 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts @@ -10,7 +10,7 @@ import { createDashboardEditUrl, DashboardConstants, } from '@kbn/dashboard-plugin/public/dashboard_constants'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_spaces.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts rename to x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_spaces.ts index 0d177a0f3c65ba..24a90b883315e7 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_spaces.ts @@ -10,7 +10,7 @@ import { createDashboardEditUrl, DashboardConstants, } from '@kbn/dashboard-plugin/public/dashboard_constants'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/index.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/index.ts similarity index 89% rename from x-pack/test/functional/apps/dashboard/feature_controls/index.ts rename to x-pack/test/functional/apps/dashboard/group1/feature_controls/index.ts index 3b32ea031f6e28..2ea15966f37404 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/index.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts rename to x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts index 9eeb49f5eb0d25..57ddc76835213b 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/functional/apps/dashboard/group1/index.ts b/x-pack/test/functional/apps/dashboard/group1/index.ts new file mode 100644 index 00000000000000..f829002448f334 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/group1/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('dashboard', function () { + loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./preserve_url')); + loadTestFile(require.resolve('./reporting')); + loadTestFile(require.resolve('./drilldowns')); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/preserve_url.ts b/x-pack/test/functional/apps/dashboard/group1/preserve_url.ts similarity index 97% rename from x-pack/test/functional/apps/dashboard/preserve_url.ts rename to x-pack/test/functional/apps/dashboard/group1/preserve_url.ts index e391a8b346f6f7..88ad055d34c20a 100644 --- a/x-pack/test/functional/apps/dashboard/preserve_url.ts +++ b/x-pack/test/functional/apps/dashboard/group1/preserve_url.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/reporting/README.md b/x-pack/test/functional/apps/dashboard/group1/reporting/README.md similarity index 87% rename from x-pack/test/functional/apps/dashboard/reporting/README.md rename to x-pack/test/functional/apps/dashboard/group1/reporting/README.md index 3a2b8f5cc783f9..149b691dc31150 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/README.md +++ b/x-pack/test/functional/apps/dashboard/group1/reporting/README.md @@ -11,8 +11,8 @@ Every now and then visual changes will be made that will require the snapshots t 1. **Load the ES Archive containing the dashboard and data.** This will load the test data into an Elasticsearch instance running via the functional test server: ``` - node scripts/es_archiver load reporting/ecommerce --config=x-pack/test/functional/config.js - node scripts/es_archiver load reporting/ecommerce_kibana --config=x-pack/test/functional/config.js + node scripts/es_archiver load reporting/ecommerce --config=x-pack/test/functional/apps/dashboard/config.ts + node scripts/es_archiver load reporting/ecommerce_kibana --config=x-pack/test/functional/apps/dashboard/config.ts ``` 2. **Generate the reports of the E-commerce dashboard in the Kibana UI.** Navigate to `http://localhost:5620`, find the archived dashboard, and generate all the types of reports for which there are stored baseline images. diff --git a/x-pack/test/functional/apps/dashboard/reporting/__snapshots__/download_csv.snap b/x-pack/test/functional/apps/dashboard/group1/reporting/__snapshots__/download_csv.snap similarity index 99% rename from x-pack/test/functional/apps/dashboard/reporting/__snapshots__/download_csv.snap rename to x-pack/test/functional/apps/dashboard/group1/reporting/__snapshots__/download_csv.snap index d524543183a3f3..e6b31be13861db 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/__snapshots__/download_csv.snap +++ b/x-pack/test/functional/apps/dashboard/group1/reporting/__snapshots__/download_csv.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`dashboard Reporting Download CSV Default Saved Search Data Download CSV export of a saved search panel 1`] = ` +exports[`dashboard Reporting Download CSV Default Saved Search Data Download CSV export of a saved search panel 1`] = ` "\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"products.created_on\\",sku \\"Jun 22, 2019 @ 00:00:00.000\\",\\"Men's Clothing, Men's Shoes\\",EUR,23,564670,6,\\"Dec 11, 2016 @ 00:00:00.000, Dec 11, 2016 @ 00:00:00.000\\",\\"ZO0531205312, ZO0684706847\\" \\"Jun 22, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,564710,6,\\"Dec 11, 2016 @ 00:00:00.000, Dec 11, 2016 @ 00:00:00.000\\",\\"ZO0263402634, ZO0499404994\\" @@ -460,7 +460,7 @@ exports[`dashboard Reporting Download CSV Default Saved Search Data Download CS " `; -exports[`dashboard Reporting Download CSV Default Saved Search Data Downloads a filtered CSV export of a saved search panel 1`] = ` +exports[`dashboard Reporting Download CSV Default Saved Search Data Downloads a filtered CSV export of a saved search panel 1`] = ` "\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"products.created_on\\",sku \\"Jun 22, 2019 @ 00:00:00.000\\",\\"Men's Clothing, Men's Shoes\\",EUR,23,564670,6,\\"Dec 11, 2016 @ 00:00:00.000, Dec 11, 2016 @ 00:00:00.000\\",\\"ZO0531205312, ZO0684706847\\" \\"Jun 22, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing\\",EUR,52,564513,6,\\"Dec 11, 2016 @ 00:00:00.000, Dec 11, 2016 @ 00:00:00.000\\",\\"ZO0390003900, ZO0287902879\\" @@ -562,13 +562,13 @@ exports[`dashboard Reporting Download CSV Default Saved Search Data Downloads a " `; -exports[`dashboard Reporting Download CSV Field Formatters and Scripted Fields Download CSV export of a saved search panel 1`] = ` +exports[`dashboard Reporting Download CSV Field Formatters and Scripted Fields Download CSV export of a saved search panel 1`] = ` "date,\\"_id\\",name,gender,value,year,\\"years_ago\\",\\"date_informal\\" \\"Jan 1, 1982 @ 00:00:00.000\\",\\"1982-Fethany-F\\",Fethany,F,780,1982,\\"37.00000000000000000000\\",\\"Jan 1st 82\\" " `; -exports[`dashboard Reporting Download CSV Filtered Saved Search Downloads filtered Discover saved search report 1`] = ` +exports[`dashboard Reporting Download CSV Filtered Saved Search Downloads filtered Discover saved search report 1`] = ` "\\"order_date\\",category,\\"customer_full_name\\",\\"taxful_total_price\\",currency \\"Jun 25, 2019 @ 00:00:00.000\\",\\"Women's Accessories\\",\\"Betty Reese\\",\\"22.984\\",EUR \\"Jun 25, 2019 @ 00:00:00.000\\",\\"Women's Accessories, Women's Clothing\\",\\"Betty Brewer\\",\\"28.984\\",EUR diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/group1/reporting/download_csv.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/reporting/download_csv.ts rename to x-pack/test/functional/apps/dashboard/group1/reporting/download_csv.ts index 1e2d465fee66b1..bdcf95955b451b 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/group1/reporting/download_csv.ts @@ -9,7 +9,7 @@ import { REPO_ROOT } from '@kbn/utils'; import expect from '@kbn/expect'; import fs from 'fs'; import path from 'path'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/reporting/index.ts b/x-pack/test/functional/apps/dashboard/group1/reporting/index.ts similarity index 86% rename from x-pack/test/functional/apps/dashboard/reporting/index.ts rename to x-pack/test/functional/apps/dashboard/group1/reporting/index.ts index 088e54d534b6a7..ef9821a544b3ad 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/index.ts +++ b/x-pack/test/functional/apps/dashboard/group1/reporting/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Reporting', function () { diff --git a/x-pack/test/functional/apps/dashboard/reporting/reports/baseline/large_dashboard_preserve_layout.png b/x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/large_dashboard_preserve_layout.png similarity index 100% rename from x-pack/test/functional/apps/dashboard/reporting/reports/baseline/large_dashboard_preserve_layout.png rename to x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/large_dashboard_preserve_layout.png diff --git a/x-pack/test/functional/apps/dashboard/reporting/reports/baseline/small_dashboard_preserve_layout.png b/x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/small_dashboard_preserve_layout.png similarity index 100% rename from x-pack/test/functional/apps/dashboard/reporting/reports/baseline/small_dashboard_preserve_layout.png rename to x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/small_dashboard_preserve_layout.png diff --git a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/group1/reporting/screenshots.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/reporting/screenshots.ts rename to x-pack/test/functional/apps/dashboard/group1/reporting/screenshots.ts index d42d23b7578a5c..5a69262801d2ef 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts +++ b/x-pack/test/functional/apps/dashboard/group1/reporting/screenshots.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import path from 'path'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/group2/_async_dashboard.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/_async_dashboard.ts rename to x-pack/test/functional/apps/dashboard/group2/_async_dashboard.ts index 89c58645b64b06..ae79c1893ac8b4 100644 --- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts +++ b/x-pack/test/functional/apps/dashboard/group2/_async_dashboard.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/x-pack/test/functional/apps/dashboard/group2/config.ts b/x-pack/test/functional/apps/dashboard/group2/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/group2/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts rename to x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts index 9e4c2554100b9c..2604b942f73131 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'visualize', 'lens', 'timePicker']); diff --git a/x-pack/test/functional/apps/dashboard/dashboard_maps_by_value.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/dashboard_maps_by_value.ts rename to x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts index e885d1c0d3f73c..c1224b604364d5 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_maps_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/functional/apps/dashboard/dashboard_tagging.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_tagging.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/dashboard_tagging.ts rename to x-pack/test/functional/apps/dashboard/group2/dashboard_tagging.ts index 784d1f36784157..ca0093135b7da8 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_tagging.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_tagging.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); diff --git a/x-pack/test/functional/apps/dashboard/group2/index.ts b/x-pack/test/functional/apps/dashboard/group2/index.ts new file mode 100644 index 00000000000000..666756735e80f5 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/group2/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('dashboard', function () { + loadTestFile(require.resolve('./sync_colors')); + loadTestFile(require.resolve('./_async_dashboard')); + loadTestFile(require.resolve('./dashboard_tagging')); + loadTestFile(require.resolve('./dashboard_lens_by_value')); + loadTestFile(require.resolve('./dashboard_maps_by_value')); + loadTestFile(require.resolve('./panel_titles')); + + loadTestFile(require.resolve('./migration_smoke_tests/lens_migration_smoke_test')); + loadTestFile(require.resolve('./migration_smoke_tests/controls_migration_smoke_test')); + loadTestFile(require.resolve('./migration_smoke_tests/visualize_migration_smoke_test')); + loadTestFile(require.resolve('./migration_smoke_tests/tsvb_migration_smoke_test')); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/controls_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/controls_migration_smoke_test.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/controls_migration_smoke_test.ts rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/controls_migration_smoke_test.ts index b87ee15910d231..20fe8a94137235 100644 --- a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/controls_migration_smoke_test.ts +++ b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/controls_migration_smoke_test.ts @@ -12,7 +12,7 @@ import expect from '@kbn/expect'; import path from 'path'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/controls_dashboard_migration_test_8_0_0.ndjson b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/controls_dashboard_migration_test_8_0_0.ndjson similarity index 100% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/controls_dashboard_migration_test_8_0_0.ndjson rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/controls_dashboard_migration_test_8_0_0.ndjson diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson similarity index 100% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_12_1.ndjson b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_12_1.ndjson similarity index 100% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_12_1.ndjson rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_12_1.ndjson diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_13_3.ndjson b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_13_3.ndjson similarity index 100% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_13_3.ndjson rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_13_3.ndjson diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson similarity index 100% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/lens_migration_smoke_test.ts similarity index 97% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/lens_migration_smoke_test.ts index 78b7ccfe7df082..32c6449ad97b5b 100644 --- a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts +++ b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/lens_migration_smoke_test.ts @@ -11,7 +11,7 @@ import expect from '@kbn/expect'; import path from 'path'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/tsvb_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/tsvb_migration_smoke_test.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/tsvb_migration_smoke_test.ts rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/tsvb_migration_smoke_test.ts index 22606cf6d28620..0c3f9c652a6ec8 100644 --- a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/tsvb_migration_smoke_test.ts +++ b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/tsvb_migration_smoke_test.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import path from 'path'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/visualize_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/visualize_migration_smoke_test.ts similarity index 97% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/visualize_migration_smoke_test.ts rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/visualize_migration_smoke_test.ts index d3d6ca46cd2273..f4e86217a4d758 100644 --- a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/visualize_migration_smoke_test.ts +++ b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/visualize_migration_smoke_test.ts @@ -11,7 +11,7 @@ import expect from '@kbn/expect'; import path from 'path'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/panel_titles.ts b/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/panel_titles.ts rename to x-pack/test/functional/apps/dashboard/group2/panel_titles.ts index 3db72ba8d0a90c..34b7e14d26a1a6 100644 --- a/x-pack/test/functional/apps/dashboard/panel_titles.ts +++ b/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/sync_colors.ts b/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/sync_colors.ts rename to x-pack/test/functional/apps/dashboard/group2/sync_colors.ts index a3628635dfa2ff..093318bb8b5cd7 100644 --- a/x-pack/test/functional/apps/dashboard/sync_colors.ts +++ b/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts @@ -7,7 +7,7 @@ import { DebugState } from '@elastic/charts'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts deleted file mode 100644 index baf37ff56bd804..00000000000000 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('dashboard', function () { - describe('', function () { - this.tags('ciGroup19'); - loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./preserve_url')); - loadTestFile(require.resolve('./reporting')); - loadTestFile(require.resolve('./drilldowns')); - }); - - describe('', function () { - this.tags('ciGroup31'); - loadTestFile(require.resolve('./sync_colors')); - loadTestFile(require.resolve('./_async_dashboard')); - loadTestFile(require.resolve('./dashboard_tagging')); - loadTestFile(require.resolve('./dashboard_lens_by_value')); - loadTestFile(require.resolve('./dashboard_maps_by_value')); - loadTestFile(require.resolve('./panel_titles')); - - loadTestFile(require.resolve('./migration_smoke_tests/lens_migration_smoke_test')); - loadTestFile(require.resolve('./migration_smoke_tests/controls_migration_smoke_test')); - loadTestFile(require.resolve('./migration_smoke_tests/visualize_migration_smoke_test')); - loadTestFile(require.resolve('./migration_smoke_tests/tsvb_migration_smoke_test')); - }); - }); -} diff --git a/x-pack/test/functional/apps/data_views/config.ts b/x-pack/test/functional/apps/data_views/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/data_views/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/data_views/index.ts b/x-pack/test/functional/apps/data_views/index.ts index 3b3f7b36081133..3284dc901c25a6 100644 --- a/x-pack/test/functional/apps/data_views/index.ts +++ b/x-pack/test/functional/apps/data_views/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function advancedSettingsApp({ loadTestFile }: FtrProviderContext) { describe('Data Views', function indexPatternsTestSuite() { - this.tags('ciGroup2'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./spaces')); }); diff --git a/x-pack/test/functional/apps/dev_tools/config.ts b/x-pack/test/functional/apps/dev_tools/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/dev_tools/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/dev_tools/index.ts b/x-pack/test/functional/apps/dev_tools/index.ts index 4f0e9290cc25ea..6ef1688bb4c4ec 100644 --- a/x-pack/test/functional/apps/dev_tools/index.ts +++ b/x-pack/test/functional/apps/dev_tools/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Dev Tools', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./breadcrumbs')); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./searchprofiler_editor')); diff --git a/x-pack/test/functional/apps/discover/config.ts b/x-pack/test/functional/apps/discover/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/discover/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts index 9eda11bc6e6fbd..bb5ac2f8ea9d4a 100644 --- a/x-pack/test/functional/apps/discover/index.ts +++ b/x-pack/test/functional/apps/discover/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('discover', function () { - this.tags('ciGroup25'); - loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); loadTestFile(require.resolve('./async_scripted_fields')); diff --git a/x-pack/test/functional/apps/graph/config.ts b/x-pack/test/functional/apps/graph/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/graph/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/graph/index.ts b/x-pack/test/functional/apps/graph/index.ts index 561cf8e5833d4d..ca0b02e8b0f7da 100644 --- a/x-pack/test/functional/apps/graph/index.ts +++ b/x-pack/test/functional/apps/graph/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('graph app', function () { - this.tags('ciGroup12'); - loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./graph')); }); diff --git a/x-pack/test/functional/apps/grok_debugger/config.ts b/x-pack/test/functional/apps/grok_debugger/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/grok_debugger/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/grok_debugger/index.ts b/x-pack/test/functional/apps/grok_debugger/index.ts index 1fed41f6e3e360..7b0bd70508b6f1 100644 --- a/x-pack/test/functional/apps/grok_debugger/index.ts +++ b/x-pack/test/functional/apps/grok_debugger/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Grok Debugger App', function () { - this.tags('ciGroup13'); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/home/config.ts b/x-pack/test/functional/apps/home/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/home/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/home/index.ts b/x-pack/test/functional/apps/home/index.ts index c7579efff03a4b..fd2c5b3b752c8f 100644 --- a/x-pack/test/functional/apps/home/index.ts +++ b/x-pack/test/functional/apps/home/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Home page', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./feature_controls')); }); }; diff --git a/x-pack/test/functional/apps/index_lifecycle_management/config.ts b/x-pack/test/functional/apps/index_lifecycle_management/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/index_lifecycle_management/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts index d362b84ee479ac..71056c2d836fcb 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts @@ -29,7 +29,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { body: { type: 'fs', settings: { - // use one of the values defined in path.repo in test/functional/config.js + // use one of the values defined in path.repo in test/functional/config.base.js location: '/tmp/', }, }, diff --git a/x-pack/test/functional/apps/index_lifecycle_management/index.ts b/x-pack/test/functional/apps/index_lifecycle_management/index.ts index cf83939c942d9d..38b5803bd77ef7 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/index.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Index Lifecycle Management app', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); diff --git a/x-pack/test/functional/apps/index_management/config.ts b/x-pack/test/functional/apps/index_management/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/index_management/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/index_management/index.ts b/x-pack/test/functional/apps/index_management/index.ts index 81bd94769db62b..83da8cc4bba898 100644 --- a/x-pack/test/functional/apps/index_management/index.ts +++ b/x-pack/test/functional/apps/index_management/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Index Management app', function () { - this.tags('ciGroup13'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); diff --git a/x-pack/test/functional/apps/infra/config.ts b/x-pack/test/functional/apps/infra/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/infra/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index 15f92b8f37fd47..d574c747bf0416 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -9,14 +9,15 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('InfraOps App', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./feature_controls')); + describe('Metrics UI', function () { loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./metrics_source_configuration')); loadTestFile(require.resolve('./metrics_anomalies')); loadTestFile(require.resolve('./metrics_explorer')); }); + describe('Logs UI', function () { loadTestFile(require.resolve('./log_entry_categories_tab')); loadTestFile(require.resolve('./log_entry_rate_tab')); diff --git a/x-pack/test/functional/apps/ingest_pipelines/config.ts b/x-pack/test/functional/apps/ingest_pipelines/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/ingest_pipelines/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/ingest_pipelines/index.ts b/x-pack/test/functional/apps/ingest_pipelines/index.ts index 655fccaf35a953..3c585319cfe13c 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/index.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Ingest pipelines app', function () { - this.tags('ciGroup13'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./ingest_pipelines')); }); diff --git a/x-pack/test/functional/apps/lens/README.md b/x-pack/test/functional/apps/lens/README.md new file mode 100644 index 00000000000000..5e87a8b210bdd4 --- /dev/null +++ b/x-pack/test/functional/apps/lens/README.md @@ -0,0 +1,7 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations \ No newline at end of file diff --git a/x-pack/test/functional/apps/lens/group1/config.ts b/x-pack/test/functional/apps/lens/group1/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/lens/group1/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/group1/index.ts similarity index 55% rename from x-pack/test/functional/apps/lens/index.ts rename to x-pack/test/functional/apps/lens/group1/index.ts index 372d17b473e4d7..35030e3636c965 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/group1/index.ts @@ -6,7 +6,7 @@ */ import { EsArchiver } from '@kbn/es-archiver'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext) => { const browser = getService('browser'); @@ -17,7 +17,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext const config = getService('config'); let remoteEsArchiver; - describe('lens app', () => { + describe('lens app - group 1', () => { const esArchive = 'x-pack/test/functional/es_archives/logstash_functional'; const localIndexPatternString = 'logstash-*'; const remoteIndexPatternString = 'ftr-remote:logstash-*'; @@ -70,54 +70,12 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext }); if (config.get('esTestCluster.ccs')) { - describe('', function () { - this.tags(['ciGroup3', 'skipFirefox']); - loadTestFile(require.resolve('./smokescreen')); - }); + loadTestFile(require.resolve('./smokescreen')); } else { - describe('', function () { - this.tags(['ciGroup3', 'skipFirefox']); - loadTestFile(require.resolve('./smokescreen')); - loadTestFile(require.resolve('./persistent_context')); - }); - - describe('', function () { - this.tags(['ciGroup16', 'skipFirefox']); - - loadTestFile(require.resolve('./add_to_dashboard')); - loadTestFile(require.resolve('./table_dashboard')); - loadTestFile(require.resolve('./table')); - loadTestFile(require.resolve('./runtime_fields')); - loadTestFile(require.resolve('./dashboard')); - loadTestFile(require.resolve('./multi_terms')); - loadTestFile(require.resolve('./epoch_millis')); - loadTestFile(require.resolve('./show_underlying_data')); - loadTestFile(require.resolve('./show_underlying_data_dashboard')); - }); - - describe('', function () { - this.tags(['ciGroup4', 'skipFirefox']); - - loadTestFile(require.resolve('./colors')); - loadTestFile(require.resolve('./chart_data')); - loadTestFile(require.resolve('./time_shift')); - loadTestFile(require.resolve('./drag_and_drop')); - loadTestFile(require.resolve('./disable_auto_apply')); - loadTestFile(require.resolve('./geo_field')); - loadTestFile(require.resolve('./formula')); - loadTestFile(require.resolve('./heatmap')); - loadTestFile(require.resolve('./gauge')); - loadTestFile(require.resolve('./metrics')); - loadTestFile(require.resolve('./reference_lines')); - loadTestFile(require.resolve('./annotations')); - loadTestFile(require.resolve('./inspector')); - loadTestFile(require.resolve('./error_handling')); - loadTestFile(require.resolve('./lens_tagging')); - loadTestFile(require.resolve('./lens_reporting')); - loadTestFile(require.resolve('./tsvb_open_in_lens')); - // has to be last one in the suite because it overrides saved objects - loadTestFile(require.resolve('./rollup')); - }); + loadTestFile(require.resolve('./smokescreen')); + loadTestFile(require.resolve('./persistent_context')); + loadTestFile(require.resolve('./table_dashboard')); + loadTestFile(require.resolve('./table')); } }); }; diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/group1/persistent_context.ts similarity index 99% rename from x-pack/test/functional/apps/lens/persistent_context.ts rename to x-pack/test/functional/apps/lens/group1/persistent_context.ts index 445caa1abbec2a..b6c37f88423297 100644 --- a/x-pack/test/functional/apps/lens/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/group1/persistent_context.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/group1/smokescreen.ts similarity index 99% rename from x-pack/test/functional/apps/lens/smokescreen.ts rename to x-pack/test/functional/apps/lens/group1/smokescreen.ts index d817ee912664a5..70887b337114ff 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/group1/smokescreen.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { range } from 'lodash'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/table.ts b/x-pack/test/functional/apps/lens/group1/table.ts similarity index 99% rename from x-pack/test/functional/apps/lens/table.ts rename to x-pack/test/functional/apps/lens/group1/table.ts index 2070eb047ef614..18ecc2e90cfe4d 100644 --- a/x-pack/test/functional/apps/lens/table.ts +++ b/x-pack/test/functional/apps/lens/group1/table.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/table_dashboard.ts b/x-pack/test/functional/apps/lens/group1/table_dashboard.ts similarity index 97% rename from x-pack/test/functional/apps/lens/table_dashboard.ts rename to x-pack/test/functional/apps/lens/group1/table_dashboard.ts index 6e76d816fa6a6a..136f1903420def 100644 --- a/x-pack/test/functional/apps/lens/table_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group1/table_dashboard.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['lens', 'visualize', 'dashboard']); diff --git a/x-pack/test/functional/apps/lens/add_to_dashboard.ts b/x-pack/test/functional/apps/lens/group2/add_to_dashboard.ts similarity index 99% rename from x-pack/test/functional/apps/lens/add_to_dashboard.ts rename to x-pack/test/functional/apps/lens/group2/add_to_dashboard.ts index 5fbfdd0a5806e8..8ad5cd41e0bece 100644 --- a/x-pack/test/functional/apps/lens/add_to_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/add_to_dashboard.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/functional/apps/lens/group2/config.ts b/x-pack/test/functional/apps/lens/group2/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/lens/group2/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/group2/dashboard.ts similarity index 99% rename from x-pack/test/functional/apps/lens/dashboard.ts rename to x-pack/test/functional/apps/lens/group2/dashboard.ts index 97dc29280761f7..9a8cc99b243150 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/dashboard.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/functional/apps/lens/epoch_millis.ts b/x-pack/test/functional/apps/lens/group2/epoch_millis.ts similarity index 97% rename from x-pack/test/functional/apps/lens/epoch_millis.ts rename to x-pack/test/functional/apps/lens/group2/epoch_millis.ts index d882d69ddd1fdb..ce773baf27ad99 100644 --- a/x-pack/test/functional/apps/lens/epoch_millis.ts +++ b/x-pack/test/functional/apps/lens/group2/epoch_millis.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/group2/index.ts b/x-pack/test/functional/apps/lens/group2/index.ts new file mode 100644 index 00000000000000..0e1c732dff41cb --- /dev/null +++ b/x-pack/test/functional/apps/lens/group2/index.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EsArchiver } from '@kbn/es-archiver'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext) => { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['timePicker']); + const config = getService('config'); + let remoteEsArchiver; + + describe('lens app - group 2', () => { + const esArchive = 'x-pack/test/functional/es_archives/logstash_functional'; + const localIndexPatternString = 'logstash-*'; + const remoteIndexPatternString = 'ftr-remote:logstash-*'; + const localFixtures = { + lensBasic: 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json', + lensDefault: 'x-pack/test/functional/fixtures/kbn_archiver/lens/default', + }; + + const remoteFixtures = { + lensBasic: 'x-pack/test/functional/fixtures/kbn_archiver/lens/ccs/lens_basic.json', + lensDefault: 'x-pack/test/functional/fixtures/kbn_archiver/lens/ccs/default', + }; + let esNode: EsArchiver; + let fixtureDirs: { + lensBasic: string; + lensDefault: string; + }; + let indexPatternString: string; + before(async () => { + await log.debug('Starting lens before method'); + await browser.setWindowSize(1280, 1200); + try { + config.get('esTestCluster.ccs'); + remoteEsArchiver = getService('remoteEsArchiver' as 'esArchiver'); + esNode = remoteEsArchiver; + fixtureDirs = remoteFixtures; + indexPatternString = remoteIndexPatternString; + } catch (error) { + esNode = esArchiver; + fixtureDirs = localFixtures; + indexPatternString = localIndexPatternString; + } + + await esNode.load(esArchive); + // changing the timepicker default here saves us from having to set it in Discover (~8s) + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update({ + defaultIndex: indexPatternString, + 'dateFormat:tz': 'UTC', + }); + await kibanaServer.importExport.load(fixtureDirs.lensBasic); + await kibanaServer.importExport.load(fixtureDirs.lensDefault); + }); + + after(async () => { + await esArchiver.unload(esArchive); + await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.importExport.unload(fixtureDirs.lensBasic); + await kibanaServer.importExport.unload(fixtureDirs.lensDefault); + }); + + loadTestFile(require.resolve('./add_to_dashboard')); + loadTestFile(require.resolve('./runtime_fields')); + loadTestFile(require.resolve('./dashboard')); + loadTestFile(require.resolve('./multi_terms')); + loadTestFile(require.resolve('./epoch_millis')); + loadTestFile(require.resolve('./show_underlying_data')); + loadTestFile(require.resolve('./show_underlying_data_dashboard')); + }); +}; diff --git a/x-pack/test/functional/apps/lens/multi_terms.ts b/x-pack/test/functional/apps/lens/group2/multi_terms.ts similarity index 97% rename from x-pack/test/functional/apps/lens/multi_terms.ts rename to x-pack/test/functional/apps/lens/group2/multi_terms.ts index 05283b80a7c81c..58fa1723789641 100644 --- a/x-pack/test/functional/apps/lens/multi_terms.ts +++ b/x-pack/test/functional/apps/lens/group2/multi_terms.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/runtime_fields.ts b/x-pack/test/functional/apps/lens/group2/runtime_fields.ts similarity index 97% rename from x-pack/test/functional/apps/lens/runtime_fields.ts rename to x-pack/test/functional/apps/lens/group2/runtime_fields.ts index 252951cba4bd09..868432ce1ae4db 100644 --- a/x-pack/test/functional/apps/lens/runtime_fields.ts +++ b/x-pack/test/functional/apps/lens/group2/runtime_fields.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/show_underlying_data.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts similarity index 99% rename from x-pack/test/functional/apps/lens/show_underlying_data.ts rename to x-pack/test/functional/apps/lens/group2/show_underlying_data.ts index 4bc8be22eb8f40..910a4a68806441 100644 --- a/x-pack/test/functional/apps/lens/show_underlying_data.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts @@ -5,7 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header', 'discover']); diff --git a/x-pack/test/functional/apps/lens/show_underlying_data_dashboard.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts similarity index 98% rename from x-pack/test/functional/apps/lens/show_underlying_data_dashboard.ts rename to x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts index dc25e5f77f4125..9446b28c1e3ca8 100644 --- a/x-pack/test/functional/apps/lens/show_underlying_data_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; import uuid from 'uuid'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/functional/apps/lens/annotations.ts b/x-pack/test/functional/apps/lens/group3/annotations.ts similarity index 97% rename from x-pack/test/functional/apps/lens/annotations.ts rename to x-pack/test/functional/apps/lens/group3/annotations.ts index c54b3081f74187..2b641c6c161d4f 100644 --- a/x-pack/test/functional/apps/lens/annotations.ts +++ b/x-pack/test/functional/apps/lens/group3/annotations.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/chart_data.ts b/x-pack/test/functional/apps/lens/group3/chart_data.ts similarity index 98% rename from x-pack/test/functional/apps/lens/chart_data.ts rename to x-pack/test/functional/apps/lens/group3/chart_data.ts index 7ea61c1fa3d81e..6ef40c11407e89 100644 --- a/x-pack/test/functional/apps/lens/chart_data.ts +++ b/x-pack/test/functional/apps/lens/group3/chart_data.ts @@ -8,7 +8,7 @@ import { DebugState } from '@elastic/charts'; import expect from '@kbn/expect'; import { range } from 'lodash'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/colors.ts b/x-pack/test/functional/apps/lens/group3/colors.ts similarity index 96% rename from x-pack/test/functional/apps/lens/colors.ts rename to x-pack/test/functional/apps/lens/group3/colors.ts index 638da79b5c5cb2..4078b0663d7ae3 100644 --- a/x-pack/test/functional/apps/lens/colors.ts +++ b/x-pack/test/functional/apps/lens/group3/colors.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common']); diff --git a/x-pack/test/functional/apps/lens/group3/config.ts b/x-pack/test/functional/apps/lens/group3/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/lens/disable_auto_apply.ts b/x-pack/test/functional/apps/lens/group3/disable_auto_apply.ts similarity index 98% rename from x-pack/test/functional/apps/lens/disable_auto_apply.ts rename to x-pack/test/functional/apps/lens/group3/disable_auto_apply.ts index f73ce56bae694a..e52b1cccda8a39 100644 --- a/x-pack/test/functional/apps/lens/disable_auto_apply.ts +++ b/x-pack/test/functional/apps/lens/group3/disable_auto_apply.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['lens', 'visualize']); diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/group3/drag_and_drop.ts similarity index 99% rename from x-pack/test/functional/apps/lens/drag_and_drop.ts rename to x-pack/test/functional/apps/lens/group3/drag_and_drop.ts index d5b929481e6dcf..dec72008d6f04b 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/group3/drag_and_drop.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/error_handling.ts b/x-pack/test/functional/apps/lens/group3/error_handling.ts similarity index 97% rename from x-pack/test/functional/apps/lens/error_handling.ts rename to x-pack/test/functional/apps/lens/group3/error_handling.ts index 87f62abf88e4ff..8f6659bda15620 100644 --- a/x-pack/test/functional/apps/lens/error_handling.ts +++ b/x-pack/test/functional/apps/lens/group3/error_handling.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/group3/formula.ts similarity index 99% rename from x-pack/test/functional/apps/lens/formula.ts rename to x-pack/test/functional/apps/lens/group3/formula.ts index 02d4fda0e96a65..33a24d3aefb1c3 100644 --- a/x-pack/test/functional/apps/lens/formula.ts +++ b/x-pack/test/functional/apps/lens/group3/formula.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common']); diff --git a/x-pack/test/functional/apps/lens/gauge.ts b/x-pack/test/functional/apps/lens/group3/gauge.ts similarity index 98% rename from x-pack/test/functional/apps/lens/gauge.ts rename to x-pack/test/functional/apps/lens/group3/gauge.ts index c21ddf5b70791f..9cbcbb606b4234 100644 --- a/x-pack/test/functional/apps/lens/gauge.ts +++ b/x-pack/test/functional/apps/lens/group3/gauge.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/geo_field.ts b/x-pack/test/functional/apps/lens/group3/geo_field.ts similarity index 95% rename from x-pack/test/functional/apps/lens/geo_field.ts rename to x-pack/test/functional/apps/lens/group3/geo_field.ts index f9b8277a227317..bb8012ba50d231 100644 --- a/x-pack/test/functional/apps/lens/geo_field.ts +++ b/x-pack/test/functional/apps/lens/group3/geo_field.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'header', 'maps', 'timePicker']); diff --git a/x-pack/test/functional/apps/lens/heatmap.ts b/x-pack/test/functional/apps/lens/group3/heatmap.ts similarity index 99% rename from x-pack/test/functional/apps/lens/heatmap.ts rename to x-pack/test/functional/apps/lens/group3/heatmap.ts index ff1378ac825392..aa1e10a7155472 100644 --- a/x-pack/test/functional/apps/lens/heatmap.ts +++ b/x-pack/test/functional/apps/lens/group3/heatmap.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common']); diff --git a/x-pack/test/functional/apps/lens/group3/index.ts b/x-pack/test/functional/apps/lens/group3/index.ts new file mode 100644 index 00000000000000..03c42e4c70ebfb --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/index.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EsArchiver } from '@kbn/es-archiver'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext) => { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['timePicker']); + const config = getService('config'); + let remoteEsArchiver; + + describe('lens app - group 3', () => { + const esArchive = 'x-pack/test/functional/es_archives/logstash_functional'; + const localIndexPatternString = 'logstash-*'; + const remoteIndexPatternString = 'ftr-remote:logstash-*'; + const localFixtures = { + lensBasic: 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json', + lensDefault: 'x-pack/test/functional/fixtures/kbn_archiver/lens/default', + }; + + const remoteFixtures = { + lensBasic: 'x-pack/test/functional/fixtures/kbn_archiver/lens/ccs/lens_basic.json', + lensDefault: 'x-pack/test/functional/fixtures/kbn_archiver/lens/ccs/default', + }; + let esNode: EsArchiver; + let fixtureDirs: { + lensBasic: string; + lensDefault: string; + }; + let indexPatternString: string; + before(async () => { + await log.debug('Starting lens before method'); + await browser.setWindowSize(1280, 1200); + try { + config.get('esTestCluster.ccs'); + remoteEsArchiver = getService('remoteEsArchiver' as 'esArchiver'); + esNode = remoteEsArchiver; + fixtureDirs = remoteFixtures; + indexPatternString = remoteIndexPatternString; + } catch (error) { + esNode = esArchiver; + fixtureDirs = localFixtures; + indexPatternString = localIndexPatternString; + } + + await esNode.load(esArchive); + // changing the timepicker default here saves us from having to set it in Discover (~8s) + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update({ + defaultIndex: indexPatternString, + 'dateFormat:tz': 'UTC', + }); + await kibanaServer.importExport.load(fixtureDirs.lensBasic); + await kibanaServer.importExport.load(fixtureDirs.lensDefault); + }); + + after(async () => { + await esArchiver.unload(esArchive); + await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.importExport.unload(fixtureDirs.lensBasic); + await kibanaServer.importExport.unload(fixtureDirs.lensDefault); + }); + + loadTestFile(require.resolve('./colors')); + loadTestFile(require.resolve('./chart_data')); + loadTestFile(require.resolve('./time_shift')); + loadTestFile(require.resolve('./drag_and_drop')); + loadTestFile(require.resolve('./disable_auto_apply')); + loadTestFile(require.resolve('./geo_field')); + loadTestFile(require.resolve('./formula')); + loadTestFile(require.resolve('./heatmap')); + loadTestFile(require.resolve('./gauge')); + loadTestFile(require.resolve('./metrics')); + loadTestFile(require.resolve('./reference_lines')); + loadTestFile(require.resolve('./annotations')); + loadTestFile(require.resolve('./inspector')); + loadTestFile(require.resolve('./error_handling')); + loadTestFile(require.resolve('./lens_tagging')); + loadTestFile(require.resolve('./lens_reporting')); + loadTestFile(require.resolve('./tsvb_open_in_lens')); + // has to be last one in the suite because it overrides saved objects + loadTestFile(require.resolve('./rollup')); + }); +}; diff --git a/x-pack/test/functional/apps/lens/inspector.ts b/x-pack/test/functional/apps/lens/group3/inspector.ts similarity index 96% rename from x-pack/test/functional/apps/lens/inspector.ts rename to x-pack/test/functional/apps/lens/group3/inspector.ts index d94d3413c07b0e..9f52d783011c4f 100644 --- a/x-pack/test/functional/apps/lens/inspector.ts +++ b/x-pack/test/functional/apps/lens/group3/inspector.ts @@ -5,7 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; -import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/lens_reporting.ts b/x-pack/test/functional/apps/lens/group3/lens_reporting.ts similarity index 96% rename from x-pack/test/functional/apps/lens/lens_reporting.ts rename to x-pack/test/functional/apps/lens/group3/lens_reporting.ts index 6dfb1dd923b3e9..2cbb55ae03d970 100644 --- a/x-pack/test/functional/apps/lens/lens_reporting.ts +++ b/x-pack/test/functional/apps/lens/group3/lens_reporting.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'reporting', 'timePicker']); diff --git a/x-pack/test/functional/apps/lens/lens_tagging.ts b/x-pack/test/functional/apps/lens/group3/lens_tagging.ts similarity index 98% rename from x-pack/test/functional/apps/lens/lens_tagging.ts rename to x-pack/test/functional/apps/lens/group3/lens_tagging.ts index 3852fdb0456acf..d69b49403fc315 100644 --- a/x-pack/test/functional/apps/lens/lens_tagging.ts +++ b/x-pack/test/functional/apps/lens/group3/lens_tagging.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); diff --git a/x-pack/test/functional/apps/lens/metrics.ts b/x-pack/test/functional/apps/lens/group3/metrics.ts similarity index 97% rename from x-pack/test/functional/apps/lens/metrics.ts rename to x-pack/test/functional/apps/lens/group3/metrics.ts index 62a8b69141a58b..bf651f7a12a1bb 100644 --- a/x-pack/test/functional/apps/lens/metrics.ts +++ b/x-pack/test/functional/apps/lens/group3/metrics.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/reference_lines.ts b/x-pack/test/functional/apps/lens/group3/reference_lines.ts similarity index 98% rename from x-pack/test/functional/apps/lens/reference_lines.ts rename to x-pack/test/functional/apps/lens/group3/reference_lines.ts index 97c6ebf5b138f7..f022a6cef6e7a4 100644 --- a/x-pack/test/functional/apps/lens/reference_lines.ts +++ b/x-pack/test/functional/apps/lens/group3/reference_lines.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/rollup.ts b/x-pack/test/functional/apps/lens/group3/rollup.ts similarity index 98% rename from x-pack/test/functional/apps/lens/rollup.ts rename to x-pack/test/functional/apps/lens/group3/rollup.ts index 25ab766d04bbd2..d42cc67ad673ce 100644 --- a/x-pack/test/functional/apps/lens/rollup.ts +++ b/x-pack/test/functional/apps/lens/group3/rollup.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'header', 'timePicker']); diff --git a/x-pack/test/functional/apps/lens/time_shift.ts b/x-pack/test/functional/apps/lens/group3/time_shift.ts similarity index 97% rename from x-pack/test/functional/apps/lens/time_shift.ts rename to x-pack/test/functional/apps/lens/group3/time_shift.ts index e3363bee419aa7..4dd22ea719ec76 100644 --- a/x-pack/test/functional/apps/lens/time_shift.ts +++ b/x-pack/test/functional/apps/lens/group3/time_shift.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/tsvb_open_in_lens.ts b/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts similarity index 99% rename from x-pack/test/functional/apps/lens/tsvb_open_in_lens.ts rename to x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts index 0315d20e5fc919..f9d21f80462e6f 100644 --- a/x-pack/test/functional/apps/lens/tsvb_open_in_lens.ts +++ b/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const { visualize, visualBuilder, header, lens, timeToVisualize, dashboard, canvas } = diff --git a/x-pack/test/functional/apps/license_management/config.ts b/x-pack/test/functional/apps/license_management/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/license_management/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/license_management/index.ts b/x-pack/test/functional/apps/license_management/index.ts index a209a4370ced9f..d4256588667ec1 100644 --- a/x-pack/test/functional/apps/license_management/index.ts +++ b/x-pack/test/functional/apps/license_management/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('License app', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); diff --git a/x-pack/test/functional/apps/logstash/config.ts b/x-pack/test/functional/apps/logstash/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/logstash/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/logstash/feature_controls/index.ts b/x-pack/test/functional/apps/logstash/feature_controls/index.ts index eb4c681531cbcd..c50611d7fa7c42 100644 --- a/x-pack/test/functional/apps/logstash/feature_controls/index.ts +++ b/x-pack/test/functional/apps/logstash/feature_controls/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { - this.tags(['ciGroup2']); - loadTestFile(require.resolve('./logstash_security')); }); } diff --git a/x-pack/test/functional/apps/logstash/index.js b/x-pack/test/functional/apps/logstash/index.js index 8c1ac233e9c571..deaf4041b1b59d 100644 --- a/x-pack/test/functional/apps/logstash/index.js +++ b/x-pack/test/functional/apps/logstash/index.js @@ -7,8 +7,6 @@ export default function ({ loadTestFile }) { describe('logstash', function () { - this.tags(['ciGroup2']); - loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./pipeline_list')); loadTestFile(require.resolve('./pipeline_create')); diff --git a/x-pack/test/functional/apps/management/config.ts b/x-pack/test/functional/apps/management/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/management/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/management/feature_controls/index.ts b/x-pack/test/functional/apps/management/feature_controls/index.ts index 66eb2e49376201..17e71ef1908560 100644 --- a/x-pack/test/functional/apps/management/feature_controls/index.ts +++ b/x-pack/test/functional/apps/management/feature_controls/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { - this.tags(['ciGroup2']); - loadTestFile(require.resolve('./management_security')); }); } diff --git a/x-pack/test/functional/apps/management/index.ts b/x-pack/test/functional/apps/management/index.ts index 03fec9fffe4fb9..72da3e0fd739ae 100644 --- a/x-pack/test/functional/apps/management/index.ts +++ b/x-pack/test/functional/apps/management/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('management', function () { - this.tags(['ciGroup2']); - loadTestFile(require.resolve('./create_index_pattern_wizard')); loadTestFile(require.resolve('./feature_controls')); }); diff --git a/x-pack/test/functional/apps/maps/README.md b/x-pack/test/functional/apps/maps/README.md new file mode 100644 index 00000000000000..5e87a8b210bdd4 --- /dev/null +++ b/x-pack/test/functional/apps/maps/README.md @@ -0,0 +1,7 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations \ No newline at end of file diff --git a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js b/x-pack/test/functional/apps/maps/group1/auto_fit_to_bounds.js similarity index 100% rename from x-pack/test/functional/apps/maps/auto_fit_to_bounds.js rename to x-pack/test/functional/apps/maps/group1/auto_fit_to_bounds.js diff --git a/x-pack/test/functional/apps/maps/blended_vector_layer.js b/x-pack/test/functional/apps/maps/group1/blended_vector_layer.js similarity index 100% rename from x-pack/test/functional/apps/maps/blended_vector_layer.js rename to x-pack/test/functional/apps/maps/group1/blended_vector_layer.js diff --git a/x-pack/test/functional/apps/maps/group1/config.ts b/x-pack/test/functional/apps/maps/group1/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/maps/group1/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/group1/documents_source/docvalue_fields.js similarity index 100% rename from x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js rename to x-pack/test/functional/apps/maps/group1/documents_source/docvalue_fields.js diff --git a/x-pack/test/functional/apps/maps/documents_source/index.js b/x-pack/test/functional/apps/maps/group1/documents_source/index.js similarity index 100% rename from x-pack/test/functional/apps/maps/documents_source/index.js rename to x-pack/test/functional/apps/maps/group1/documents_source/index.js diff --git a/x-pack/test/functional/apps/maps/documents_source/search_hits.js b/x-pack/test/functional/apps/maps/group1/documents_source/search_hits.js similarity index 100% rename from x-pack/test/functional/apps/maps/documents_source/search_hits.js rename to x-pack/test/functional/apps/maps/group1/documents_source/search_hits.js diff --git a/x-pack/test/functional/apps/maps/documents_source/top_hits.js b/x-pack/test/functional/apps/maps/group1/documents_source/top_hits.js similarity index 100% rename from x-pack/test/functional/apps/maps/documents_source/top_hits.js rename to x-pack/test/functional/apps/maps/group1/documents_source/top_hits.js diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/group1/feature_controls/maps_security.ts similarity index 99% rename from x-pack/test/functional/apps/maps/feature_controls/maps_security.ts rename to x-pack/test/functional/apps/maps/group1/feature_controls/maps_security.ts index db6bfb642ebbb1..ad4a2acd475a0c 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/group1/feature_controls/maps_security.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const security = getService('security'); diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts b/x-pack/test/functional/apps/maps/group1/feature_controls/maps_spaces.ts similarity index 98% rename from x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts rename to x-pack/test/functional/apps/maps/group1/feature_controls/maps_spaces.ts index 1beddd72d3fde1..a306c15bfbdb26 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts +++ b/x-pack/test/functional/apps/maps/group1/feature_controls/maps_spaces.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { APP_ID } from '@kbn/maps-plugin/common/constants'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); diff --git a/x-pack/test/functional/apps/maps/full_screen_mode.js b/x-pack/test/functional/apps/maps/group1/full_screen_mode.js similarity index 100% rename from x-pack/test/functional/apps/maps/full_screen_mode.js rename to x-pack/test/functional/apps/maps/group1/full_screen_mode.js diff --git a/x-pack/test/functional/apps/maps/group1/index.js b/x-pack/test/functional/apps/maps/group1/index.js new file mode 100644 index 00000000000000..be59d430126265 --- /dev/null +++ b/x-pack/test/functional/apps/maps/group1/index.js @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function ({ loadTestFile, getService }) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); + const supertest = getService('supertest'); + + describe('maps app', function () { + this.tags(['skipFirefox']); + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' + ); + // Functional tests verify behavior when referenced index pattern saved objects can not be found. + // However, saved object import fails when reference saved objects can not be found. + // To prevent import errors, index pattern saved object references exist during import + // but are then deleted afterwards to enable testing of missing reference index pattern saved objects. + + log.info('Delete index pattern'); + log.debug('id: ' + 'idThatDoesNotExitForESGeoGridSource'); + log.debug('id: ' + 'idThatDoesNotExitForESSearchSource'); + log.debug('id: ' + 'idThatDoesNotExitForESJoinSource'); + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESGeoGridSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESSearchSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESJoinSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await esArchiver.load('x-pack/test/functional/es_archives/maps/data'); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', + }); + await browser.setWindowSize(1600, 1000); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/maps/data'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' + ); + }); + + loadTestFile(require.resolve('./documents_source')); + loadTestFile(require.resolve('./blended_vector_layer')); + loadTestFile(require.resolve('./vector_styling')); + loadTestFile(require.resolve('./saved_object_management')); + loadTestFile(require.resolve('./sample_data')); + loadTestFile(require.resolve('./auto_fit_to_bounds')); + loadTestFile(require.resolve('./layer_visibility')); + loadTestFile(require.resolve('./feature_controls/maps_security')); + loadTestFile(require.resolve('./feature_controls/maps_spaces')); + loadTestFile(require.resolve('./full_screen_mode')); + }); +} diff --git a/x-pack/test/functional/apps/maps/layer_visibility.js b/x-pack/test/functional/apps/maps/group1/layer_visibility.js similarity index 100% rename from x-pack/test/functional/apps/maps/layer_visibility.js rename to x-pack/test/functional/apps/maps/group1/layer_visibility.js diff --git a/x-pack/test/functional/apps/maps/sample_data.js b/x-pack/test/functional/apps/maps/group1/sample_data.js similarity index 100% rename from x-pack/test/functional/apps/maps/sample_data.js rename to x-pack/test/functional/apps/maps/group1/sample_data.js diff --git a/x-pack/test/functional/apps/maps/saved_object_management.js b/x-pack/test/functional/apps/maps/group1/saved_object_management.js similarity index 100% rename from x-pack/test/functional/apps/maps/saved_object_management.js rename to x-pack/test/functional/apps/maps/group1/saved_object_management.js diff --git a/x-pack/test/functional/apps/maps/vector_styling.js b/x-pack/test/functional/apps/maps/group1/vector_styling.js similarity index 100% rename from x-pack/test/functional/apps/maps/vector_styling.js rename to x-pack/test/functional/apps/maps/group1/vector_styling.js diff --git a/x-pack/test/functional/apps/maps/group2/config.ts b/x-pack/test/functional/apps/maps/group2/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/maps/group2/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/maps/embeddable/add_to_dashboard.js b/x-pack/test/functional/apps/maps/group2/embeddable/add_to_dashboard.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/add_to_dashboard.js rename to x-pack/test/functional/apps/maps/group2/embeddable/add_to_dashboard.js diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/group2/embeddable/dashboard.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/dashboard.js rename to x-pack/test/functional/apps/maps/group2/embeddable/dashboard.js diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js b/x-pack/test/functional/apps/maps/group2/embeddable/embeddable_library.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/embeddable_library.js rename to x-pack/test/functional/apps/maps/group2/embeddable/embeddable_library.js diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js b/x-pack/test/functional/apps/maps/group2/embeddable/embeddable_state.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/embeddable_state.js rename to x-pack/test/functional/apps/maps/group2/embeddable/embeddable_state.js diff --git a/x-pack/test/functional/apps/maps/embeddable/filter_by_map_extent.js b/x-pack/test/functional/apps/maps/group2/embeddable/filter_by_map_extent.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/filter_by_map_extent.js rename to x-pack/test/functional/apps/maps/group2/embeddable/filter_by_map_extent.js diff --git a/x-pack/test/functional/apps/maps/embeddable/index.js b/x-pack/test/functional/apps/maps/group2/embeddable/index.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/index.js rename to x-pack/test/functional/apps/maps/group2/embeddable/index.js diff --git a/x-pack/test/functional/apps/maps/embeddable/save_and_return.js b/x-pack/test/functional/apps/maps/group2/embeddable/save_and_return.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/save_and_return.js rename to x-pack/test/functional/apps/maps/group2/embeddable/save_and_return.js diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/group2/embeddable/tooltip_filter_actions.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js rename to x-pack/test/functional/apps/maps/group2/embeddable/tooltip_filter_actions.js diff --git a/x-pack/test/functional/apps/maps/es_geo_grid_source.js b/x-pack/test/functional/apps/maps/group2/es_geo_grid_source.js similarity index 100% rename from x-pack/test/functional/apps/maps/es_geo_grid_source.js rename to x-pack/test/functional/apps/maps/group2/es_geo_grid_source.js diff --git a/x-pack/test/functional/apps/maps/group2/index.js b/x-pack/test/functional/apps/maps/group2/index.js new file mode 100644 index 00000000000000..b8b3a15a10ed58 --- /dev/null +++ b/x-pack/test/functional/apps/maps/group2/index.js @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function ({ loadTestFile, getService }) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); + const supertest = getService('supertest'); + + describe('maps app', function () { + this.tags(['skipFirefox']); + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' + ); + // Functional tests verify behavior when referenced index pattern saved objects can not be found. + // However, saved object import fails when reference saved objects can not be found. + // To prevent import errors, index pattern saved object references exist during import + // but are then deleted afterwards to enable testing of missing reference index pattern saved objects. + + log.info('Delete index pattern'); + log.debug('id: ' + 'idThatDoesNotExitForESGeoGridSource'); + log.debug('id: ' + 'idThatDoesNotExitForESSearchSource'); + log.debug('id: ' + 'idThatDoesNotExitForESJoinSource'); + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESGeoGridSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESSearchSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESJoinSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await esArchiver.load('x-pack/test/functional/es_archives/maps/data'); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', + }); + await browser.setWindowSize(1600, 1000); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/maps/data'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' + ); + }); + + loadTestFile(require.resolve('./es_geo_grid_source')); + loadTestFile(require.resolve('./embeddable')); + }); +} diff --git a/x-pack/test/functional/apps/maps/group3/config.ts b/x-pack/test/functional/apps/maps/group3/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/maps/group3/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/maps/group3/index.js b/x-pack/test/functional/apps/maps/group3/index.js new file mode 100644 index 00000000000000..fda116cecc307e --- /dev/null +++ b/x-pack/test/functional/apps/maps/group3/index.js @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function ({ loadTestFile, getService }) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); + const supertest = getService('supertest'); + + describe('maps app', function () { + this.tags(['skipFirefox']); + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' + ); + // Functional tests verify behavior when referenced index pattern saved objects can not be found. + // However, saved object import fails when reference saved objects can not be found. + // To prevent import errors, index pattern saved object references exist during import + // but are then deleted afterwards to enable testing of missing reference index pattern saved objects. + + log.info('Delete index pattern'); + log.debug('id: ' + 'idThatDoesNotExitForESGeoGridSource'); + log.debug('id: ' + 'idThatDoesNotExitForESSearchSource'); + log.debug('id: ' + 'idThatDoesNotExitForESJoinSource'); + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESGeoGridSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESSearchSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESJoinSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await esArchiver.load('x-pack/test/functional/es_archives/maps/data'); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', + }); + await browser.setWindowSize(1600, 1000); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/maps/data'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' + ); + }); + + loadTestFile(require.resolve('./reports')); + }); +} diff --git a/x-pack/test/functional/apps/maps/reports/baseline/example_map_report.png b/x-pack/test/functional/apps/maps/group3/reports/baseline/example_map_report.png similarity index 100% rename from x-pack/test/functional/apps/maps/reports/baseline/example_map_report.png rename to x-pack/test/functional/apps/maps/group3/reports/baseline/example_map_report.png diff --git a/x-pack/test/functional/apps/maps/reports/baseline/geo_map_report.png b/x-pack/test/functional/apps/maps/group3/reports/baseline/geo_map_report.png similarity index 100% rename from x-pack/test/functional/apps/maps/reports/baseline/geo_map_report.png rename to x-pack/test/functional/apps/maps/group3/reports/baseline/geo_map_report.png diff --git a/x-pack/test/functional/apps/maps/reports/index.ts b/x-pack/test/functional/apps/maps/group3/reports/index.ts similarity index 97% rename from x-pack/test/functional/apps/maps/reports/index.ts rename to x-pack/test/functional/apps/maps/group3/reports/index.ts index 4e942b1e150ef0..c892842782aa66 100644 --- a/x-pack/test/functional/apps/maps/reports/index.ts +++ b/x-pack/test/functional/apps/maps/group3/reports/index.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const REPORTS_FOLDER = __dirname; diff --git a/x-pack/test/functional/apps/maps/add_layer_panel.js b/x-pack/test/functional/apps/maps/group4/add_layer_panel.js similarity index 100% rename from x-pack/test/functional/apps/maps/add_layer_panel.js rename to x-pack/test/functional/apps/maps/group4/add_layer_panel.js diff --git a/x-pack/test/functional/apps/maps/group4/config.ts b/x-pack/test/functional/apps/maps/group4/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/maps/group4/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/maps/discover.js b/x-pack/test/functional/apps/maps/group4/discover.js similarity index 100% rename from x-pack/test/functional/apps/maps/discover.js rename to x-pack/test/functional/apps/maps/group4/discover.js diff --git a/x-pack/test/functional/apps/maps/es_pew_pew_source.js b/x-pack/test/functional/apps/maps/group4/es_pew_pew_source.js similarity index 100% rename from x-pack/test/functional/apps/maps/es_pew_pew_source.js rename to x-pack/test/functional/apps/maps/group4/es_pew_pew_source.js diff --git a/x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.dbf b/x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.dbf similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.dbf rename to x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.dbf diff --git a/x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.prj b/x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.prj similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.prj rename to x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.prj diff --git a/x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.shp b/x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.shp similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.shp rename to x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.shp diff --git a/x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.shx b/x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.shx similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.shx rename to x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.shx diff --git a/x-pack/test/functional/apps/maps/file_upload/files/point.json b/x-pack/test/functional/apps/maps/group4/file_upload/files/point.json similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/point.json rename to x-pack/test/functional/apps/maps/group4/file_upload/files/point.json diff --git a/x-pack/test/functional/apps/maps/file_upload/files/polygon.json b/x-pack/test/functional/apps/maps/group4/file_upload/files/polygon.json similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/polygon.json rename to x-pack/test/functional/apps/maps/group4/file_upload/files/polygon.json diff --git a/x-pack/test/functional/apps/maps/file_upload/files/world_countries_v7.geo.json b/x-pack/test/functional/apps/maps/group4/file_upload/files/world_countries_v7.geo.json similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/world_countries_v7.geo.json rename to x-pack/test/functional/apps/maps/group4/file_upload/files/world_countries_v7.geo.json diff --git a/x-pack/test/functional/apps/maps/file_upload/geojson.js b/x-pack/test/functional/apps/maps/group4/file_upload/geojson.js similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/geojson.js rename to x-pack/test/functional/apps/maps/group4/file_upload/geojson.js diff --git a/x-pack/test/functional/apps/maps/file_upload/index.js b/x-pack/test/functional/apps/maps/group4/file_upload/index.js similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/index.js rename to x-pack/test/functional/apps/maps/group4/file_upload/index.js diff --git a/x-pack/test/functional/apps/maps/file_upload/shapefile.js b/x-pack/test/functional/apps/maps/group4/file_upload/shapefile.js similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/shapefile.js rename to x-pack/test/functional/apps/maps/group4/file_upload/shapefile.js diff --git a/x-pack/test/functional/apps/maps/file_upload/wizard.js b/x-pack/test/functional/apps/maps/group4/file_upload/wizard.js similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/wizard.js rename to x-pack/test/functional/apps/maps/group4/file_upload/wizard.js diff --git a/x-pack/test/functional/apps/maps/geofile_wizard_auto_open.ts b/x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts similarity index 95% rename from x-pack/test/functional/apps/maps/geofile_wizard_auto_open.ts rename to x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts index dd69cc7882fc7a..ebe434b6afe6e6 100644 --- a/x-pack/test/functional/apps/maps/geofile_wizard_auto_open.ts +++ b/x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'maps']); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/group4/index.js similarity index 57% rename from x-pack/test/functional/apps/maps/index.js rename to x-pack/test/functional/apps/maps/group4/index.js index 9d7e7a9cbddcc2..70cb5ac6607689 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/group4/index.js @@ -58,46 +58,18 @@ export default function ({ loadTestFile, getService }) { ); }); - describe('', async function () { - this.tags('ciGroup9'); - loadTestFile(require.resolve('./documents_source')); - loadTestFile(require.resolve('./blended_vector_layer')); - loadTestFile(require.resolve('./vector_styling')); - loadTestFile(require.resolve('./saved_object_management')); - loadTestFile(require.resolve('./sample_data')); - loadTestFile(require.resolve('./auto_fit_to_bounds')); - loadTestFile(require.resolve('./layer_visibility')); - loadTestFile(require.resolve('./feature_controls/maps_security')); - loadTestFile(require.resolve('./feature_controls/maps_spaces')); - loadTestFile(require.resolve('./full_screen_mode')); - }); - - describe('', function () { - this.tags('ciGroup22'); - loadTestFile(require.resolve('./es_geo_grid_source')); - loadTestFile(require.resolve('./embeddable')); - }); - - describe('', function () { - this.tags('ciGroup2'); // same group used in x-pack/test/reporting_functional - loadTestFile(require.resolve('./reports')); - }); - - describe('', function () { - this.tags('ciGroup10'); - loadTestFile(require.resolve('./es_pew_pew_source')); - loadTestFile(require.resolve('./joins')); - loadTestFile(require.resolve('./mvt_joins')); - loadTestFile(require.resolve('./mapbox_styles')); - loadTestFile(require.resolve('./mvt_scaling')); - loadTestFile(require.resolve('./mvt_geotile_grid')); - loadTestFile(require.resolve('./add_layer_panel')); - loadTestFile(require.resolve('./file_upload')); - loadTestFile(require.resolve('./layer_errors')); - loadTestFile(require.resolve('./visualize_create_menu')); - loadTestFile(require.resolve('./discover')); - loadTestFile(require.resolve('./geofile_wizard_auto_open')); - loadTestFile(require.resolve('./lens')); - }); + loadTestFile(require.resolve('./es_pew_pew_source')); + loadTestFile(require.resolve('./joins')); + loadTestFile(require.resolve('./mvt_joins')); + loadTestFile(require.resolve('./mapbox_styles')); + loadTestFile(require.resolve('./mvt_scaling')); + loadTestFile(require.resolve('./mvt_geotile_grid')); + loadTestFile(require.resolve('./add_layer_panel')); + loadTestFile(require.resolve('./file_upload')); + loadTestFile(require.resolve('./layer_errors')); + loadTestFile(require.resolve('./visualize_create_menu')); + loadTestFile(require.resolve('./discover')); + loadTestFile(require.resolve('./geofile_wizard_auto_open')); + loadTestFile(require.resolve('./lens')); }); } diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/group4/joins.js similarity index 100% rename from x-pack/test/functional/apps/maps/joins.js rename to x-pack/test/functional/apps/maps/group4/joins.js diff --git a/x-pack/test/functional/apps/maps/layer_errors.js b/x-pack/test/functional/apps/maps/group4/layer_errors.js similarity index 100% rename from x-pack/test/functional/apps/maps/layer_errors.js rename to x-pack/test/functional/apps/maps/group4/layer_errors.js diff --git a/x-pack/test/functional/apps/maps/lens/choropleth_chart.ts b/x-pack/test/functional/apps/maps/group4/lens/choropleth_chart.ts similarity index 97% rename from x-pack/test/functional/apps/maps/lens/choropleth_chart.ts rename to x-pack/test/functional/apps/maps/group4/lens/choropleth_chart.ts index 420f895fe6aa68..b026dd74447484 100644 --- a/x-pack/test/functional/apps/maps/lens/choropleth_chart.ts +++ b/x-pack/test/functional/apps/maps/group4/lens/choropleth_chart.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'maps']); diff --git a/x-pack/test/functional/apps/maps/lens/index.ts b/x-pack/test/functional/apps/maps/group4/lens/index.ts similarity index 85% rename from x-pack/test/functional/apps/maps/lens/index.ts rename to x-pack/test/functional/apps/maps/group4/lens/index.ts index 78086303166a1b..484faf431be75b 100644 --- a/x-pack/test/functional/apps/maps/lens/index.ts +++ b/x-pack/test/functional/apps/maps/group4/lens/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('lens', function () { diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/group4/mapbox_styles.js similarity index 100% rename from x-pack/test/functional/apps/maps/mapbox_styles.js rename to x-pack/test/functional/apps/maps/group4/mapbox_styles.js diff --git a/x-pack/test/functional/apps/maps/mvt_geotile_grid.js b/x-pack/test/functional/apps/maps/group4/mvt_geotile_grid.js similarity index 100% rename from x-pack/test/functional/apps/maps/mvt_geotile_grid.js rename to x-pack/test/functional/apps/maps/group4/mvt_geotile_grid.js diff --git a/x-pack/test/functional/apps/maps/mvt_joins.ts b/x-pack/test/functional/apps/maps/group4/mvt_joins.ts similarity index 98% rename from x-pack/test/functional/apps/maps/mvt_joins.ts rename to x-pack/test/functional/apps/maps/group4/mvt_joins.ts index 2ae8f7ea5943b8..c6567c84b21655 100644 --- a/x-pack/test/functional/apps/maps/mvt_joins.ts +++ b/x-pack/test/functional/apps/maps/group4/mvt_joins.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['maps']); diff --git a/x-pack/test/functional/apps/maps/mvt_scaling.js b/x-pack/test/functional/apps/maps/group4/mvt_scaling.js similarity index 100% rename from x-pack/test/functional/apps/maps/mvt_scaling.js rename to x-pack/test/functional/apps/maps/group4/mvt_scaling.js diff --git a/x-pack/test/functional/apps/maps/visualize_create_menu.js b/x-pack/test/functional/apps/maps/group4/visualize_create_menu.js similarity index 100% rename from x-pack/test/functional/apps/maps/visualize_create_menu.js rename to x-pack/test/functional/apps/maps/group4/visualize_create_menu.js diff --git a/x-pack/test/functional/apps/ml/README.md b/x-pack/test/functional/apps/ml/README.md new file mode 100644 index 00000000000000..5e87a8b210bdd4 --- /dev/null +++ b/x-pack/test/functional/apps/ml/README.md @@ -0,0 +1,7 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations \ No newline at end of file diff --git a/x-pack/test/functional/apps/ml/data_visualizer/config.ts b/x-pack/test/functional/apps/ml/data_visualizer/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_visualizer/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts index f5610e8b607da3..ef15775f862046 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts @@ -5,8 +5,6 @@ * 2.0. */ -import path from 'path'; - import { ML_JOB_FIELD_TYPES } from '@kbn/ml-plugin/common/constants/field_types'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -16,7 +14,7 @@ export default function ({ getService }: FtrProviderContext) { const testDataListPositive = [ { suiteSuffix: 'with an artificial server log', - filePath: path.join(__dirname, 'files_to_import', 'artificial_server_log'), + filePath: require.resolve('./files_to_import/artificial_server_log'), indexName: 'user-import_1', createIndexPattern: false, fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER, ML_JOB_FIELD_TYPES.DATE], @@ -116,7 +114,7 @@ export default function ({ getService }: FtrProviderContext) { }, { suiteSuffix: 'with a file containing geo field', - filePath: path.join(__dirname, 'files_to_import', 'geo_file.csv'), + filePath: require.resolve('./files_to_import/geo_file.csv'), indexName: 'user-import_2', createIndexPattern: false, fieldTypeFilters: [ML_JOB_FIELD_TYPES.GEO_POINT], @@ -158,7 +156,7 @@ export default function ({ getService }: FtrProviderContext) { }, { suiteSuffix: 'with a file with a missing new line char at the end', - filePath: path.join(__dirname, 'files_to_import', 'missing_end_of_file_newline.csv'), + filePath: require.resolve('./files_to_import/missing_end_of_file_newline.csv'), indexName: 'user-import_3', createIndexPattern: false, fieldTypeFilters: [], @@ -205,7 +203,7 @@ export default function ({ getService }: FtrProviderContext) { const testDataListNegative = [ { suiteSuffix: 'with a non-log file', - filePath: path.join(__dirname, 'files_to_import', 'not_a_log_file'), + filePath: require.resolve('./files_to_import/not_a_log_file'), }, ]; diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index.ts b/x-pack/test/functional/apps/ml/data_visualizer/index.ts index 3bb8e3d7283187..973ebf2bbe3ab3 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index.ts @@ -7,10 +7,38 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; -export default function ({ loadTestFile }: FtrProviderContext) { - describe('data visualizer', function () { +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning - data visualizer', function () { this.tags(['skipFirefox', 'mlqa']); + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/event_rate_nanos'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/bm_classification'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/egs_regression'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); + + await ml.testResources.resetKibanaTimeZone(); + }); + loadTestFile(require.resolve('./index_data_visualizer')); loadTestFile(require.resolve('./index_data_visualizer_grid_in_discover')); loadTestFile(require.resolve('./index_data_visualizer_grid_in_dashboard')); diff --git a/x-pack/test/functional/apps/ml/group1/config.ts b/x-pack/test/functional/apps/ml/group1/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/ml/group1/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation.ts similarity index 98% rename from x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation.ts index 2ba4ac6f08350d..0cf7c4177f057f 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation_saved_search.ts similarity index 98% rename from x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation_saved_search.ts index 67550ae17a4b06..cfba10c25b17b3 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation_saved_search.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/cloning.ts similarity index 99% rename from x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/cloning.ts index 3a33c95edba423..82f76e66b4ebd5 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/cloning.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { DeepPartial } from '@kbn/ml-plugin/common/types/common'; import { DataFrameAnalyticsConfig } from '@kbn/ml-plugin/public/application/data_frame_analytics/common'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/index.ts similarity index 93% rename from x-pack/test/functional/apps/ml/data_frame_analytics/index.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/index.ts index bc11a44148546c..cf9bd17f11b814 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('data frame analytics', function () { diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts similarity index 98% rename from x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts index 1dacd8a7e80b46..8a53528a899224 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation_saved_search.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts similarity index 99% rename from x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation_saved_search.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts index 861be18591a118..89247aed78ac4c 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation_saved_search.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation.ts similarity index 98% rename from x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation.ts index 7a84c41aa4a661..a0cbd123b51694 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation_saved_search.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation_saved_search.ts similarity index 98% rename from x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation_saved_search.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation_saved_search.ts index e22c4908486d18..6b09b35c610a07 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation_saved_search.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation_saved_search.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/results_view_content.ts similarity index 99% rename from x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/results_view_content.ts index 2bddf0a7d95125..8d04c4897dab0c 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/results_view_content.ts @@ -8,7 +8,7 @@ import { DeepPartial } from '@kbn/ml-plugin/common/types/common'; import { DataFrameAnalyticsConfig } from '@kbn/ml-plugin/public/application/data_frame_analytics/common'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/group1/index.ts similarity index 64% rename from x-pack/test/functional/apps/ml/index.ts rename to x-pack/test/functional/apps/ml/group1/index.ts index c58b20e1c374bd..7129f3e24d4f1e 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/group1/index.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('machine learning', function () { + describe('machine learning - group 2', () => { before(async () => { await ml.securityCommon.createMlRoles(); await ml.securityCommon.createMlUsers(); @@ -37,26 +37,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - describe('', function () { - this.tags('ciGroup15'); - loadTestFile(require.resolve('./permissions')); - loadTestFile(require.resolve('./pages')); - loadTestFile(require.resolve('./data_visualizer')); - loadTestFile(require.resolve('./data_frame_analytics')); - loadTestFile(require.resolve('./model_management')); - }); - - describe('', function () { - this.tags('ciGroup26'); - loadTestFile(require.resolve('./anomaly_detection')); - }); - - describe('', function () { - this.tags('ciGroup8'); - loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./settings')); - loadTestFile(require.resolve('./embeddables')); - loadTestFile(require.resolve('./stack_management_jobs')); - }); + loadTestFile(require.resolve('./permissions')); + loadTestFile(require.resolve('./pages')); + loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./model_management')); }); } diff --git a/x-pack/test/functional/apps/ml/model_management/index.ts b/x-pack/test/functional/apps/ml/group1/model_management/index.ts similarity index 86% rename from x-pack/test/functional/apps/ml/model_management/index.ts rename to x-pack/test/functional/apps/ml/group1/model_management/index.ts index e958392d9ba748..5595486260deee 100644 --- a/x-pack/test/functional/apps/ml/model_management/index.ts +++ b/x-pack/test/functional/apps/ml/group1/model_management/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('model management', function () { diff --git a/x-pack/test/functional/apps/ml/model_management/model_list.ts b/x-pack/test/functional/apps/ml/group1/model_management/model_list.ts similarity index 99% rename from x-pack/test/functional/apps/ml/model_management/model_list.ts rename to x-pack/test/functional/apps/ml/group1/model_management/model_list.ts index 08fb3b7124aec3..ca360130b89f9f 100644 --- a/x-pack/test/functional/apps/ml/model_management/model_list.ts +++ b/x-pack/test/functional/apps/ml/group1/model_management/model_list.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); diff --git a/x-pack/test/functional/apps/ml/pages.ts b/x-pack/test/functional/apps/ml/group1/pages.ts similarity index 94% rename from x-pack/test/functional/apps/ml/pages.ts rename to x-pack/test/functional/apps/ml/group1/pages.ts index 6c8687d213cee8..2cc271e67194e5 100644 --- a/x-pack/test/functional/apps/ml/pages.ts +++ b/x-pack/test/functional/apps/ml/group1/pages.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); @@ -21,7 +21,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('loads the ML home page'); await ml.navigation.navigateToMl(); - await ml.testExecution.logTestStep('loads the overview page'); + await ml.testExecution.logTestStep('loads the overview page'); await ml.navigation.navigateToOverview(); await ml.testExecution.logTestStep('loads the anomaly detection area'); diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/group1/permissions/full_ml_access.ts similarity index 98% rename from x-pack/test/functional/apps/ml/permissions/full_ml_access.ts rename to x-pack/test/functional/apps/ml/group1/permissions/full_ml_access.ts index 467bfc370e8e2b..c632ae48b3f885 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/group1/permissions/full_ml_access.ts @@ -5,11 +5,9 @@ * 2.0. */ -import path from 'path'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -import { USER } from '../../../services/ml/security_common'; +import { USER } from '../../../../services/ml/security_common'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -123,12 +121,8 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - const uploadFilePath = path.join( - __dirname, - '..', - 'data_visualizer', - 'files_to_import', - 'artificial_server_log' + const uploadFilePath = require.resolve( + '../../data_visualizer/files_to_import/artificial_server_log' ); const expectedUploadFileTitle = 'artificial_server_log'; diff --git a/x-pack/test/functional/apps/ml/permissions/index.ts b/x-pack/test/functional/apps/ml/group1/permissions/index.ts similarity index 88% rename from x-pack/test/functional/apps/ml/permissions/index.ts rename to x-pack/test/functional/apps/ml/group1/permissions/index.ts index e777f241eaf85c..23d7d6fe9e2b53 100644 --- a/x-pack/test/functional/apps/ml/permissions/index.ts +++ b/x-pack/test/functional/apps/ml/group1/permissions/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('permissions', function () { diff --git a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/group1/permissions/no_ml_access.ts similarity index 93% rename from x-pack/test/functional/apps/ml/permissions/no_ml_access.ts rename to x-pack/test/functional/apps/ml/group1/permissions/no_ml_access.ts index 6132e6e63b1b01..4a1c108b2fa5a8 100644 --- a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional/apps/ml/group1/permissions/no_ml_access.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { USER } from '../../../services/ml/security_common'; +import { USER } from '../../../../services/ml/security_common'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'error']); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/group1/permissions/read_ml_access.ts similarity index 98% rename from x-pack/test/functional/apps/ml/permissions/read_ml_access.ts rename to x-pack/test/functional/apps/ml/group1/permissions/read_ml_access.ts index fd9cb2cb4c79e7..a18a6075055a63 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/group1/permissions/read_ml_access.ts @@ -5,11 +5,9 @@ * 2.0. */ -import path from 'path'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -import { USER } from '../../../services/ml/security_common'; +import { USER } from '../../../../services/ml/security_common'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -117,12 +115,8 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - const uploadFilePath = path.join( - __dirname, - '..', - 'data_visualizer', - 'files_to_import', - 'artificial_server_log' + const uploadFilePath = require.resolve( + '../../data_visualizer/files_to_import/artificial_server_log' ); const expectedUploadFileTitle = 'artificial_server_log'; diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/advanced_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/advanced_job.ts index 1fc4c87619f18d..ba0d030cfcf6f9 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/advanced_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; interface Detector { identifier: string; diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/aggregated_scripted_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/aggregated_scripted_job.ts index c47171c1cd75a2..78974ecf1e64ca 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/aggregated_scripted_job.ts @@ -6,7 +6,7 @@ */ import { Datafeed, Job } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/annotations.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/annotations.ts index 3b36701e651a7d..17c576281835a9 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/annotations.ts @@ -6,7 +6,7 @@ */ import { Annotation } from '@kbn/ml-plugin/common/types/annotations'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/anomaly_explorer.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/anomaly_explorer.ts index bf6fcd10a91523..c71f4a5789fd22 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/anomaly_explorer.ts @@ -6,7 +6,7 @@ */ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; // @ts-expect-error not full interface const JOB_CONFIG: Job = { diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/categorization_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/categorization_job.ts index 555040ef8e59e4..96c02f7827a587 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/categorization_job.ts @@ -6,7 +6,7 @@ */ import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '@kbn/ml-plugin/common/constants/categorization_job'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/custom_urls.ts similarity index 98% rename from x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/custom_urls.ts index a83f5d814c028f..1e6e020aff69c6 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/custom_urls.ts @@ -7,12 +7,12 @@ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; import { TIME_RANGE_TYPE } from '@kbn/ml-plugin/public/application/jobs/components/custom_url_editor/constants'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import type { DiscoverUrlConfig, DashboardUrlConfig, OtherUrlConfig, -} from '../../../services/ml/job_table'; +} from '../../../../services/ml/job_table'; // @ts-expect-error doesn't implement the full interface const JOB_CONFIG: Job = { diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/date_nanos_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/date_nanos_job.ts index 0401d137804036..4b593aacbebf11 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/date_nanos_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; interface Detector { identifier: string; diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/forecasts.ts similarity index 98% rename from x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/forecasts.ts index 964e0762d93220..b290789419ed88 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/forecasts.ts @@ -6,7 +6,7 @@ */ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; // @ts-expect-error not full interface const JOB_CONFIG: Job = { diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/index.ts similarity index 94% rename from x-pack/test/functional/apps/ml/anomaly_detection/index.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/index.ts index ed5f618f86644c..a1127c0e71c77c 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('anomaly detection', function () { diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/multi_metric_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/multi_metric_job.ts index 4786f51bdc414d..783312b0d8608b 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/multi_metric_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/population_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/population_job.ts index f522f5ebefd9a3..af2573e21f93de 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/population_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/saved_search_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/saved_search_job.ts index f314052035ff11..72dbac602cf8f1 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/saved_search_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job.ts index 8ec2e4a48e2286..e698dd270e1a87 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job_without_datafeed_start.ts similarity index 98% rename from x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job_without_datafeed_start.ts index 8d08fb84a8f55a..2afa284fcc3d75 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job_without_datafeed_start.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_viewer.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_viewer.ts index c0946713a25640..b970a0efe56023 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_viewer.ts @@ -6,7 +6,7 @@ */ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; // @ts-expect-error not full interface const JOB_CONFIG: Job = { diff --git a/x-pack/test/functional/apps/ml/group2/config.ts b/x-pack/test/functional/apps/ml/group2/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/ml/group2/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/ml/group2/index.ts b/x-pack/test/functional/apps/ml/group2/index.ts new file mode 100644 index 00000000000000..4515715327e055 --- /dev/null +++ b/x-pack/test/functional/apps/ml/group2/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning - group 2', () => { + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/event_rate_nanos'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/bm_classification'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/egs_regression'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); + + await ml.testResources.resetKibanaTimeZone(); + }); + + loadTestFile(require.resolve('./anomaly_detection')); + }); +} diff --git a/x-pack/test/functional/apps/ml/group3/config.ts b/x-pack/test/functional/apps/ml/group3/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/ml/group3/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts b/x-pack/test/functional/apps/ml/group3/embeddables/anomaly_charts_dashboard_embeddables.ts similarity index 98% rename from x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts rename to x-pack/test/functional/apps/ml/group3/embeddables/anomaly_charts_dashboard_embeddables.ts index 89b733c21498f4..68981de99fc9a8 100644 --- a/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts +++ b/x-pack/test/functional/apps/ml/group3/embeddables/anomaly_charts_dashboard_embeddables.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { JOB_CONFIG, DATAFEED_CONFIG, ML_EMBEDDABLE_TYPES } from './constants'; const testDataList = [ diff --git a/x-pack/test/functional/apps/ml/embeddables/anomaly_embeddables_migration.ts b/x-pack/test/functional/apps/ml/group3/embeddables/anomaly_embeddables_migration.ts similarity index 98% rename from x-pack/test/functional/apps/ml/embeddables/anomaly_embeddables_migration.ts rename to x-pack/test/functional/apps/ml/group3/embeddables/anomaly_embeddables_migration.ts index ed38ff7021a920..a4c50549f5aed2 100644 --- a/x-pack/test/functional/apps/ml/embeddables/anomaly_embeddables_migration.ts +++ b/x-pack/test/functional/apps/ml/group3/embeddables/anomaly_embeddables_migration.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { JOB_CONFIG, DATAFEED_CONFIG, ML_EMBEDDABLE_TYPES } from './constants'; const testDataList = [ diff --git a/x-pack/test/functional/apps/ml/embeddables/constants.ts b/x-pack/test/functional/apps/ml/group3/embeddables/constants.ts similarity index 100% rename from x-pack/test/functional/apps/ml/embeddables/constants.ts rename to x-pack/test/functional/apps/ml/group3/embeddables/constants.ts diff --git a/x-pack/test/functional/apps/ml/embeddables/index.ts b/x-pack/test/functional/apps/ml/group3/embeddables/index.ts similarity index 88% rename from x-pack/test/functional/apps/ml/embeddables/index.ts rename to x-pack/test/functional/apps/ml/group3/embeddables/index.ts index 31074a59866a60..d786491e55a4ef 100644 --- a/x-pack/test/functional/apps/ml/embeddables/index.ts +++ b/x-pack/test/functional/apps/ml/group3/embeddables/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('embeddables', function () { diff --git a/x-pack/test/functional/apps/ml/feature_controls/index.ts b/x-pack/test/functional/apps/ml/group3/feature_controls/index.ts similarity index 87% rename from x-pack/test/functional/apps/ml/feature_controls/index.ts rename to x-pack/test/functional/apps/ml/group3/feature_controls/index.ts index ffe419b506fd6a..ab0988c424761f 100644 --- a/x-pack/test/functional/apps/ml/feature_controls/index.ts +++ b/x-pack/test/functional/apps/ml/group3/feature_controls/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { diff --git a/x-pack/test/functional/apps/ml/feature_controls/ml_security.ts b/x-pack/test/functional/apps/ml/group3/feature_controls/ml_security.ts similarity index 98% rename from x-pack/test/functional/apps/ml/feature_controls/ml_security.ts rename to x-pack/test/functional/apps/ml/group3/feature_controls/ml_security.ts index 58af3abbf6a473..fd498f00a8262a 100644 --- a/x-pack/test/functional/apps/ml/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/ml/group3/feature_controls/ml_security.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const security = getService('security'); diff --git a/x-pack/test/functional/apps/ml/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/ml/group3/feature_controls/ml_spaces.ts similarity index 97% rename from x-pack/test/functional/apps/ml/feature_controls/ml_spaces.ts rename to x-pack/test/functional/apps/ml/group3/feature_controls/ml_spaces.ts index 1ea0e1e717e5fd..0352a0059ba554 100644 --- a/x-pack/test/functional/apps/ml/feature_controls/ml_spaces.ts +++ b/x-pack/test/functional/apps/ml/group3/feature_controls/ml_spaces.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); diff --git a/x-pack/test/functional/apps/ml/group3/index.ts b/x-pack/test/functional/apps/ml/group3/index.ts new file mode 100644 index 00000000000000..e85b95b274720d --- /dev/null +++ b/x-pack/test/functional/apps/ml/group3/index.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning - group 3', function () { + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/event_rate_nanos'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/bm_classification'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/egs_regression'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); + + await ml.testResources.resetKibanaTimeZone(); + }); + + loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./settings')); + loadTestFile(require.resolve('./embeddables')); + loadTestFile(require.resolve('./stack_management_jobs')); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/calendar_creation.ts b/x-pack/test/functional/apps/ml/group3/settings/calendar_creation.ts similarity index 98% rename from x-pack/test/functional/apps/ml/settings/calendar_creation.ts rename to x-pack/test/functional/apps/ml/group3/settings/calendar_creation.ts index f0f5cd71cafe73..91121528d477a4 100644 --- a/x-pack/test/functional/apps/ml/settings/calendar_creation.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/calendar_creation.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { asyncForEach, createJobConfig } from './common'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/functional/apps/ml/settings/calendar_delete.ts b/x-pack/test/functional/apps/ml/group3/settings/calendar_delete.ts similarity index 96% rename from x-pack/test/functional/apps/ml/settings/calendar_delete.ts rename to x-pack/test/functional/apps/ml/group3/settings/calendar_delete.ts index ff417a32c1d7c2..28b526147c96eb 100644 --- a/x-pack/test/functional/apps/ml/settings/calendar_delete.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/calendar_delete.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { asyncForEach } from './common'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/functional/apps/ml/settings/calendar_edit.ts b/x-pack/test/functional/apps/ml/group3/settings/calendar_edit.ts similarity index 98% rename from x-pack/test/functional/apps/ml/settings/calendar_edit.ts rename to x-pack/test/functional/apps/ml/group3/settings/calendar_edit.ts index 70c3e50ec309be..9f68ccc8a11964 100644 --- a/x-pack/test/functional/apps/ml/settings/calendar_edit.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/calendar_edit.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { asyncForEach, createJobConfig } from './common'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/functional/apps/ml/settings/common.ts b/x-pack/test/functional/apps/ml/group3/settings/common.ts similarity index 89% rename from x-pack/test/functional/apps/ml/settings/common.ts rename to x-pack/test/functional/apps/ml/group3/settings/common.ts index f161ae637e3ad5..77d798ecf241bc 100644 --- a/x-pack/test/functional/apps/ml/settings/common.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/common.ts @@ -5,7 +5,10 @@ * 2.0. */ -export async function asyncForEach(array: T[], callback: (item: T, index: number) => void) { +export async function asyncForEach( + array: T[], + callback: (item: T, index: number) => Promise +) { for (let index = 0; index < array.length; index++) { await callback(array[index], index); } diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts b/x-pack/test/functional/apps/ml/group3/settings/filter_list_creation.ts similarity index 96% rename from x-pack/test/functional/apps/ml/settings/filter_list_creation.ts rename to x-pack/test/functional/apps/ml/group3/settings/filter_list_creation.ts index 3938b73a131f4a..38ee8a3e6e4c2e 100644 --- a/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/filter_list_creation.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts b/x-pack/test/functional/apps/ml/group3/settings/filter_list_delete.ts similarity index 96% rename from x-pack/test/functional/apps/ml/settings/filter_list_delete.ts rename to x-pack/test/functional/apps/ml/group3/settings/filter_list_delete.ts index aad56ffe55606f..cdbf26ea12a03b 100644 --- a/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/filter_list_delete.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { asyncForEach } from './common'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts b/x-pack/test/functional/apps/ml/group3/settings/filter_list_edit.ts similarity index 97% rename from x-pack/test/functional/apps/ml/settings/filter_list_edit.ts rename to x-pack/test/functional/apps/ml/group3/settings/filter_list_edit.ts index 7acd1b2bbc123e..0a4c4f63ee7414 100644 --- a/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/filter_list_edit.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { asyncForEach } from './common'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/functional/apps/ml/settings/index.ts b/x-pack/test/functional/apps/ml/group3/settings/index.ts similarity index 91% rename from x-pack/test/functional/apps/ml/settings/index.ts rename to x-pack/test/functional/apps/ml/group3/settings/index.ts index e904eaedb8db00..9ac25b7fc9483b 100644 --- a/x-pack/test/functional/apps/ml/settings/index.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('settings', function () { diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/export_jobs.ts similarity index 99% rename from x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/export_jobs.ts index 85e249861378f2..4ced89e35d6088 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts +++ b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/export_jobs.ts @@ -7,7 +7,7 @@ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; import type { DataFrameAnalyticsConfig } from '@kbn/ml-plugin/public/application/data_frame_analytics/common'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const testADJobs: Array<{ job: Job; datafeed: Datafeed }> = [ { diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json similarity index 100% rename from x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/bad_data.json similarity index 100% rename from x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/bad_data.json diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json similarity index 100% rename from x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/import_jobs.ts similarity index 92% rename from x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/import_jobs.ts index ef367797ef7e91..212bb029b6e0bb 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts +++ b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/import_jobs.ts @@ -5,17 +5,15 @@ * 2.0. */ -import path from 'path'; - import { JobType } from '@kbn/ml-plugin/common/types/saved_objects'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); const testDataListPositive = [ { - filePath: path.join(__dirname, 'files_to_import', 'anomaly_detection_jobs_7.16.json'), + filePath: require.resolve('./files_to_import/anomaly_detection_jobs_7.16.json'), expected: { jobType: 'anomaly-detector' as JobType, jobIds: ['ad-test1', 'ad-test3'], @@ -23,7 +21,7 @@ export default function ({ getService }: FtrProviderContext) { }, }, { - filePath: path.join(__dirname, 'files_to_import', 'data_frame_analytics_jobs_7.16.json'), + filePath: require.resolve('./files_to_import/data_frame_analytics_jobs_7.16.json'), expected: { jobType: 'data-frame-analytics' as JobType, jobIds: ['dfa-test1'], @@ -107,7 +105,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('selects job import'); await ml.stackManagementJobs.openImportFlyout(); await ml.stackManagementJobs.selectFileToImport( - path.join(__dirname, 'files_to_import', 'bad_data.json'), + require.resolve('./files_to_import/bad_data.json'), true ); }); diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/index.ts similarity index 89% rename from x-pack/test/functional/apps/ml/stack_management_jobs/index.ts rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/index.ts index c5e0728266bab4..4c4bedfeb9b768 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts +++ b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('stack management jobs', function () { diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/manage_spaces.ts similarity index 99% rename from x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/manage_spaces.ts index 4841fca9d74f74..5563bb9043c7f6 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts +++ b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/manage_spaces.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const browser = getService('browser'); diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/synchronize.ts similarity index 98% rename from x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/synchronize.ts index cf22f29bc277f3..e760549b7a1516 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts +++ b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/synchronize.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/monitoring/config.ts b/x-pack/test/functional/apps/monitoring/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index b8f6f223092f6b..cfa29d2f784e6f 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -7,7 +7,6 @@ export default function ({ loadTestFile }) { describe('Monitoring app', function () { - this.tags('ciGroup28'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./cluster/list')); diff --git a/x-pack/test/functional/apps/remote_clusters/config.ts b/x-pack/test/functional/apps/remote_clusters/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/remote_clusters/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/remote_clusters/index.ts b/x-pack/test/functional/apps/remote_clusters/index.ts index a1cc660b6426a0..74c4ce6e68bfc2 100644 --- a/x-pack/test/functional/apps/remote_clusters/index.ts +++ b/x-pack/test/functional/apps/remote_clusters/index.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; // https://www.elastic.co/guide/en/kibana/7.9/working-remote-clusters.html export default ({ loadTestFile }: FtrProviderContext) => { describe('Remote Clusters app', function () { - this.tags(['ciGroup4', 'skipCloud']); + this.tags('skipCloud'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); diff --git a/x-pack/test/functional/apps/reporting/README.md b/x-pack/test/functional/apps/reporting/README.md index ec9bba8b883417..5d47804fef284d 100644 --- a/x-pack/test/functional/apps/reporting/README.md +++ b/x-pack/test/functional/apps/reporting/README.md @@ -5,7 +5,7 @@ Functional tests on report generation are under the applications that use report **PDF/PNG Report testing:** - `x-pack/test/functional/apps/canvas/reports.ts` - `x-pack/test/functional/apps/dashboard/reporting/screenshots.ts` - - `x-pack/test/functional/apps/lens/lens_reporting.ts` + - `x-pack/test/functional/apps/lens/group3/lens_reporting.ts` - `x-pack/test/functional/apps/visualize/reporting.ts` **CSV Report testing:** diff --git a/x-pack/test/functional/apps/reporting_management/config.ts b/x-pack/test/functional/apps/reporting_management/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/reporting_management/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/reporting_management/index.js b/x-pack/test/functional/apps/reporting_management/index.js index 4e6abe85a4041d..dcf5583eeb92a5 100644 --- a/x-pack/test/functional/apps/reporting_management/index.js +++ b/x-pack/test/functional/apps/reporting_management/index.js @@ -7,7 +7,6 @@ export default ({ loadTestFile }) => { describe('reporting management app', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./report_listing')); }); }; diff --git a/x-pack/test/functional/apps/rollup_job/config.ts b/x-pack/test/functional/apps/rollup_job/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/rollup_job/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/rollup_job/index.js b/x-pack/test/functional/apps/rollup_job/index.js index 8fa9cd6f7aa722..943536539c5ad9 100644 --- a/x-pack/test/functional/apps/rollup_job/index.js +++ b/x-pack/test/functional/apps/rollup_job/index.js @@ -7,8 +7,6 @@ export default function ({ loadTestFile }) { describe('rollup app', function () { - this.tags('ciGroup28'); - loadTestFile(require.resolve('./rollup_jobs')); loadTestFile(require.resolve('./hybrid_index_pattern')); loadTestFile(require.resolve('./tsvb')); diff --git a/x-pack/test/functional/apps/saved_objects_management/config.ts b/x-pack/test/functional/apps/saved_objects_management/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/saved_objects_management/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/saved_objects_management/index.ts b/x-pack/test/functional/apps/saved_objects_management/index.ts index 17cdae0707213c..dc0dae5134f501 100644 --- a/x-pack/test/functional/apps/saved_objects_management/index.ts +++ b/x-pack/test/functional/apps/saved_objects_management/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function savedObjectsManagementApp({ loadTestFile }: FtrProviderContext) { describe('Saved objects management', function savedObjectsManagementAppTestSuite() { - this.tags(['ciGroup2', 'skipFirefox']); + this.tags('skipFirefox'); loadTestFile(require.resolve('./spaces_integration')); loadTestFile(require.resolve('./feature_controls/saved_objects_management_security')); diff --git a/x-pack/test/functional/apps/security/basic_license/index.ts b/x-pack/test/functional/apps/security/basic_license/index.ts index 771874f853de47..04e55abb9bac17 100644 --- a/x-pack/test/functional/apps/security/basic_license/index.ts +++ b/x-pack/test/functional/apps/security/basic_license/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - basic license', function () { - this.tags('ciGroup4'); - loadTestFile(require.resolve('./role_mappings')); }); } diff --git a/x-pack/test/functional/apps/security/config.ts b/x-pack/test/functional/apps/security/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/security/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/security/index.ts b/x-pack/test/functional/apps/security/index.ts index fc9caafbabb293..3260e61e67cbf0 100644 --- a/x-pack/test/functional/apps/security/index.ts +++ b/x-pack/test/functional/apps/security/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app', function () { - this.tags('ciGroup7'); - loadTestFile(require.resolve('./security')); loadTestFile(require.resolve('./doc_level_security_roles')); loadTestFile(require.resolve('./management')); diff --git a/x-pack/test/functional/apps/snapshot_restore/config.ts b/x-pack/test/functional/apps/snapshot_restore/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/snapshot_restore/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/snapshot_restore/index.ts b/x-pack/test/functional/apps/snapshot_restore/index.ts index 95fc0b80c2c917..0eefd3884cd311 100644 --- a/x-pack/test/functional/apps/snapshot_restore/index.ts +++ b/x-pack/test/functional/apps/snapshot_restore/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Snapshots app', function () { - this.tags(['ciGroup4', 'skipCloud']); + this.tags('skipCloud'); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/spaces/config.ts b/x-pack/test/functional/apps/spaces/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/spaces/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/spaces/index.ts b/x-pack/test/functional/apps/spaces/index.ts index 780d355e1f5c6a..c951609d6a33f1 100644 --- a/x-pack/test/functional/apps/spaces/index.ts +++ b/x-pack/test/functional/apps/spaces/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function spacesApp({ loadTestFile }: FtrProviderContext) { describe('Spaces app', function spacesAppTestSuite() { - this.tags('ciGroup9'); - loadTestFile(require.resolve('./copy_saved_objects')); loadTestFile(require.resolve('./feature_controls/spaces_security')); loadTestFile(require.resolve('./spaces_selection')); diff --git a/x-pack/test/functional/apps/status_page/config.ts b/x-pack/test/functional/apps/status_page/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/status_page/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/status_page/index.ts b/x-pack/test/functional/apps/status_page/index.ts index 69b18984f0adeb..cdf6bb52ee6054 100644 --- a/x-pack/test/functional/apps/status_page/index.ts +++ b/x-pack/test/functional/apps/status_page/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function statusPage({ loadTestFile }: FtrProviderContext) { describe('Status page', function statusPageTestSuite() { - this.tags('ciGroup4'); - loadTestFile(require.resolve('./status_page')); }); } diff --git a/x-pack/test/functional/apps/transform/config.ts b/x-pack/test/functional/apps/transform/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/transform/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 181325e83b36b0..0c5227ae2f472c 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -16,7 +16,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const transform = getService('transform'); describe('transform', function () { - this.tags(['ciGroup21', 'transform']); + this.tags('transform'); before(async () => { await transform.securityCommon.createTransformRoles(); diff --git a/x-pack/test/functional/apps/upgrade_assistant/config.ts b/x-pack/test/functional/apps/upgrade_assistant/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/upgrade_assistant/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/upgrade_assistant/index.ts b/x-pack/test/functional/apps/upgrade_assistant/index.ts index d1ab46463e9307..fcb9dd4ba7ccae 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/index.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function upgradeCheckup({ loadTestFile }: FtrProviderContext) { describe('Upgrade Assistant', function upgradeAssistantTestSuite() { - this.tags('ciGroup4'); - loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./deprecation_pages')); loadTestFile(require.resolve('./overview_page')); diff --git a/x-pack/test/functional/apps/uptime/config.ts b/x-pack/test/functional/apps/uptime/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/uptime/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index cc0578602951a2..99359ee1265020 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -42,8 +42,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { const uptime = getService('uptime'); describe('Uptime app', function () { - this.tags('ciGroup10'); - beforeEach('delete settings', async () => { await deleteUptimeSettingsObject(server); }); diff --git a/x-pack/test/functional/apps/visualize/config.ts b/x-pack/test/functional/apps/visualize/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/visualize/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/visualize/index.ts b/x-pack/test/functional/apps/visualize/index.ts index 105303aa1b537f..c99182201eb5dc 100644 --- a/x-pack/test/functional/apps/visualize/index.ts +++ b/x-pack/test/functional/apps/visualize/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function visualize({ loadTestFile }: FtrProviderContext) { describe('Visualize', function visualizeTestSuite() { - this.tags(['ciGroup30', 'skipFirefox']); + this.tags('skipFirefox'); loadTestFile(require.resolve('./feature_controls/visualize_security')); loadTestFile(require.resolve('./feature_controls/visualize_spaces')); diff --git a/x-pack/test/functional/apps/watcher/config.ts b/x-pack/test/functional/apps/watcher/config.ts new file mode 100644 index 00000000000000..d0d07ff2002816 --- /dev/null +++ b/x-pack/test/functional/apps/watcher/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/watcher/index.js b/x-pack/test/functional/apps/watcher/index.js index fb39fe4aa7b293..fab662b26e80c3 100644 --- a/x-pack/test/functional/apps/watcher/index.js +++ b/x-pack/test/functional/apps/watcher/index.js @@ -7,7 +7,7 @@ export default function ({ loadTestFile }) { describe('watcher app', function () { - this.tags(['ciGroup28', 'includeFirefox']); + this.tags('includeFirefox'); loadTestFile(require.resolve('./watcher_test')); }); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.base.js similarity index 88% rename from x-pack/test/functional/config.js rename to x-pack/test/functional/config.base.js index 8d39f26d4569d0..b5b671a54744e8 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.base.js @@ -24,52 +24,10 @@ export default async function ({ readConfigFile }) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); return { - // list paths to the files that contain your plugins tests - testFiles: [ - resolve(__dirname, './apps/home'), - resolve(__dirname, './apps/advanced_settings'), - resolve(__dirname, './apps/canvas'), - resolve(__dirname, './apps/graph'), - resolve(__dirname, './apps/monitoring'), - resolve(__dirname, './apps/watcher'), - resolve(__dirname, './apps/dashboard'), - resolve(__dirname, './apps/discover'), - resolve(__dirname, './apps/security'), - resolve(__dirname, './apps/spaces'), - resolve(__dirname, './apps/logstash'), - resolve(__dirname, './apps/grok_debugger'), - resolve(__dirname, './apps/infra'), - resolve(__dirname, './apps/ml'), - resolve(__dirname, './apps/rollup_job'), - resolve(__dirname, './apps/maps'), - resolve(__dirname, './apps/status_page'), - resolve(__dirname, './apps/upgrade_assistant'), - resolve(__dirname, './apps/visualize'), - resolve(__dirname, './apps/uptime'), - resolve(__dirname, './apps/saved_objects_management'), - resolve(__dirname, './apps/dev_tools'), - resolve(__dirname, './apps/apm'), - resolve(__dirname, './apps/api_keys'), - resolve(__dirname, './apps/data_views'), - resolve(__dirname, './apps/index_management'), - resolve(__dirname, './apps/index_lifecycle_management'), - resolve(__dirname, './apps/ingest_pipelines'), - resolve(__dirname, './apps/snapshot_restore'), - resolve(__dirname, './apps/cross_cluster_replication'), - resolve(__dirname, './apps/remote_clusters'), - resolve(__dirname, './apps/transform'), - resolve(__dirname, './apps/reporting_management'), - resolve(__dirname, './apps/management'), - resolve(__dirname, './apps/lens'), // smokescreen tests cause flakiness in other tests - - // This license_management file must be last because it is destructive. - resolve(__dirname, './apps/license_management'), - ], - services, pageObjects, diff --git a/x-pack/test/functional/config.ccs.ts b/x-pack/test/functional/config.ccs.ts index e6e0da52931900..87bdb17e736bd8 100644 --- a/x-pack/test/functional/config.ccs.ts +++ b/x-pack/test/functional/config.ccs.ts @@ -10,12 +10,12 @@ import { RemoteEsArchiverProvider } from './services/remote_es/remote_es_archive import { RemoteEsProvider } from './services/remote_es/remote_es'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('./config')); + const functionalConfig = await readConfigFile(require.resolve('./config.base.js')); return { ...functionalConfig.getAll(), - testFiles: [require.resolve('./apps/lens')], + testFiles: [require.resolve('./apps/lens/group1')], junit: { reportName: 'X-Pack CCS Tests', diff --git a/x-pack/test/functional/config.coverage.js b/x-pack/test/functional/config.coverage.js deleted file mode 100644 index b403fe933c4a3e..00000000000000 --- a/x-pack/test/functional/config.coverage.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export default async function ({ readConfigFile }) { - const chromeConfig = await readConfigFile(require.resolve('./config')); - - return { - ...chromeConfig.getAll(), - - suiteTags: { - exclude: ['skipCoverage'], - }, - - junit: { - reportName: 'Code Coverage for Functional Tests', - }, - }; -} diff --git a/x-pack/test/functional/config.edge.js b/x-pack/test/functional/config.edge.js index b51b09d0f48788..58f35e844e2830 100644 --- a/x-pack/test/functional/config.edge.js +++ b/x-pack/test/functional/config.edge.js @@ -6,10 +6,10 @@ */ export default async function ({ readConfigFile }) { - const chromeConfig = await readConfigFile(require.resolve('./config')); + const firefoxConfig = await readConfigFile(require.resolve('./config.firefox.js')); return { - ...chromeConfig.getAll(), + ...firefoxConfig.getAll(), browser: { type: 'msedge', diff --git a/x-pack/test/functional/config.firefox.js b/x-pack/test/functional/config.firefox.js index 148f3c454a859e..91249df789de83 100644 --- a/x-pack/test/functional/config.firefox.js +++ b/x-pack/test/functional/config.firefox.js @@ -6,16 +6,26 @@ */ export default async function ({ readConfigFile }) { - const chromeConfig = await readConfigFile(require.resolve('./config')); + const chromeConfig = await readConfigFile(require.resolve('./config.base.js')); return { ...chromeConfig.getAll(), + testFiles: [ + require.resolve('./apps/canvas'), + require.resolve('./apps/infra'), + require.resolve('./apps/security'), + require.resolve('./apps/spaces'), + require.resolve('./apps/status_page'), + require.resolve('./apps/watcher'), + ], + browser: { type: 'firefox', }, suiteTags: { + include: ['includeFirefox'], exclude: ['skipFirefox'], }, diff --git a/x-pack/test/functional/config_security_basic.ts b/x-pack/test/functional/config_security_basic.ts index dc4bfc437347eb..d56b91d45a63e0 100644 --- a/x-pack/test/functional/config_security_basic.ts +++ b/x-pack/test/functional/config_security_basic.ts @@ -20,7 +20,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); return { diff --git a/x-pack/test/functional/services/ml/common_data_grid.ts b/x-pack/test/functional/services/ml/common_data_grid.ts index c48cf92107dab2..444b01cce902f3 100644 --- a/x-pack/test/functional/services/ml/common_data_grid.ts +++ b/x-pack/test/functional/services/ml/common_data_grid.ts @@ -8,8 +8,8 @@ import expect from '@kbn/expect'; import { chunk } from 'lodash'; import type { ProvidedType } from '@kbn/test'; +import { asyncForEachWithLimit } from '@kbn/std'; import type { FtrProviderContext } from '../../ftr_provider_context'; -import { asyncForEach } from '../../apps/ml/settings/common'; export interface SetValueOptions { clearWithKeyboard?: boolean; @@ -154,7 +154,7 @@ export function MachineLearningCommonDataGridProvider({ getService }: FtrProvide await find.byClassName('euiDataGrid__controlScroll') ).findAllByCssSelector('[role="switch"]'); - await asyncForEach(visibilityToggles, async (toggle) => { + await asyncForEachWithLimit(visibilityToggles, 1, async (toggle) => { const checked = (await toggle.getAttribute('aria-checked')) === 'true'; expect(checked).to.eql( expectedState, diff --git a/x-pack/test/functional_basic/apps/ml/index.ts b/x-pack/test/functional_basic/apps/ml/index.ts index af2fdc8c45f297..0188aa0361d942 100644 --- a/x-pack/test/functional_basic/apps/ml/index.ts +++ b/x-pack/test/functional_basic/apps/ml/index.ts @@ -12,7 +12,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); describe('machine learning basic license', function () { - this.tags(['ciGroup14', 'skipFirefox', 'mlqa']); + this.tags(['skipFirefox', 'mlqa']); before(async () => { await ml.securityCommon.createMlRoles(); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts index b44c5f08bdbc64..b90b97ca87a573 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts @@ -5,8 +5,6 @@ * 2.0. */ -import path from 'path'; - import { FtrProviderContext } from '../../../ftr_provider_context'; import { USER } from '../../../../functional/services/ml/security_common'; @@ -26,18 +24,8 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - const uploadFilePath = path.join( - __dirname, - '..', - '..', - '..', - '..', - 'functional', - 'apps', - 'ml', - 'data_visualizer', - 'files_to_import', - 'artificial_server_log' + const uploadFilePath = require.resolve( + '../../../../functional/apps/ml/data_visualizer/files_to_import/artificial_server_log' ); const expectedUploadFileTitle = 'artificial_server_log'; diff --git a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts index c1b13d6dc1f112..fc293defceb866 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts @@ -5,8 +5,6 @@ * 2.0. */ -import path from 'path'; - import { FtrProviderContext } from '../../../ftr_provider_context'; import { USER } from '../../../../functional/services/ml/security_common'; @@ -26,18 +24,8 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - const uploadFilePath = path.join( - __dirname, - '..', - '..', - '..', - '..', - 'functional', - 'apps', - 'ml', - 'data_visualizer', - 'files_to_import', - 'artificial_server_log' + const uploadFilePath = require.resolve( + '../../../../functional/apps/ml/data_visualizer/files_to_import/artificial_server_log' ); const expectedUploadFileTitle = 'artificial_server_log'; diff --git a/x-pack/test/functional_basic/config.ts b/x-pack/test/functional_basic/config.ts index e1dac88436e4c2..f35ece0ce5d165 100644 --- a/x-pack/test/functional_basic/config.ts +++ b/x-pack/test/functional_basic/config.ts @@ -8,7 +8,9 @@ import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { // default to the xpack functional config diff --git a/x-pack/test/functional_cors/config.ts b/x-pack/test/functional_cors/config.ts index 738285b4ff40f4..364be1383ae94c 100644 --- a/x-pack/test/functional_cors/config.ts +++ b/x-pack/test/functional_cors/config.ts @@ -16,7 +16,9 @@ const pluginPort = process.env.TEST_CORS_SERVER_PORT : 5699; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const kibanaFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); const corsTestPlugin = Path.resolve(__dirname, './plugins/kibana_cors_test'); diff --git a/x-pack/test/functional_cors/tests/index.ts b/x-pack/test/functional_cors/tests/index.ts index 424dac86c4f1a1..3ca455eccd339d 100644 --- a/x-pack/test/functional_cors/tests/index.ts +++ b/x-pack/test/functional_cors/tests/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Kibana cors', function () { - this.tags('ciGroup12'); loadTestFile(require.resolve('./cors')); }); } diff --git a/x-pack/test/functional_embedded/config.ts b/x-pack/test/functional_embedded/config.ts index 868d53ee17ee9e..cdbbffea0eab98 100644 --- a/x-pack/test/functional_embedded/config.ts +++ b/x-pack/test/functional_embedded/config.ts @@ -12,7 +12,9 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const kibanaFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); const iframeEmbeddedPlugin = resolve(__dirname, './plugins/iframe_embedded'); diff --git a/x-pack/test/functional_embedded/tests/index.ts b/x-pack/test/functional_embedded/tests/index.ts index aa210aff197287..1c3f01febd6d4b 100644 --- a/x-pack/test/functional_embedded/tests/index.ts +++ b/x-pack/test/functional_embedded/tests/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Kibana embedded', function () { - this.tags('ciGroup2'); loadTestFile(require.resolve('./iframe_embedded')); }); } diff --git a/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/index.ts b/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/index.ts index 653f0842ef1bbc..dda2e207453947 100644 --- a/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/index.ts +++ b/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Enterprise Search', function () { - this.tags('ciGroup10'); - loadTestFile(require.resolve('./app_search/setup_guide')); loadTestFile(require.resolve('./workplace_search/setup_guide')); }); diff --git a/x-pack/test/functional_enterprise_search/base_config.ts b/x-pack/test/functional_enterprise_search/base_config.ts index 2c21ccf5c5c390..8f72e1ebd65035 100644 --- a/x-pack/test/functional_enterprise_search/base_config.ts +++ b/x-pack/test/functional_enterprise_search/base_config.ts @@ -10,7 +10,9 @@ import { pageObjects } from './page_objects'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xPackFunctionalConfig = await readConfigFile(require.resolve('../functional/config')); + const xPackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { // default to the xpack functional config diff --git a/x-pack/test/functional_execution_context/config.ts b/x-pack/test/functional_execution_context/config.ts index 6169df6adcef30..caf88769b6a065 100644 --- a/x-pack/test/functional_execution_context/config.ts +++ b/x-pack/test/functional_execution_context/config.ts @@ -12,7 +12,7 @@ import { logFilePath } from './test_utils'; const alertTestPlugin = Path.resolve(__dirname, './fixtures/plugins/alerts'); export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); const servers = { ...functionalConfig.get('servers'), diff --git a/x-pack/test/functional_execution_context/tests/index.ts b/x-pack/test/functional_execution_context/tests/index.ts index c092be9bd8bdbe..2c34783a9bae30 100644 --- a/x-pack/test/functional_execution_context/tests/index.ts +++ b/x-pack/test/functional_execution_context/tests/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Execution context', function () { - this.tags('ciGroup1'); loadTestFile(require.resolve('./browser')); loadTestFile(require.resolve('./server')); loadTestFile(require.resolve('./log_correlation')); diff --git a/x-pack/test/functional_synthetics/apps/uptime/index.ts b/x-pack/test/functional_synthetics/apps/uptime/index.ts index 925731c769bfc1..64a9da5c30ea33 100644 --- a/x-pack/test/functional_synthetics/apps/uptime/index.ts +++ b/x-pack/test/functional_synthetics/apps/uptime/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile, getService }: FtrProviderContext) => { describe('Uptime app', function () { - this.tags('ciGroup8'); describe('with generated data', () => { loadTestFile(require.resolve('./synthetics_integration')); }); diff --git a/x-pack/test/functional_synthetics/config.js b/x-pack/test/functional_synthetics/config.js index 20488e3c52b2cf..f5db09a5d60d9c 100644 --- a/x-pack/test/functional_synthetics/config.js +++ b/x-pack/test/functional_synthetics/config.js @@ -28,7 +28,7 @@ export default async function ({ readConfigFile }) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); // mount the config file for the package registry as well as diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/index.ts b/x-pack/test/functional_with_es_ssl/apps/cases/index.ts index 53d2c2d9767f18..04cceba32c8a76 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Cases', function () { - this.tags('ciGroup27'); loadTestFile(require.resolve('./create_case_form')); loadTestFile(require.resolve('./view_case')); loadTestFile(require.resolve('./list_view')); diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/index.ts b/x-pack/test/functional_with_es_ssl/apps/discover/index.ts index 708da2f02da74f..f876140dbc92ab 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/index.ts @@ -8,7 +8,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile, getService }: FtrProviderContext) => { describe('Discover alerting', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./search_source_alert')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts index 9a486d4983d9cb..4676004d9eb894 100644 --- a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts @@ -68,8 +68,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { let testJobId = ''; describe('anomaly detection alert', function () { - this.tags('ciGroup13'); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index 9c57f29c6f7073..73b084c2ce0e4d 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile, getService }: FtrProviderContext) => { describe('Actions and Triggers app', function () { - this.tags('ciGroup10'); loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./alerts_list')); loadTestFile(require.resolve('./alert_create_flyout')); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts index d2078267bde855..2c39ef045972f3 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts @@ -14,8 +14,6 @@ export default ({ getService, loadTestFile }: FtrProviderContext) => { const kibanaServer = getService('kibanaServer'); describe('Uptime app', function () { - this.tags('ciGroup6'); - describe('with real-world data', () => { before(async () => { await esArchiver.load(ARCHIVE); diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 4783ad683c0cf9..4872d2fd6fa38b 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -31,7 +31,9 @@ const enabledActionTypes = [ ]; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); const servers = { ...xpackFunctionalConfig.get('servers'), diff --git a/x-pack/test/licensing_plugin/config.ts b/x-pack/test/licensing_plugin/config.ts index 155d761020b29f..c4b197c10a8245 100644 --- a/x-pack/test/licensing_plugin/config.ts +++ b/x-pack/test/licensing_plugin/config.ts @@ -11,7 +11,9 @@ import { services, pageObjects } from './services'; const license = 'basic'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalTestsConfig = await readConfigFile(require.resolve('../functional/config.js')); + const functionalTestsConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); const servers = { ...functionalTestsConfig.get('servers'), diff --git a/x-pack/test/licensing_plugin/public/index.ts b/x-pack/test/licensing_plugin/public/index.ts index 194db6266b5109..904b9eaecd7579 100644 --- a/x-pack/test/licensing_plugin/public/index.ts +++ b/x-pack/test/licensing_plugin/public/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../services'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile }: FtrProviderContext) { describe('Licensing plugin public client', function () { - this.tags('ciGroup5'); loadTestFile(require.resolve('./feature_usage')); // MUST BE LAST! CHANGES LICENSE TYPE! loadTestFile(require.resolve('./updates')); diff --git a/x-pack/test/licensing_plugin/server/index.ts b/x-pack/test/licensing_plugin/server/index.ts index 619d29a4a2fd2c..28426eba962b83 100644 --- a/x-pack/test/licensing_plugin/server/index.ts +++ b/x-pack/test/licensing_plugin/server/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../services'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile }: FtrProviderContext) { describe('Licensing plugin server client', function () { - this.tags('ciGroup13'); loadTestFile(require.resolve('./info')); loadTestFile(require.resolve('./header')); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts index c28447ef0ac18e..95cce7e7fb85fd 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('lists api security and spaces enabled', function () { - this.tags('ciGroup1'); - loadTestFile(require.resolve('./create_lists')); loadTestFile(require.resolve('./create_list_items')); loadTestFile(require.resolve('./read_lists')); diff --git a/x-pack/test/load/config.ts b/x-pack/test/load/config.ts index 2d20806f3e9e86..dcaa2031c9c021 100644 --- a/x-pack/test/load/config.ts +++ b/x-pack/test/load/config.ts @@ -21,7 +21,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); return { diff --git a/x-pack/test/observability_api_integration/basic/tests/index.ts b/x-pack/test/observability_api_integration/basic/tests/index.ts index c62cf4be0d7c7a..a1ea41e2b7c36f 100644 --- a/x-pack/test/observability_api_integration/basic/tests/index.ts +++ b/x-pack/test/observability_api_integration/basic/tests/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function observabilityApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('Observability specs (basic)', function () { - this.tags('ciGroup1'); loadTestFile(require.resolve('./annotations')); }); } diff --git a/x-pack/test/observability_api_integration/trial/tests/index.ts b/x-pack/test/observability_api_integration/trial/tests/index.ts index 037cf48275de28..e426efd90188ce 100644 --- a/x-pack/test/observability_api_integration/trial/tests/index.ts +++ b/x-pack/test/observability_api_integration/trial/tests/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('Observability specs (trial)', function () { - this.tags('ciGroup1'); loadTestFile(require.resolve('./annotations')); }); } diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts index 9ec3791aef35fc..d60f93f1285ada 100644 --- a/x-pack/test/observability_functional/apps/observability/index.ts +++ b/x-pack/test/observability_functional/apps/observability/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('ObservabilityApp', function () { - this.tags('ciGroup22'); - loadTestFile(require.resolve('./alerts')); loadTestFile(require.resolve('./alerts/add_to_case')); loadTestFile(require.resolve('./alerts/alert_disclaimer')); diff --git a/x-pack/test/observability_functional/with_rac_write.config.ts b/x-pack/test/observability_functional/with_rac_write.config.ts index 71a1de1df6a77e..bc5b39358fedb3 100644 --- a/x-pack/test/observability_functional/with_rac_write.config.ts +++ b/x-pack/test/observability_functional/with_rac_write.config.ts @@ -27,7 +27,9 @@ const enabledActionTypes = [ ]; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); const servers = { ...xpackFunctionalConfig.get('servers'), diff --git a/x-pack/test/osquery_cypress/config.ts b/x-pack/test/osquery_cypress/config.ts index 2bd39acfa13594..37f7b3f63b36cc 100644 --- a/x-pack/test/osquery_cypress/config.ts +++ b/x-pack/test/osquery_cypress/config.ts @@ -14,7 +14,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); return { diff --git a/x-pack/test/performance/config.playwright.ts b/x-pack/test/performance/config.playwright.ts index 9077c58a30e154..0b404d5c03bdb1 100644 --- a/x-pack/test/performance/config.playwright.ts +++ b/x-pack/test/performance/config.playwright.ts @@ -15,7 +15,7 @@ const APM_SERVER_URL = 'https://kibana-ops-e2e-perf.apm.us-central1.gcp.cloud.es const APM_PUBLIC_TOKEN = 'CTs9y3cvcfq13bQqsB'; export default async function ({ readConfigFile, log }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); const testFiles = [require.resolve('./tests/playwright')]; diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/index.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/index.ts index c29367fb852abb..0901c96f522ccf 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('event_log', function taskManagerSuite() { - this.tags('ciGroup6'); loadTestFile(require.resolve('./public_api_integration')); loadTestFile(require.resolve('./service_api_integration')); }); diff --git a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts index 7f2a4c12a26eb0..6ee46b58c4bcd9 100644 --- a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Licensed feature usage APIs', function () { - this.tags('ciGroup13'); loadTestFile(require.resolve('./feature_usage')); }); } diff --git a/x-pack/test/plugin_api_integration/test_suites/platform/index.ts b/x-pack/test/plugin_api_integration/test_suites/platform/index.ts index 46c468e9b6d789..907ebfe6bdf798 100644 --- a/x-pack/test/plugin_api_integration/test_suites/platform/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/platform/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('platform', function taskManagerSuite() { - this.tags('ciGroup13'); loadTestFile(require.resolve('./elasticsearch_client')); }); } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts index fe494ac33d461b..27120690085982 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('task_manager', function taskManagerSuite() { - this.tags('ciGroup12'); loadTestFile(require.resolve('./health_route')); loadTestFile(require.resolve('./task_management')); loadTestFile(require.resolve('./task_management_scheduled_at')); diff --git a/x-pack/test/plugin_api_perf/README.md b/x-pack/test/plugin_api_perf/README.md index f47a2aeb7878a2..2ae7c7d2013287 100644 --- a/x-pack/test/plugin_api_perf/README.md +++ b/x-pack/test/plugin_api_perf/README.md @@ -71,7 +71,7 @@ Ideally we can clean this up and make it easier and less hacky in the future, bu 1. You can run the FTS in the main clone of your fork by running `node scripts/functional_tests_server.js --config=test/plugin_api_perf/config.js` in the `x-pack` folder. 1. Once you've began running the default FTS, you want your second FTS to run such that it is referencing the Elasticsearch instance started by that first FTS. You achieve this by exporting a `TEST_ES_URL` Environment variable that points at it. By default, you should be able to run this: `export TEST_ES_URL=http://elastic:changeme@localhost:9220`. Do this in a terminal window opened in your **second** clone of Kibana (in my case, the `./elastic/_kibana` folder). 1. One issue I encountered with FTS is that I can't tell it _not to start its own ES instance at all_. To achieve this, in `packages/kbn-test/src/functional_tests/tasks.js` you need to comment out the line that starts up its own ES (`const es = await runElasticsearch({ config, options: opts });` [line 85] and `await es.cleanup();` shortly after) -1. Next you want each instance of Kibana to run with its own UUID as that is used to identify each Kibana's owned tasks. In the file `x-pack/test/functional/config.js` simple change the uuid on the line `--server.uuid=` into any random UUID. +1. Next you want each instance of Kibana to run with its own UUID as that is used to identify each Kibana's owned tasks. In the file `x-pack/test/functional/config.base.js` simple change the uuid on the line `--server.uuid=` into any random UUID. 1. Now that you've made these changes you can kick off your second Kibana FTS by running ths following in the second clone's `x-pack` folder: `TEST_KIBANA_PORT=5621 node scripts/functional_tests_server.js --config=test/plugin_api_perf/config.js`. This runs Kibana on a different port than the first FTS (`5621` instead of `5620`). 1. With two FTS Kibana running and both pointing at the same Elasticsearch. Now, you can run the actual perf test by running `node scripts/functional_test_runner.js --config=test/plugin_api_perf/config.js` in a third terminal diff --git a/x-pack/test/plugin_api_perf/test_suites/task_manager/index.ts b/x-pack/test/plugin_api_perf/test_suites/task_manager/index.ts index 703c97e4f2c637..9055a2d9e023c4 100644 --- a/x-pack/test/plugin_api_perf/test_suites/task_manager/index.ts +++ b/x-pack/test/plugin_api_perf/test_suites/task_manager/index.ts @@ -15,7 +15,6 @@ export default function ({ loadTestFile }: { loadTestFile: (file: string) => voi * worth keeping around for future use, rather than being rewritten time and time again. */ describe.skip('task_manager_perf', function taskManagerSuite() { - this.tags('ciGroup12'); loadTestFile(require.resolve('./task_manager_perf_integration')); }); } diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index 8f3c5be04a8bc3..a21b8f406e5065 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -17,7 +17,9 @@ import { pageObjects } from './page_objects'; // that returns an object with the projects config values export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(resolve(__dirname, 'plugins')); diff --git a/x-pack/test/plugin_functional/test_suites/global_search/index.ts b/x-pack/test/plugin_functional/test_suites/global_search/index.ts index a87d9c5e4d5039..651bb2b9039243 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/index.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('GlobalSearch API', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./global_search_providers')); loadTestFile(require.resolve('./global_search_bar')); }); diff --git a/x-pack/test/plugin_functional/test_suites/resolver/index.ts b/x-pack/test/plugin_functional/test_suites/resolver/index.ts index 688ba536b12325..3c43683b6bf7ad 100644 --- a/x-pack/test/plugin_functional/test_suites/resolver/index.ts +++ b/x-pack/test/plugin_functional/test_suites/resolver/index.ts @@ -29,8 +29,6 @@ export default function ({ // FLAKY: https://github.com/elastic/kibana/issues/87425 describe('Resolver test app', function () { - this.tags('ciGroup7'); - // Note: these tests are intended to run on the same page in serial. before(async function () { await pageObjects.common.navigateToApp('resolverTest'); diff --git a/x-pack/test/plugin_functional/test_suites/timelines/index.ts b/x-pack/test/plugin_functional/test_suites/timelines/index.ts index 2ca8d81132ab3e..955966eab12c06 100644 --- a/x-pack/test/plugin_functional/test_suites/timelines/index.ts +++ b/x-pack/test/plugin_functional/test_suites/timelines/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Timelines plugin API', function () { - this.tags('ciGroup7'); const pageObjects = getPageObjects(['common']); const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts index 4cff15dc9f4443..808c813145b843 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('Reporting APIs', function () { - this.tags('ciGroup2'); - before(async () => { const reportingAPI = getService('reportingAPI'); await reportingAPI.logTaskManagerHealth(); diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/index.ts b/x-pack/test/reporting_api_integration/reporting_without_security/index.ts index 72cfc369475175..19f96aa5d2869f 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security/index.ts @@ -14,7 +14,7 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { const reportingAPI = getService('reportingAPI'); await reportingAPI.logTaskManagerHealth(); }); - this.tags('ciGroup13'); + loadTestFile(require.resolve('./job_apis_csv')); }); } diff --git a/x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts b/x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts index 4725cb1eae82e3..722b6545115cda 100644 --- a/x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts +++ b/x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts @@ -43,8 +43,6 @@ export default function (context: FtrProviderContext) { }; describe('Reporting Functional Tests with Deprecated Security configuration enabled', function () { - this.tags('ciGroup20'); - before(async () => { const reportingAPI = context.getService('reportingAPI'); await reportingAPI.logTaskManagerHealth(); diff --git a/x-pack/test/reporting_functional/reporting_and_security.config.ts b/x-pack/test/reporting_functional/reporting_and_security.config.ts index 3037aeacde0334..7d8c3ed6966963 100644 --- a/x-pack/test/reporting_functional/reporting_and_security.config.ts +++ b/x-pack/test/reporting_functional/reporting_and_security.config.ts @@ -11,7 +11,7 @@ import { ReportingAPIProvider } from '../reporting_api_integration/services'; import { ReportingFunctionalProvider } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); // Reporting API tests need a fully working UI + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); // Reporting API tests need a fully working UI const apiConfig = await readConfigFile(require.resolve('../api_integration/config')); return { diff --git a/x-pack/test/reporting_functional/reporting_and_security/index.ts b/x-pack/test/reporting_functional/reporting_and_security/index.ts index 4b06eb426389e2..ec7afea96f1945 100644 --- a/x-pack/test/reporting_functional/reporting_and_security/index.ts +++ b/x-pack/test/reporting_functional/reporting_and_security/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('Reporting Functional Tests with Security enabled', function () { - this.tags('ciGroup20'); - before(async () => { const reportingFunctional = getService('reportingFunctional'); await reportingFunctional.logTaskManagerHealth(); diff --git a/x-pack/test/reporting_functional/reporting_without_security/index.ts b/x-pack/test/reporting_functional/reporting_without_security/index.ts index fecc0e97daac04..cc07e97a9c3a70 100644 --- a/x-pack/test/reporting_functional/reporting_without_security/index.ts +++ b/x-pack/test/reporting_functional/reporting_without_security/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { describe('Reporting Functional Tests with Security disabled', function () { - this.tags('ciGroup2'); - before(async () => { const reportingAPI = getService('reportingAPI'); await reportingAPI.logTaskManagerHealth(); diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts index ad63d6d1c7ef5f..d010cbfce91504 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts @@ -11,9 +11,6 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('rules security and spaces enabled: basic', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpacesAndUsers(getService); }); diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts index 1f5ec04cb8ffb4..dfda18b5a0c059 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts @@ -19,7 +19,7 @@ import { waitForSignalsToBePresent, waitForRuleSuccessOrStatus, } from '../../../../detection_engine_api_integration/utils'; -import { ID } from '../../../../detection_engine_api_integration/security_and_spaces/tests/generating_signals'; +import { ID } from '../../../../detection_engine_api_integration/security_and_spaces/group1/generating_signals'; import { obsOnlySpacesAllEsRead, obsOnlySpacesAll, diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts index 3e13d64b936a46..53a788f6c78299 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts @@ -39,9 +39,6 @@ import { export default ({ loadTestFile, getService }: FtrProviderContext): void => { // FAILING: https://github.com/elastic/kibana/issues/110153 describe.skip('rules security and spaces enabled: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpaces(getService); await createUsersAndRoles( diff --git a/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts b/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts index aeb2b085ad3796..f47b4b6254ff21 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts @@ -11,9 +11,6 @@ import { createSpaces, deleteSpaces } from '../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('rule registry spaces only: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpaces(getService); }); diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts index 19e35019eb50a9..d519dd16dab45b 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts @@ -11,9 +11,6 @@ import { createSpaces, deleteSpaces } from '../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('rule registry spaces only: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpaces(getService); }); 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 8ca74c7fcea49d..32d2d73d5cebca 100644 --- a/x-pack/test/saved_object_api_integration/common/config.ts +++ b/x-pack/test/saved_object_api_integration/common/config.ts @@ -24,7 +24,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) const config = { kibana: { api: await readConfigFile(path.resolve(REPO_ROOT, 'test/api_integration/config.js')), - functional: await readConfigFile(require.resolve('../../../../test/functional/config.js')), + functional: await readConfigFile( + require.resolve('../../../../test/functional/config.base.js') + ), }, xpack: { api: await readConfigFile(require.resolve('../../api_integration/config.ts')), 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 740b9d91927bf6..4eb0b904803148 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 @@ -13,8 +13,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const supertest = getService('supertest'); describe('saved objects security and spaces enabled', function () { - this.tags('ciGroup20'); - before(async () => { await createUsersAndRoles(es, supertest); }); 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 c6bdbde07fc02c..1be7ed754a9715 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 @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('saved objects spaces only enabled', function () { - this.tags('ciGroup5'); - loadTestFile(require.resolve('./bulk_create')); loadTestFile(require.resolve('./bulk_get')); loadTestFile(require.resolve('./bulk_resolve')); diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts index 54740e73aba652..f28b3cd6158878 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts @@ -11,8 +11,6 @@ import { createUsersAndRoles } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('saved objects tagging API - security and spaces integration', function () { - this.tags('ciGroup10'); - before(async () => { await createUsersAndRoles(getService); }); diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts index 3d5b0b9c3b989d..f291d2537ed029 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../services'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile }: FtrProviderContext) { describe('saved objects tagging API', function () { - this.tags('ciGroup12'); - loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./update')); diff --git a/x-pack/test/saved_object_tagging/functional/config.ts b/x-pack/test/saved_object_tagging/functional/config.ts index 6ad1f05e2be5b8..1c40864f2fa02c 100644 --- a/x-pack/test/saved_object_tagging/functional/config.ts +++ b/x-pack/test/saved_object_tagging/functional/config.ts @@ -11,7 +11,7 @@ import { services, pageObjects } from './ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../functional/config.js') + require.resolve('../../functional/config.base.js') ); return { diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts index fbf0954382dd1d..2d79d0a7a45ec2 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/index.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts @@ -11,8 +11,6 @@ import { createUsersAndRoles } from '../../common/lib'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { describe('saved objects tagging - functional tests', function () { - this.tags('ciGroup14'); - before(async () => { await createUsersAndRoles(getService); }); diff --git a/x-pack/test/saved_objects_field_count/config.ts b/x-pack/test/saved_objects_field_count/config.ts index 7967b6c4f3b9c8..ab5154adb8d590 100644 --- a/x-pack/test/saved_objects_field_count/config.ts +++ b/x-pack/test/saved_objects_field_count/config.ts @@ -6,7 +6,6 @@ */ import { FtrConfigProviderContext } from '@kbn/test'; -import { testRunner } from './runner'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaCommonTestsConfig = await readConfigFile( @@ -16,7 +15,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...kibanaCommonTestsConfig.getAll(), - testRunner, + testFiles: [require.resolve('./test')], esTestCluster: { license: 'trial', @@ -28,5 +27,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...kibanaCommonTestsConfig.get('kbnTestServer'), serverArgs: [...kibanaCommonTestsConfig.get('kbnTestServer.serverArgs')], }, + + junit: { + reportName: 'Saved Object Field Count', + }, }; } diff --git a/x-pack/test/saved_objects_field_count/runner.ts b/x-pack/test/saved_objects_field_count/runner.ts deleted file mode 100644 index b88f2129ba64d2..00000000000000 --- a/x-pack/test/saved_objects_field_count/runner.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CiStatsReporter } from '@kbn/ci-stats-reporter'; -import { FtrProviderContext } from '../functional/ftr_provider_context'; - -const IGNORED_FIELDS = [ - // The following fields are returned by the _field_caps API but aren't counted - // towards the index field limit. - '_seq_no', - '_id', - '_version', - '_field_names', - '_ignored', - '_feature', - '_index', - '_routing', - '_source', - '_type', - '_nested_path', - '_timestamp', - // migrationVersion is dynamic so will be anywhere between 1..type count - // depending on which objects are present in the index when querying the - // field caps API. See https://github.com/elastic/kibana/issues/70815 - 'migrationVersion', -]; - -export async function testRunner({ getService }: FtrProviderContext) { - const log = getService('log'); - const es = getService('es'); - - const reporter = CiStatsReporter.fromEnv(log); - - log.debug('Saved Objects field count metrics starting'); - - const { fields } = await es.fieldCaps({ - index: '.kibana', - fields: '*', - }); - - const fieldCountPerTypeMap: Map = Object.keys(fields) - .map((f) => f.split('.')[0]) - .filter((f) => !IGNORED_FIELDS.includes(f)) - .reduce((accumulator, f) => { - accumulator.set(f, accumulator.get(f) + 1 || 1); - return accumulator; - }, new Map()); - - const metrics = Array.from(fieldCountPerTypeMap.entries()) - .sort((a, b) => a[0].localeCompare(b[0])) - .map(([fieldType, count]) => ({ - group: 'Saved Objects .kibana field count', - id: fieldType, - value: count, - })); - - log.debug( - 'Saved Objects field count metrics:\n', - metrics.map(({ id, value }) => `${id}:${value}`).join('\n') - ); - await reporter.metrics(metrics); - log.debug('Saved Objects field count metrics done'); -} diff --git a/x-pack/test/saved_objects_field_count/test.ts b/x-pack/test/saved_objects_field_count/test.ts new file mode 100644 index 00000000000000..e931b1aa5ef265 --- /dev/null +++ b/x-pack/test/saved_objects_field_count/test.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CiStatsReporter } from '@kbn/ci-stats-reporter'; +import { FtrProviderContext } from '../functional/ftr_provider_context'; + +const IGNORED_FIELDS = [ + // The following fields are returned by the _field_caps API but aren't counted + // towards the index field limit. + '_seq_no', + '_id', + '_version', + '_field_names', + '_ignored', + '_feature', + '_index', + '_routing', + '_source', + '_type', + '_nested_path', + '_timestamp', + // migrationVersion is dynamic so will be anywhere between 1..type count + // depending on which objects are present in the index when querying the + // field caps API. See https://github.com/elastic/kibana/issues/70815 + 'migrationVersion', +]; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const log = getService('log'); + const es = getService('es'); + + describe('Saved Objects Field Count', () => { + it('capture', async () => { + const reporter = CiStatsReporter.fromEnv(log); + + log.debug('Saved Objects field count metrics starting'); + + const { fields } = await es.fieldCaps({ + index: '.kibana', + fields: '*', + }); + + const fieldCountPerTypeMap: Map = Object.keys(fields) + .map((f) => f.split('.')[0]) + .filter((f) => !IGNORED_FIELDS.includes(f)) + .reduce((accumulator, f) => { + accumulator.set(f, accumulator.get(f) + 1 || 1); + return accumulator; + }, new Map()); + + const metrics = Array.from(fieldCountPerTypeMap.entries()) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([fieldType, count]) => ({ + group: 'Saved Objects .kibana field count', + id: fieldType, + value: count, + })); + + log.debug( + 'Saved Objects field count metrics:\n', + metrics.map(({ id, value }) => `${id}:${value}`).join('\n') + ); + + await reporter.metrics(metrics); + log.debug('Saved Objects field count metrics done'); + }); + }); +} diff --git a/x-pack/test/screenshot_creation/config.ts b/x-pack/test/screenshot_creation/config.ts index 659034e9fbe8bf..18dda361ac2cdb 100644 --- a/x-pack/test/screenshot_creation/config.ts +++ b/x-pack/test/screenshot_creation/config.ts @@ -9,7 +9,9 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { // default to the xpack functional config diff --git a/x-pack/test/search_sessions_integration/config.ts b/x-pack/test/search_sessions_integration/config.ts index 9dc542038a48a5..2d570a607c7467 100644 --- a/x-pack/test/search_sessions_integration/config.ts +++ b/x-pack/test/search_sessions_integration/config.ts @@ -10,7 +10,9 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { services } from '../functional/services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { // default to the xpack functional config diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/index.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/index.ts index b87f43f73ed2e6..9465de1de5922c 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/index.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/index.ts @@ -14,8 +14,6 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const searchSessions = getService('searchSessions'); describe('Dashboard', function () { - this.tags('ciGroup5'); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await esArchiver.load('x-pack/test/functional/es_archives/dashboard/async_search'); diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/index.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/index.ts index 6a13dc268b7051..1ff11eb9884561 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/index.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/index.ts @@ -13,8 +13,6 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const PageObjects = getPageObjects(['common']); describe('Search session sharing', function () { - this.tags('ciGroup5'); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/index.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/index.ts index 1f8d2196744285..2af94730ff9181 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/discover/index.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/discover/index.ts @@ -14,8 +14,6 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const searchSessions = getService('searchSessions'); describe('Discover', function () { - this.tags('ciGroup5'); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); diff --git a/x-pack/test/search_sessions_integration/tests/apps/lens/index.ts b/x-pack/test/search_sessions_integration/tests/apps/lens/index.ts index 5e45db7bd72330..978c5bdec0df5d 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/lens/index.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/lens/index.ts @@ -12,8 +12,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('lens search sessions', function () { - this.tags('ciGroup5'); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); diff --git a/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/index.ts b/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/index.ts index b72d8db06a1d8f..60e4ea1b3bfbbc 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/index.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/index.ts @@ -12,8 +12,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('search sessions management', function () { - this.tags('ciGroup5'); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await esArchiver.load('x-pack/test/functional/es_archives/dashboard/async_search'); diff --git a/x-pack/test/security_api_integration/tests/anonymous/index.ts b/x-pack/test/security_api_integration/tests/anonymous/index.ts index 08f075950a57f7..0f976589483a8f 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/index.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Anonymous access', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./login')); loadTestFile(require.resolve('./capabilities')); }); diff --git a/x-pack/test/security_api_integration/tests/audit/index.ts b/x-pack/test/security_api_integration/tests/audit/index.ts index 14628bbc51e892..96b2ceb5ae3a78 100644 --- a/x-pack/test/security_api_integration/tests/audit/index.ts +++ b/x-pack/test/security_api_integration/tests/audit/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Audit Log', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./audit_log')); }); } diff --git a/x-pack/test/security_api_integration/tests/http_bearer/index.ts b/x-pack/test/security_api_integration/tests/http_bearer/index.ts index 4dbad2660ebaa3..c796c0be9befb3 100644 --- a/x-pack/test/security_api_integration/tests/http_bearer/index.ts +++ b/x-pack/test/security_api_integration/tests/http_bearer/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - HTTP Bearer', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./header')); }); } diff --git a/x-pack/test/security_api_integration/tests/http_no_auth_providers/index.ts b/x-pack/test/security_api_integration/tests/http_no_auth_providers/index.ts index 652bcc419e2430..23096b2449c9f6 100644 --- a/x-pack/test/security_api_integration/tests/http_no_auth_providers/index.ts +++ b/x-pack/test/security_api_integration/tests/http_no_auth_providers/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - HTTP no authentication providers are enabled', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./authentication')); }); } diff --git a/x-pack/test/security_api_integration/tests/kerberos/index.ts b/x-pack/test/security_api_integration/tests/kerberos/index.ts index cec92939a5194f..828ce7220458fe 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/index.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Kerberos', function () { - this.tags('ciGroup31'); - loadTestFile(require.resolve('./kerberos_login')); }); } diff --git a/x-pack/test/security_api_integration/tests/login_selector/index.ts b/x-pack/test/security_api_integration/tests/login_selector/index.ts index a38a0acc68cca5..e3698340d39676 100644 --- a/x-pack/test/security_api_integration/tests/login_selector/index.ts +++ b/x-pack/test/security_api_integration/tests/login_selector/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Login Selector', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./basic_functionality')); }); } diff --git a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/index.ts b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/index.ts index 858b8e2fbb7502..2c8edc1569bd2e 100644 --- a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/index.ts +++ b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - OIDC (Authorization Code Flow)', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./oidc_auth')); }); } diff --git a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/index.ts b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/index.ts index c42014661b915a..7479ba8e7bd811 100644 --- a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/index.ts +++ b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - OIDC (Implicit Flow)', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./oidc_auth')); }); } diff --git a/x-pack/test/security_api_integration/tests/pki/index.ts b/x-pack/test/security_api_integration/tests/pki/index.ts index c3b733d0b31f81..9926f166198981 100644 --- a/x-pack/test/security_api_integration/tests/pki/index.ts +++ b/x-pack/test/security_api_integration/tests/pki/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - PKI', function () { - this.tags('ciGroup22'); - loadTestFile(require.resolve('./pki_auth')); }); } diff --git a/x-pack/test/security_api_integration/tests/saml/index.ts b/x-pack/test/security_api_integration/tests/saml/index.ts index e7ffdbd410de30..3597f1a6104ecb 100644 --- a/x-pack/test/security_api_integration/tests/saml/index.ts +++ b/x-pack/test/security_api_integration/tests/saml/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - SAML', function () { - this.tags('ciGroup27'); - loadTestFile(require.resolve('./saml_login')); }); } diff --git a/x-pack/test/security_api_integration/tests/session_idle/index.ts b/x-pack/test/security_api_integration/tests/session_idle/index.ts index 76457ee7ad0c7b..6966b9f2ed2c78 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/index.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Idle', function () { - this.tags('ciGroup18'); - loadTestFile(require.resolve('./cleanup')); loadTestFile(require.resolve('./extension')); }); diff --git a/x-pack/test/security_api_integration/tests/session_invalidate/index.ts b/x-pack/test/security_api_integration/tests/session_invalidate/index.ts index 6408e4cfbd43d3..dcfb3d7fc52599 100644 --- a/x-pack/test/security_api_integration/tests/session_invalidate/index.ts +++ b/x-pack/test/security_api_integration/tests/session_invalidate/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Invalidate', function () { - this.tags('ciGroup6'); - loadTestFile(require.resolve('./invalidate')); }); } diff --git a/x-pack/test/security_api_integration/tests/session_lifespan/index.ts b/x-pack/test/security_api_integration/tests/session_lifespan/index.ts index 15522da907958b..e297805b4ab3cc 100644 --- a/x-pack/test/security_api_integration/tests/session_lifespan/index.ts +++ b/x-pack/test/security_api_integration/tests/session_lifespan/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Lifespan', function () { - this.tags('ciGroup6'); - loadTestFile(require.resolve('./cleanup')); }); } diff --git a/x-pack/test/security_api_integration/tests/token/index.ts b/x-pack/test/security_api_integration/tests/token/index.ts index 88c82125ee1d9f..54717dc1c8617a 100644 --- a/x-pack/test/security_api_integration/tests/token/index.ts +++ b/x-pack/test/security_api_integration/tests/token/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Token', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./login')); loadTestFile(require.resolve('./logout')); loadTestFile(require.resolve('./header')); diff --git a/x-pack/test/security_functional/login_selector.config.ts b/x-pack/test/security_functional/login_selector.config.ts index aa145e2ec62167..d2035a9b228e81 100644 --- a/x-pack/test/security_functional/login_selector.config.ts +++ b/x-pack/test/security_functional/login_selector.config.ts @@ -17,7 +17,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); const kibanaPort = kibanaFunctionalConfig.get('servers.kibana.port'); diff --git a/x-pack/test/security_functional/oidc.config.ts b/x-pack/test/security_functional/oidc.config.ts index 9c00960671e03a..6476bbb501b776 100644 --- a/x-pack/test/security_functional/oidc.config.ts +++ b/x-pack/test/security_functional/oidc.config.ts @@ -17,7 +17,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); const kibanaPort = kibanaFunctionalConfig.get('servers.kibana.port'); diff --git a/x-pack/test/security_functional/saml.config.ts b/x-pack/test/security_functional/saml.config.ts index 264197c961123d..60a934a712bbff 100644 --- a/x-pack/test/security_functional/saml.config.ts +++ b/x-pack/test/security_functional/saml.config.ts @@ -17,7 +17,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); const kibanaPort = kibanaFunctionalConfig.get('servers.kibana.port'); diff --git a/x-pack/test/security_functional/tests/login_selector/index.ts b/x-pack/test/security_functional/tests/login_selector/index.ts index 1a34fc5eac6d9a..bf3cc557f0bd7d 100644 --- a/x-pack/test/security_functional/tests/login_selector/index.ts +++ b/x-pack/test/security_functional/tests/login_selector/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - login selector', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./basic_functionality')); loadTestFile(require.resolve('./auth_provider_hint')); }); diff --git a/x-pack/test/security_functional/tests/oidc/index.ts b/x-pack/test/security_functional/tests/oidc/index.ts index cd328384febd3a..37490a0193089b 100644 --- a/x-pack/test/security_functional/tests/oidc/index.ts +++ b/x-pack/test/security_functional/tests/oidc/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - OIDC interactions', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./url_capture')); }); } diff --git a/x-pack/test/security_functional/tests/saml/index.ts b/x-pack/test/security_functional/tests/saml/index.ts index 66a497db8af405..ebf97ebf8edfb1 100644 --- a/x-pack/test/security_functional/tests/saml/index.ts +++ b/x-pack/test/security_functional/tests/saml/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - SAML interactions', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./url_capture')); }); } diff --git a/x-pack/test/security_solution_cypress/config.firefox.ts b/x-pack/test/security_solution_cypress/config.firefox.ts index 2a2ce410850ff6..c29f47708a1702 100644 --- a/x-pack/test/security_solution_cypress/config.firefox.ts +++ b/x-pack/test/security_solution_cypress/config.firefox.ts @@ -16,7 +16,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); return { diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 7f196880e8fca3..4b5b2c361c1b91 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -14,7 +14,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); return { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index e57895f4f32b75..c776dcf91602fd 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -16,7 +16,6 @@ export default function (providerContext: FtrProviderContext) { const { loadTestFile, getService } = providerContext; describe('endpoint', function () { - this.tags('ciGroup7'); const ingestManager = getService('ingestManager'); const log = getService('log'); const endpointTestResources = getService('endpointTestResources'); diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index b00df7732ea4fe..b5b52b7bc5cd58 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -15,7 +15,9 @@ import { } from '../security_solution_endpoint_api_int/registry'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { ...xpackFunctionalConfig.getAll(), diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 3c98b703aed55c..7be4ce22433032 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -13,8 +13,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider const { loadTestFile, getService } = providerContext; describe('Endpoint plugin', function () { - this.tags('ciGroup9'); - const ingestManager = getService('ingestManager'); const log = getService('log'); diff --git a/x-pack/test/spaces_api_integration/common/config.ts b/x-pack/test/spaces_api_integration/common/config.ts index 5d135cd05605c4..15a63fec6d309a 100644 --- a/x-pack/test/spaces_api_integration/common/config.ts +++ b/x-pack/test/spaces_api_integration/common/config.ts @@ -21,7 +21,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) const config = { kibana: { api: await readConfigFile(path.resolve(REPO_ROOT, 'test/api_integration/config.js')), - functional: await readConfigFile(require.resolve('../../../../test/functional/config.js')), + functional: await readConfigFile( + require.resolve('../../../../test/functional/config.base.js') + ), }, xpack: { api: await readConfigFile(require.resolve('../../api_integration/config.ts')), 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 a86fef0d758fc9..75381f35dacd3d 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 @@ -14,8 +14,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { const supertest = getService('supertest'); describe('spaces api with security', function () { - this.tags('ciGroup8'); - before(async () => { await createUsersAndRoles(es, supertest); }); 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 f64336b2b49086..6a8148efaa1d64 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 @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function spacesOnlyTestSuite({ loadTestFile }: FtrProviderContext) { describe('spaces api without security', function () { - this.tags('ciGroup5'); - loadTestFile(require.resolve('./copy_to_space')); loadTestFile(require.resolve('./resolve_copy_to_space_conflicts')); loadTestFile(require.resolve('./create')); diff --git a/x-pack/test/stack_functional_integration/apps/telemetry/index.js b/x-pack/test/stack_functional_integration/apps/telemetry/index.js index 80cffcfaf70a7b..3d1dffb3a442f6 100644 --- a/x-pack/test/stack_functional_integration/apps/telemetry/index.js +++ b/x-pack/test/stack_functional_integration/apps/telemetry/index.js @@ -7,7 +7,6 @@ export default function ({ loadTestFile }) { describe('telemetry feature', function () { - this.tags('ciGroup1'); loadTestFile(require.resolve('./_telemetry')); }); } diff --git a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js index 9b768ab61baece..1658bcbf6cd355 100644 --- a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js +++ b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js @@ -25,7 +25,9 @@ const testsFolder = '../apps'; const prepend = (testFile) => require.resolve(`${testsFolder}/${testFile}`); export default async ({ readConfigFile }) => { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../../functional/config')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../../functional/config.base.js') + ); const externalConf = consumeState(resolve(__dirname, stateFilePath)); process.env.stack_functional_integration = true; logAll(log); diff --git a/x-pack/test/timeline/security_and_spaces/tests/basic/index.ts b/x-pack/test/timeline/security_and_spaces/tests/basic/index.ts index 4672a8e2e7f65c..248c5ece3641d2 100644 --- a/x-pack/test/timeline/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/timeline/security_and_spaces/tests/basic/index.ts @@ -14,9 +14,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('timeline security and spaces enabled: basic', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpacesAndUsers(getService); }); diff --git a/x-pack/test/timeline/security_and_spaces/tests/trial/index.ts b/x-pack/test/timeline/security_and_spaces/tests/trial/index.ts index 736fb6619c82d5..2103097891a311 100644 --- a/x-pack/test/timeline/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/timeline/security_and_spaces/tests/trial/index.ts @@ -38,9 +38,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('timeline security and spaces enabled: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpaces(getService); await createUsersAndRoles( diff --git a/x-pack/test/ui_capabilities/common/config.ts b/x-pack/test/ui_capabilities/common/config.ts index f676a5eeccee11..32e7538ecbbe7b 100644 --- a/x-pack/test/ui_capabilities/common/config.ts +++ b/x-pack/test/ui_capabilities/common/config.ts @@ -20,7 +20,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) return async ({ readConfigFile }: FtrConfigProviderContext) => { const xPackFunctionalTestsConfig = await readConfigFile( - require.resolve('../../functional/config.js') + require.resolve('../../functional/config.base.js') ); return { diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts index a257f8fcabb8e4..77619157c615f7 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts @@ -16,8 +16,6 @@ export default function uiCapabilitiesTests({ loadTestFile, getService }: FtrPro const featuresService: FeaturesService = getService('features'); describe('ui capabilities', function () { - this.tags('ciGroup9'); - before(async () => { const features = await featuresService.get(); for (const space of Spaces) { diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/index.ts b/x-pack/test/ui_capabilities/spaces_only/tests/index.ts index 73a068cf9ec6b3..02ed1533b56ad3 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/index.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/index.ts @@ -14,8 +14,6 @@ export default function uiCapabilitesTests({ loadTestFile, getService }: FtrProv const featuresService: FeaturesService = getService('features'); describe('ui capabilities', function () { - this.tags('ciGroup9'); - before(async () => { // we're using a basic license, so if we want to disable all features, we have to ignore the valid licenses const features = await featuresService.get({ ignoreValidLicenses: true }); diff --git a/x-pack/test/upgrade/config.ts b/x-pack/test/upgrade/config.ts index 78d61d5239556b..181abe8ca408ff 100644 --- a/x-pack/test/upgrade/config.ts +++ b/x-pack/test/upgrade/config.ts @@ -12,7 +12,7 @@ import { MapsHelper } from './services/maps_upgrade_services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const apiConfig = await readConfigFile(require.resolve('../api_integration/config')); - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { ...functionalConfig.getAll(), diff --git a/x-pack/test/upgrade_assistant_integration/config.js b/x-pack/test/upgrade_assistant_integration/config.js index 8152b5790fc40f..e1248c717b2162 100644 --- a/x-pack/test/upgrade_assistant_integration/config.js +++ b/x-pack/test/upgrade_assistant_integration/config.js @@ -11,7 +11,7 @@ export default async function ({ readConfigFile }) { require.resolve('../../../test/api_integration/config.js') ); const xPackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); const kibanaCommonConfig = await readConfigFile( require.resolve('../../../test/common/config.js') diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js index 1a7090a3cbdfb1..eb09d24b79b6a7 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js @@ -7,8 +7,6 @@ export default function ({ loadTestFile }) { describe('upgrade assistant', function () { - this.tags('ciGroup7'); - loadTestFile(require.resolve('./reindexing')); }); } diff --git a/x-pack/test/usage_collection/config.ts b/x-pack/test/usage_collection/config.ts index beb934219422a9..248f00ff812332 100644 --- a/x-pack/test/usage_collection/config.ts +++ b/x-pack/test/usage_collection/config.ts @@ -15,7 +15,9 @@ import { pageObjects } from './page_objects'; // that returns an object with the projects config values export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(resolve(__dirname, 'plugins')); diff --git a/x-pack/test/usage_collection/test_suites/application_usage/index.ts b/x-pack/test/usage_collection/test_suites/application_usage/index.ts index 4b41aada9ad299..754ae98997c165 100644 --- a/x-pack/test/usage_collection/test_suites/application_usage/index.ts +++ b/x-pack/test/usage_collection/test_suites/application_usage/index.ts @@ -11,7 +11,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Application Usage', function () { - this.tags('ciGroup1'); const { common } = getPageObjects(['common']); const browser = getService('browser'); diff --git a/x-pack/test/usage_collection/test_suites/stack_management_usage/index.ts b/x-pack/test/usage_collection/test_suites/stack_management_usage/index.ts index dac552220f7c13..a595c2662d4510 100644 --- a/x-pack/test/usage_collection/test_suites/stack_management_usage/index.ts +++ b/x-pack/test/usage_collection/test_suites/stack_management_usage/index.ts @@ -12,7 +12,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/119038 describe.skip('Stack Management', function () { - this.tags('ciGroup1'); const { common } = getPageObjects(['common']); const browser = getService('browser'); diff --git a/x-pack/test/visual_regression/config.ts b/x-pack/test/visual_regression/config.ts index c211918ef8e529..c7f0d8203833e2 100644 --- a/x-pack/test/visual_regression/config.ts +++ b/x-pack/test/visual_regression/config.ts @@ -10,7 +10,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { ...functionalConfig.getAll(), diff --git a/x-pack/test/visual_regression/tests/canvas/index.js b/x-pack/test/visual_regression/tests/canvas/index.js index 099c96e6eaf014..20a262fef10fee 100644 --- a/x-pack/test/visual_regression/tests/canvas/index.js +++ b/x-pack/test/visual_regression/tests/canvas/index.js @@ -25,7 +25,6 @@ export default function ({ loadTestFile, getService }) { await esArchiver.unload('x-pack/test/functional/es_archives/canvas/default'); }); - this.tags('ciGroup10'); loadTestFile(require.resolve('./fullscreen')); }); } diff --git a/x-pack/test/visual_regression/tests/infra/index.js b/x-pack/test/visual_regression/tests/infra/index.js index b624c6ec848f2f..13669c50953f9a 100644 --- a/x-pack/test/visual_regression/tests/infra/index.js +++ b/x-pack/test/visual_regression/tests/infra/index.js @@ -13,7 +13,6 @@ export default function ({ loadTestFile, getService }) { await browser.setWindowSize(1600, 1000); }); - this.tags('ciGroup10'); loadTestFile(require.resolve('./waffle_map')); loadTestFile(require.resolve('./saved_views')); }); diff --git a/x-pack/test/visual_regression/tests/maps/index.js b/x-pack/test/visual_regression/tests/maps/index.js index 3459896baacd61..9d53d70ad2abc4 100644 --- a/x-pack/test/visual_regression/tests/maps/index.js +++ b/x-pack/test/visual_regression/tests/maps/index.js @@ -56,7 +56,6 @@ export default function ({ loadTestFile, getService }) { ); }); - this.tags('ciGroup10'); loadTestFile(require.resolve('./vector_styling')); }); } From 1591bfba2404f1b645002bb642842f7297a0c010 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 4 May 2022 15:11:53 -0700 Subject: [PATCH 10/83] [DOCS] Replace execution terminology in Alerting (#131357) --- docs/settings/alert-action-settings.asciidoc | 10 +++-- .../alerting-getting-started.asciidoc | 4 +- docs/user/alerting/alerting-setup.asciidoc | 2 +- .../alerting-troubleshooting.asciidoc | 8 ++-- .../alerting/create-and-manage-rules.asciidoc | 6 +-- .../alerting/rule-types/es-query.asciidoc | 24 ++++++------ .../rule-types/index-threshold.asciidoc | 2 +- .../alerting-common-issues.asciidoc | 39 +++++++++---------- .../troubleshooting/event-log-index.asciidoc | 25 +++++++----- .../testing-connectors.asciidoc | 8 ++-- 10 files changed, 69 insertions(+), 59 deletions(-) diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index f3fa027cb186e7..aa5d9f53359b73 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -92,7 +92,7 @@ URLs can use both the `ssl` and `smtp` options. + No other URL values should be part of this URL, including paths, query strings, and authentication information. When an http or smtp request -is made as part of executing an action, only the protocol, hostname, and +is made as part of running an action, only the protocol, hostname, and port of the URL for that request are used to look up these configuration values. @@ -188,10 +188,14 @@ For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`. ==== Alerting settings `xpack.alerting.maxEphemeralActionsPerAlert`:: -Sets the number of actions that will be executed ephemerally. To use this, enable ephemeral tasks in task manager first with <> +Sets the number of actions that will run ephemerally. To use this, enable +ephemeral tasks in task manager first with +<> `xpack.alerting.cancelAlertsOnRuleTimeout`:: -Specifies whether to skip writing alerts and scheduling actions if rule execution is cancelled due to timeout. Default: `true`. This setting can be overridden by individual rule types. +Specifies whether to skip writing alerts and scheduling actions if rule +processing was cancelled due to a timeout. Default: `true`. This setting can be +overridden by individual rule types. `xpack.alerting.rules.minimumScheduleInterval.value`:: Specifies the minimum schedule interval for rules. This minimum is applied to all rules created or updated after you set this value. The time is formatted as: diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 584d45dc088fd0..ca0b8ff8ee1117 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -63,7 +63,7 @@ Rule schedules are defined as an interval between subsequent checks, and can ran [IMPORTANT] ============================================== -The intervals of rule checks in {kib} are approximate. The timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. +The intervals of rule checks in {kib} are approximate. Their timing is affected by factors such as the frequency at which tasks are claimed and the task load on the system. Refer to <> for more information. ============================================== [float] @@ -82,7 +82,7 @@ The result is a template: all the parameters needed to invoke a service are supp In the server monitoring example, the `email` connector type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. -When the rule detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` connector type. +When the rule detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and runs the action on the {kib} server by invoking the `email` connector type. image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc index 6f8caedde3e18b..2b92e8caa7ef90 100644 --- a/docs/user/alerting/alerting-setup.asciidoc +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -62,7 +62,7 @@ Rules and connectors are isolated to the {kib} space in which they were created. [[alerting-authorization]] === Authorization -Rules are authorized using an <> associated with the last user to edit the rule. This API key captures a snapshot of the user's privileges at the time of edit and is subsequently used to run all background tasks associated with the rule, including condition checks, like {es} queries, and action executions. The following rule actions will re-generate the API key: +Rules are authorized using an <> associated with the last user to edit the rule. This API key captures a snapshot of the user's privileges at the time of edit and is subsequently used to run all background tasks associated with the rule, including condition checks like {es} queries and triggered actions. The following rule actions will re-generate the API key: * Creating a rule * Enabling a disabled rule diff --git a/docs/user/alerting/alerting-troubleshooting.asciidoc b/docs/user/alerting/alerting-troubleshooting.asciidoc index 5978d0f9031454..32c77d7fa57a75 100644 --- a/docs/user/alerting/alerting-troubleshooting.asciidoc +++ b/docs/user/alerting/alerting-troubleshooting.asciidoc @@ -52,7 +52,7 @@ Diagnosing these may be difficult - but there may be log messages for error cond === Use the REST APIs There is a rich set of HTTP endpoints to introspect and manage rules and connectors. -One of the http endpoints available for actions is the POST <>. You can use this to “test” an action. For instance, if you have a server log action created, you can execute it via curling the endpoint: +One of the http endpoints available for actions is the POST <>. You can use this to “test” an action. For instance, if you have a server log action created, you can run it via curling the endpoint: [source, txt] -------------------------------------------------- curl -X POST -k \ @@ -75,7 +75,7 @@ The same REST POST _execute API command will be: kbn-action execute a692dc89-15b9-4a3c-9e47-9fb6872e49ce ‘{"params":{"subject":"hallo","message":"hallo!","to":["me@example.com"]}}’ -------------------------------------------------- -The result of this http request (and printed to stdout by https://github.com/pmuellr/kbn-action[kbn-action]) will be data returned by the action execution, along with error messages if errors were encountered. +The result of this http request (and printed to stdout by https://github.com/pmuellr/kbn-action[kbn-action]) will be data returned by the action, along with error messages if errors were encountered. [float] [[alerting-error-banners]] @@ -92,8 +92,8 @@ image::images/rules-details-health.png[Rule details page with the errors banner] [[task-manager-diagnostics]] === Task Manager diagnostics -Under the hood, *Rules and Connectors* uses a plugin called Task Manager, which handles the scheduling, execution, and error handling of the tasks. -This means that failure cases in Rules or Connectors will, at times, be revealed by the Task Manager mechanism, rather than the Rules mechanism. +Under the hood, {rules-ui} uses a plugin called Task Manager, which handles the scheduling, running, and error handling of the tasks. +This means that failure cases in {rules-ui} will, at times, be revealed by the Task Manager mechanism, rather than the Rules mechanism. Task Manager provides a visible status which can be used to diagnose issues and is very well documented <> and <>. Task Manager uses the `.kibana_task_manager` index, an internal index that contains all the saved objects that represent the tasks in the system. diff --git a/docs/user/alerting/create-and-manage-rules.asciidoc b/docs/user/alerting/create-and-manage-rules.asciidoc index ba1629abd9c864..52db2ed51217e9 100644 --- a/docs/user/alerting/create-and-manage-rules.asciidoc +++ b/docs/user/alerting/create-and-manage-rules.asciidoc @@ -44,7 +44,7 @@ Notify:: This value limits how often actions are repeated when an alert rem [[alerting-concepts-suppressing-duplicate-notifications]] [NOTE] ============================================== -Since actions are executed per alert, a rule can end up generating a large number of actions. Take the following example where a rule is monitoring three servers every minute for CPU usage > 0.9, and the rule is set to notify **Every time alert is active**: +Since actions are triggered per alert, a rule can end up generating a large number of actions. Take the following example where a rule is monitoring three servers every minute for CPU usage > 0.9, and the rule is set to notify **Every time alert is active**: * Minute 1: server X123 > 0.9. *One email* is sent for server X123. * Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, one for X123 and one for Y456. @@ -163,8 +163,8 @@ A rule can have one of the following statuses: `active`:: The conditions for the rule have been met, and the associated actions should be invoked. `ok`:: The conditions for the rule have not been met, and the associated actions are not invoked. -`error`:: An error was encountered during rule execution. -`pending`:: The rule has not yet executed. The rule was either just created, or enabled after being disabled. +`error`:: An error was encountered by the rule. +`pending`:: The rule has not yet run. The rule was either just created, or enabled after being disabled. `unknown`:: A problem occurred when calculating the status. Most likely, something went wrong with the alerting code. [float] diff --git a/docs/user/alerting/rule-types/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc index dba8a4878cb26b..c8f98808ca552f 100644 --- a/docs/user/alerting/rule-types/es-query.asciidoc +++ b/docs/user/alerting/rule-types/es-query.asciidoc @@ -26,7 +26,7 @@ Index:: Specifies an *index or data view* and a *time field* that is used for the *time window*. Size:: Specifies the number of documents to pass to the configured actions when the threshold condition is met. -{es} query:: Specifies the ES DSL query to execute. The number of documents that +{es} query:: Specifies the ES DSL query. The number of documents that match this query is evaluated against the threshold condition. Only the `query` field is used, other DSL fields are not considered. Threshold:: Defines a threshold value and a comparison operator (`is above`, @@ -81,7 +81,7 @@ image::images/rule-types-es-query-example-action-variable.png[Iterate over hits Use the *Test query* feature to verify that your query DSL is valid. -* Valid queries are executed against the configured *index* using the configured +* Valid queries are run against the configured *index* using the configured *time window*. The number of documents that match the query is displayed. + [role="screenshot"] @@ -95,16 +95,14 @@ image::user/alerting/images/rule-types-es-query-invalid.png[Test {es} query show [float] ==== Handling multiple matches of the same document -This rule type checks for duplication of document matches across rule -executions. If you configure the rule with a schedule interval smaller than the -time window, and a document matches a query in multiple rule executions, it is -alerted on only once. +This rule type checks for duplication of document matches across multiple runs. +If you configure the rule with a schedule interval smaller than the time window, +and a document matches a query in multiple runs, it is alerted on only once. The rule uses the timestamp of the matches to avoid alerting on the same match multiple times. The timestamp of the latest match is used for evaluating the -rule conditions when the rule is executed. Only matches between the latest -timestamp from the previous execution and the actual rule execution are -considered. +rule conditions when the rule runs. Only matches between the latest timestamp +from the previous run and the current run are considered. Suppose you have a rule configured to run every minute. The rule uses a time window of 1 hour and checks if there are more than 99 matches for the query. The @@ -112,16 +110,16 @@ window of 1 hour and checks if there are more than 99 matches for the query. The [cols="3*<"] |=== -| `Execution 1 (0:00)` +| `Run 1 (0:00)` | Rule finds 113 matches in the last hour: `113 > 99` | Rule is active and user is alerted. -| `Execution 2 (0:01)` +| `Run 2 (0:01)` | Rule finds 127 matches in the last hour. 105 of the matches are duplicates that were already alerted on previously, so you actually have 22 matches: `22 !> 99` | No alert. -| `Execution 3 (0:02)` +| `Run 3 (0:02)` | Rule finds 159 matches in the last hour. 88 of the matches are duplicates that were already alerted on previously, so you actually have 71 matches: `71 !> 99` | No alert. -| `Execution 4 (0:03)` +| `Run 4 (0:03)` | Rule finds 190 matches in the last hour. 71 of them are duplicates that were already alerted on previously, so you actually have 119 matches: `119 > 99` | Rule is active and user is alerted. |=== \ No newline at end of file diff --git a/docs/user/alerting/rule-types/index-threshold.asciidoc b/docs/user/alerting/rule-types/index-threshold.asciidoc index c65b0f66b1b63b..03f855a8610226 100644 --- a/docs/user/alerting/rule-types/index-threshold.asciidoc +++ b/docs/user/alerting/rule-types/index-threshold.asciidoc @@ -52,7 +52,7 @@ In this example, you will use the {kib} < Rules and Connectors**. -. Create a new rule that is checked every four hours and executes actions when the rule status changes. +. Create a new rule that is checked every four hours and triggers actions when the rule status changes. + [role="screenshot"] image::user/alerting/images/rule-types-index-threshold-select.png[Choosing an index threshold rule type] diff --git a/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc b/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc index 7ab34dacacd987..75a158e6d364f8 100644 --- a/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc +++ b/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc @@ -40,35 +40,34 @@ When diagnosing issues related to alerting, focus on the tasks that begin with ` Alerting tasks always begin with `alerting:`. For example, the `alerting:.index-threshold` tasks back the <>. Action tasks always begin with `actions:`. For example, the `actions:.index` tasks back the <>. -For more details on monitoring and diagnosing task execution in Task Manager, see <>. +For more details on monitoring and diagnosing tasks in Task Manager, refer to <>. [float] [[connector-tls-settings]] -==== Connectors have TLS errors when executing actions +==== Connectors have TLS errors when running actions *Problem* -When executing actions, a connector gets a TLS socket error when connecting to -the server. +A connector gets a TLS socket error when connecting to the server to run an action. *Solution* Configuration options are available to specialize connections to TLS servers, -including ignoring server certificate validation, and providing certificate -authority data to verify servers using custom certificates. For more details, -see <>. +including ignoring server certificate validation and providing certificate +authority data to verify servers using custom certificates. For more details, +see <>. [float] -[[rules-long-execution-time]] +[[rules-long-run-time]] ==== Rules take a long time to run *Problem* -Rules are taking a long time to execute and are impacting the overall health of your deployment. +Rules are taking a long time to run and are impacting the overall health of your deployment. [IMPORTANT] ============================================== -By default, only users with a `superuser` role can query the experimental[] {kib} event log because it is a system index. To enable additional users to execute this query, assign `read` privileges to the `.kibana-event-log*` index. +By default, only users with a `superuser` role can query the experimental[] {kib} event log because it is a system index. To enable additional users to run this query, assign `read` privileges to the `.kibana-event-log*` index. ============================================== *Solution* @@ -87,9 +86,9 @@ image::images/rule-details-timeout-error.png[Rule details page with timeout erro If you want your rules to run longer, update the `xpack.alerting.rules.run.timeout` configuration in your <>. You can also target a specific rule type by using `xpack.alerting.rules.run.ruleTypeOverrides`. -Rules that consistently run longer than their <> may produce unexpected results. If the average run duration, visible on the <>, is greater than the check interval, consider increasing the check interval. +Rules that consistently run longer than their <> may produce unexpected results. If the average run duration, visible on the <>, is greater than the check interval, consider increasing the check interval. -To get all long-running rules, you can query for a list of rule ids, bucketed by their execution times: +To get all long-running rules, you can query for a list of rule ids, bucketed by their run times: [source,console] -------------------------------------------------- @@ -160,9 +159,9 @@ GET /.kibana-event-log*/_search -------------------------------------------------- // TEST -<1> This queries for rules executed in the last day. Update the values of `lte` and `gte` to query over a different time range. -<2> Use `event.provider: actions` to query for long-running action executions. -<3> Execution durations are stored as nanoseconds. This adds a runtime field to convert that duration into seconds. +<1> This queries for rules run in the last day. Update the values of `lte` and `gte` to query over a different time range. +<2> Use `event.provider: actions` to query for long-running actions. +<3> Run durations are stored as nanoseconds. This adds a runtime field to convert that duration into seconds. <4> This interval buckets the `event.duration_in_seconds` runtime field into 1 second intervals. Update this value to change the granularity of the buckets. If you are unable to use runtime fields, make sure this aggregation targets `event.duration` and use nanoseconds for the interval. <5> This retrieves the top 10 rule ids for this duration interval. Update this value to retrieve more rule ids. @@ -237,10 +236,10 @@ This query returns the following: } } -------------------------------------------------- -<1> Most rule execution durations fall within the first bucket (0 - 1 seconds). -<2> A single rule with id `41893910-6bca-11eb-9e0d-85d233e3ee35` took between 30 and 31 seconds to execute. +<1> Most run durations fall within the first bucket (0 - 1 seconds). +<2> A single rule with id `41893910-6bca-11eb-9e0d-85d233e3ee35` took between 30 and 31 seconds to run. -Use the <> to retrieve additional information about rules that take a long time to execute. +Use the <> to retrieve additional information about rules that take a long time to run. [float] [[rule-cannot-decrypt-api-key]] @@ -248,11 +247,11 @@ Use the <> to retrieve additional information about r *Problem*: -The rule fails to execute and has an `Unable to decrypt attribute "apiKey"` error. +The rule fails to run and has an `Unable to decrypt attribute "apiKey"` error. *Solution*: -This error happens when the `xpack.encryptedSavedObjects.encryptionKey` value used to create the rule does not match the value used during rule execution. Depending on the scenario, there are different ways to solve this problem: +This error happens when the `xpack.encryptedSavedObjects.encryptionKey` value used to create the rule does not match the value used when the rule runs. Depending on the scenario, there are different ways to solve this problem: [cols="2*<"] |=== diff --git a/docs/user/alerting/troubleshooting/event-log-index.asciidoc b/docs/user/alerting/troubleshooting/event-log-index.asciidoc index 5016b6d6f19c93..a0e6cd11ed1843 100644 --- a/docs/user/alerting/troubleshooting/event-log-index.asciidoc +++ b/docs/user/alerting/troubleshooting/event-log-index.asciidoc @@ -6,15 +6,16 @@ experimental[] Use the event log index to determine: -* Whether a rule successfully ran but its associated actions did not +* Whether a rule ran successfully but its associated actions did not * Whether a rule was ever activated -* Additional information about rule execution errors -* Duration times for rule and action executions +* Additional information about errors when the rule ran +* Run durations for the rules and actions [float] -==== Example Event Log Queries +==== Example event log queries + +The following event log query looks at all events related to a specific rule id: -Event log query to look at all event related to a specific rule id: [source, txt] -------------------------------------------------- GET /.kibana-event-log*/_search @@ -77,7 +78,9 @@ GET /.kibana-event-log*/_search } -------------------------------------------------- -Event log query to look at all events related to executing a rule or action. These events include duration. +The following event log query looks at all events related to running a rule or +action. These events include duration: + [source, txt] -------------------------------------------------- GET /.kibana-event-log*/_search @@ -124,8 +127,10 @@ GET /.kibana-event-log*/_search } -------------------------------------------------- -Event log query to look at the errors. -You should see an `error.message` property in that event, with a message from the action executor that might provide more detail on why the action encountered an error: +The following event log query looks at the errors. You should see an +`error.message` property in that event, with a message that might provide more +details about why the action encountered an error: + [source, txt] -------------------------------------------------- { @@ -150,7 +155,9 @@ You should see an `error.message` property in that event, with a message from th } -------------------------------------------------- -And see the errors for the rules you might provide the next search query: +You might also see the errors for the rules, which can use in the next search +query. For example: + [source, txt] -------------------------------------------------- { diff --git a/docs/user/alerting/troubleshooting/testing-connectors.asciidoc b/docs/user/alerting/troubleshooting/testing-connectors.asciidoc index 64ba106655321a..fd5a897dfd4c35 100644 --- a/docs/user/alerting/troubleshooting/testing-connectors.asciidoc +++ b/docs/user/alerting/troubleshooting/testing-connectors.asciidoc @@ -15,9 +15,10 @@ image::user/alerting/images/email-connector-test.png[Rule management page with t image::user/alerting/images/teams-connector-test.png[Five clauses define the condition to detect] [float] -==== experimental[] Troubleshooting Connectors with `kbn-action` tool +==== experimental[] Troubleshooting connectors with the `kbn-action` tool -Executing an Email action via https://github.com/pmuellr/kbn-action[kbn-action]. In this example, is using a cloud deployment of the stack: +You can run an email action via https://github.com/pmuellr/kbn-action[kbn-action]. +In this example, it is a Cloud deployment of the {stack}: [source, txt] -------------------------------------------------- @@ -44,7 +45,8 @@ $ kbn-action ls } ] -------------------------------------------------- -and then execute this: + +You can then run the following test: [source, txt] -------------------------------------------------- From ec2062ae4e38d96441b61227a3711faee8a37465 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 4 May 2022 15:24:56 -0700 Subject: [PATCH 11/83] [Reporting] Update docs to use screenshotting config properties (#127518) * [Reporting] Update docs to use screenshotting config properties * add the remaining capture settings moved from reporting Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/settings/reporting-settings.asciidoc | 42 +++++++++---------- ...porting-production-considerations.asciidoc | 4 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index b5e0af7e188a11..d7065267304595 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -63,6 +63,9 @@ Reporting generates reports in the background and jobs are coordinated using doc in {es}. Depending on how often you generate reports and the overall number of reports, you might need to change the following settings. +`xpack.reporting.capture.maxAttempts` {ess-icon}:: +If capturing a report fails for any reason, {kib} will re-queue the report job for retry, as many times as this setting. Defaults to `3`. + `xpack.reporting.queue.indexInterval`:: How often the index that stores reporting jobs rolls over to a new index. Valid values are `year`, `month`, `week`, `day`, and `hour`. Defaults to `week`. @@ -86,26 +89,23 @@ Specifies the {time-units}[time] that the reporting poller waits between polling [[reporting-capture-settings]] ==== Capture settings -Reporting works by capturing screenshots from {kib}. The following settings control the capturing process. +Reporting uses an internal "screenshotting" plugin to capture screenshots from {kib}. The following settings control the capturing process. -`xpack.reporting.capture.timeouts.openUrl` {ess-icon}:: +`xpack.screenshotting.capture.timeouts.openUrl` {ess-icon}:: Specify the {time-units}[time] to allow the Reporting browser to wait for the "Loading..." screen to dismiss and find the initial data for the page. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows a warning message. Can be specified as number of milliseconds. Defaults to `1m`. -`xpack.reporting.capture.timeouts.waitForElements` {ess-icon}:: +`xpack.screenshotting.capture.timeouts.waitForElements` {ess-icon}:: Specify the {time-units}[time] to allow the Reporting browser to wait for all visualization panels to load on the page. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows a warning message. Can be specified as number of milliseconds. Defaults to `30s`. -`xpack.reporting.capture.timeouts.renderComplete` {ess-icon}:: +`xpack.screenshotting.capture.timeouts.renderComplete` {ess-icon}:: Specify the {time-units}[time] to allow the Reporting browser to wait for all visualizations to fetch and render the data. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows a warning message. Can be specified as number of milliseconds. Defaults to `30s`. -NOTE: If any timeouts from `xpack.reporting.capture.timeouts.*` settings occur when +NOTE: If any timeouts from `xpack.screenshotting.capture.timeouts.*` settings occur when running a report job, Reporting will log the error and try to continue capturing the page with a screenshot. As a result, a download will be available, but there will likely be errors in the visualizations in the report. -`xpack.reporting.capture.maxAttempts` {ess-icon}:: -If capturing a report fails for any reason, {kib} will re-attempt other reporting job, as many times as this setting. Defaults to `3`. - -`xpack.reporting.capture.loadDelay`:: +`xpack.screenshotting.capture.loadDelay`:: deprecated:[8.0.0,This setting has no effect.] Specify the {time-units}[amount of time] before taking a screenshot when visualizations are not evented. All visualizations that ship with {kib} are evented, so this setting should not have much effect. If you are seeing empty images instead of visualizations, try increasing this value. Defaults to `3s`. *NOTE*: This setting exists for backwards compatibility, but is unused and therefore does not have an affect on reporting performance. [float] @@ -114,16 +114,16 @@ deprecated:[8.0.0,This setting has no effect.] Specify the {time-units}[amount o For PDF and PNG reports, Reporting spawns a headless Chromium browser process on the server to load and capture a screenshot of the {kib} app. When installing {kib} on Linux and Windows platforms, the Chromium binary comes bundled with the {kib} download. For Mac platforms, the Chromium binary is downloaded the first time {kib} is started. -`xpack.reporting.capture.browser.chromium.disableSandbox`:: +`xpack.screenshotting.browser.chromium.disableSandbox`:: It is recommended that you research the feasibility of enabling unprivileged user namespaces. An exception is if you are running {kib} in Docker because the container runs in a user namespace with the built-in seccomp/bpf filters. For more information, refer to <>. Defaults to `false` for all operating systems except Debian and Red Hat Linux, which use `true`. -`xpack.reporting.capture.browser.chromium.proxy.enabled`:: -Enables the proxy for Chromium to use. When set to `true`, you must also specify the `xpack.reporting.capture.browser.chromium.proxy.server` setting. Defaults to `false`. +`xpack.screenshotting.browser.chromium.proxy.enabled`:: +Enables the proxy for Chromium to use. When set to `true`, you must also specify the `xpack.screenshotting.browser.chromium.proxy.server` setting. Defaults to `false`. -`xpack.reporting.capture.browser.chromium.proxy.server`:: +`xpack.screenshotting.browser.chromium.proxy.server`:: The uri for the proxy server. Providing the username and password for the proxy server via the uri is not supported. -`xpack.reporting.capture.browser.chromium.proxy.bypass`:: +`xpack.screenshotting.browser.chromium.proxy.bypass`:: An array of hosts that should not go through the proxy server and should use a direct connection instead. Examples of valid entries are "elastic.co", "*.elastic.co", ".elastic.co", ".elastic.co:5601". [float] @@ -136,13 +136,13 @@ If the Chromium browser is asked to send a request that violates the network pol NOTE: {kib} installations are not designed to be publicly accessible over the internet. The Reporting network policy and other capabilities of the Elastic Stack security features do not change this condition. -`xpack.reporting.capture.networkPolicy`:: +`xpack.screenshotting.networkPolicy`:: Capturing a screenshot from a {kib} page involves sending out requests for all the linked web assets. For example, a Markdown visualization can show an image from a remote server. -`xpack.reporting.capture.networkPolicy.enabled`:: +`xpack.screenshotting.networkPolicy.enabled`:: When `false`, disables the *Reporting* network policy. Defaults to `true`. -`xpack.reporting.capture.networkPolicy.rules`:: +`xpack.screenshotting.networkPolicy.rules`:: A policy is specified as an array of objects that describe what to allow or deny based on a host or protocol. If a host or protocol is not specified, the rule matches any host or protocol. The rule objects are evaluated sequentially from the beginning to the end of the array, and continue until there is a matching rule. If no rules allow a request, the request is denied. @@ -150,14 +150,14 @@ The rule objects are evaluated sequentially from the beginning to the end of the [source,yaml] ------------------------------------------------------- # Only allow requests to placeholder.com -xpack.reporting.capture.networkPolicy: +xpack.screenshotting.networkPolicy: rules: [ { allow: true, host: "placeholder.com" } ] ------------------------------------------------------- [source,yaml] ------------------------------------------------------- # Only allow requests to https://placeholder.com -xpack.reporting.capture.networkPolicy: +xpack.screenshotting.networkPolicy: rules: [ { allow: true, host: "placeholder.com", protocol: "https:" } ] ------------------------------------------------------- @@ -166,7 +166,7 @@ A final `allow` rule with no host or protocol allows all requests that are not e [source,yaml] ------------------------------------------------------- # Denies requests from http://placeholder.com, but anything else is allowed. -xpack.reporting.capture.networkPolicy: +xpack.screenshotting.networkPolicy: rules: [{ allow: false, host: "placeholder.com", protocol: "http:" }, { allow: true }]; ------------------------------------------------------- @@ -175,7 +175,7 @@ A network policy can be composed of multiple rules: [source,yaml] ------------------------------------------------------- # Allow any request to http://placeholder.com but for any other host, https is required -xpack.reporting.capture.networkPolicy +xpack.screenshotting.networkPolicy rules: [ { allow: true, host: "placeholder.com", protocol: "http:" }, { allow: true, protocol: "https:" }, diff --git a/docs/user/production-considerations/reporting-production-considerations.asciidoc b/docs/user/production-considerations/reporting-production-considerations.asciidoc index f673c4538f4431..20ea82be9c6e43 100644 --- a/docs/user/production-considerations/reporting-production-considerations.asciidoc +++ b/docs/user/production-considerations/reporting-production-considerations.asciidoc @@ -31,10 +31,10 @@ distributions don't have user namespaces enabled by default, or they require the automatically disable the sandbox when it is running on Debian because additional steps are required to enable unprivileged usernamespaces. In these situations, you'll see the following message in your {kib} startup logs: `Chromium sandbox provides an additional layer of protection, but is not supported for your OS. -Automatically setting 'xpack.reporting.capture.browser.chromium.disableSandbox: true'.` +Automatically setting 'xpack.screenshotting.browser.chromium.disableSandbox: true'.` Reporting automatically enables the Chromium sandbox at startup when a supported OS is detected. However, if your kernel is 3.8 or newer, it's -recommended to set `xpack.reporting.capture.browser.chromium.disableSandbox: false` in your `kibana.yml` to explicitly enable usernamespaces. +recommended to set `xpack.screenshotting.browser.chromium.disableSandbox: false` in your `kibana.yml` to explicitly enable usernamespaces. [float] [[reporting-docker-sandbox]] From 0ea637477785a287505ac25775f7c941a6850bdf Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 4 May 2022 17:42:04 -0500 Subject: [PATCH 12/83] fix "Default Saved Object Field Metrics" task in main on-merge job --- .buildkite/scripts/saved_object_field_metrics.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.buildkite/scripts/saved_object_field_metrics.sh b/.buildkite/scripts/saved_object_field_metrics.sh index 3b6c63eeff3b0c..4cc249db20edc0 100755 --- a/.buildkite/scripts/saved_object_field_metrics.sh +++ b/.buildkite/scripts/saved_object_field_metrics.sh @@ -5,9 +5,8 @@ set -euo pipefail source .buildkite/scripts/common/util.sh echo '--- Default Saved Object Field Metrics' -cd "$XPACK_DIR" checks-reporter-with-killswitch "Capture Kibana Saved Objects field count metrics" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/saved_objects_field_count/config.ts + --config x-pack/test/saved_objects_field_count/config.ts From 8d54b1547b409af31fff568690d08e9d28cc2732 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 4 May 2022 18:00:03 -0500 Subject: [PATCH 13/83] include test_group_env when running saved_object_field_metrics.sh --- .buildkite/scripts/steps/on_merge_build_and_metrics.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.buildkite/scripts/steps/on_merge_build_and_metrics.sh b/.buildkite/scripts/steps/on_merge_build_and_metrics.sh index fb05bb99b0c54b..de46ba58e9d527 100755 --- a/.buildkite/scripts/steps/on_merge_build_and_metrics.sh +++ b/.buildkite/scripts/steps/on_merge_build_and_metrics.sh @@ -7,4 +7,6 @@ set -euo pipefail .buildkite/scripts/build_kibana_plugins.sh .buildkite/scripts/post_build_kibana_plugins.sh .buildkite/scripts/post_build_kibana.sh + +source ".buildkite/scripts/steps/test/test_group_env.sh" .buildkite/scripts/saved_object_field_metrics.sh From 99c659c58a094fbd60ced483807d783b965e28c7 Mon Sep 17 00:00:00 2001 From: Irina Truong Date: Wed, 4 May 2022 16:49:13 -0700 Subject: [PATCH 14/83] Fix for Insufficient permissions error in Enterprise Search plugin when filtering traffic or Enterprise Search is down (#131472) * Fix tests. * Remove debug code. * Force CI to rerun. * Review feedback. * Well this didn't work. * Nope, didn't work either. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/check_access.test.ts | 11 +++++++++++ .../enterprise_search/server/lib/check_access.ts | 4 ++-- .../lib/enterprise_search_config_api.test.ts | 13 ++++++++++++- .../server/lib/enterprise_search_config_api.ts | 14 +++++++++++++- .../server/routes/enterprise_search/config_data.ts | 7 ++++++- 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts index 28d9fe363ff0fe..3da63a63828ae5 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts @@ -196,6 +196,17 @@ describe('checkAccess', () => { hasWorkplaceSearchAccess: false, }); }); + + it('falls back to no access if response error', async () => { + (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({ + responseStatus: 500, + responseStatusText: 'failed', + })); + expect(await checkAccess(mockDependencies)).toEqual({ + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, + }); + }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.ts index a77415f2c2f127..444fa9d4fdb294 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.ts @@ -98,6 +98,6 @@ export const checkAccess = async ({ // When enterpriseSearch.host is defined in kibana.yml, // make a HTTP call which returns product access - const { access } = (await callEnterpriseSearchConfigAPI({ request, config, log })) || {}; - return access || DENY_ALL_PLUGINS; + const response = (await callEnterpriseSearchConfigAPI({ request, config, log })) || {}; + return 'access' in response ? response.access || DENY_ALL_PLUGINS : DENY_ALL_PLUGINS; }; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index 5f8b261f82f162..ad55b41c02ceeb 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -207,8 +207,19 @@ describe('callEnterpriseSearchConfigAPI', () => { (fetch as unknown as jest.Mock).mockReturnValueOnce(Promise.resolve('Bad Data')); expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({}); expect(mockDependencies.log.error).toHaveBeenCalledWith( - 'Could not perform access check to Enterprise Search: TypeError: response.json is not a function' + 'Could not perform access check to Enterprise Search: 500' + ); + + (fetch as unknown as jest.Mock).mockReturnValueOnce( + Promise.resolve( + new Response('{}', { + status: 500, + statusText: 'I failed', + }) + ) ); + const expected = { responseStatus: 500, responseStatusText: 'I failed' }; + expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual(expected); }); it('handles timeouts', async () => { diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index 7e27480426525f..361a5613ab67e2 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -26,6 +26,10 @@ interface Params { interface Return extends InitialAppData { publicUrl?: string; } +interface ResponseError { + responseStatus: number; + responseStatusText: string; +} /** * Calls an internal Enterprise Search API endpoint which returns @@ -38,7 +42,7 @@ export const callEnterpriseSearchConfigAPI = async ({ config, log, request, -}: Params): Promise => { +}: Params): Promise => { if (!config.host) return {}; const TIMEOUT_WARNING = `Enterprise Search access check took over ${config.accessCheckTimeoutWarning}ms. Please ensure your Enterprise Search server is responding normally and not adversely impacting Kibana load speeds.`; @@ -63,6 +67,14 @@ export const callEnterpriseSearchConfigAPI = async ({ }; const response = await fetch(enterpriseSearchUrl, options); + + if (!response.ok) { + return { + responseStatus: response.status, + responseStatusText: response.statusText, + }; + } + const data = await response.json(); warnMismatchedVersions(data?.version?.number, log); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts index 95ab8e3c3a5a9e..5be5bf8cc03735 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts @@ -17,7 +17,12 @@ export function registerConfigDataRoute({ router, config, log }: RouteDependenci async (context, request, response) => { const data = await callEnterpriseSearchConfigAPI({ request, config, log }); - if (!Object.keys(data).length) { + if ('responseStatus' in data) { + return response.customError({ + statusCode: data.responseStatus, + body: 'Error fetching data from Enterprise Search', + }); + } else if (!Object.keys(data).length) { return response.customError({ statusCode: 502, body: 'Error fetching data from Enterprise Search', From 79fc3815a4295edcfc895d83564ba90f6acfda04 Mon Sep 17 00:00:00 2001 From: jbyroads <45469917+jbyroads@users.noreply.github.com> Date: Wed, 4 May 2022 22:30:45 -0400 Subject: [PATCH 15/83] [ui] enhanced management landing text to update (#130426) index patterns to data views Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Spencer --- src/plugins/management/public/components/landing/landing.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/management/public/components/landing/landing.tsx b/src/plugins/management/public/components/landing/landing.tsx index 6ebf9804209181..0851ba3343eeea 100644 --- a/src/plugins/management/public/components/landing/landing.tsx +++ b/src/plugins/management/public/components/landing/landing.tsx @@ -46,7 +46,7 @@ export const ManagementLandingPage = ({

From fe92ccfbc411f2055ea29e0cb4805f3710659f72 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Thu, 5 May 2022 01:19:46 -0400 Subject: [PATCH 16/83] [Security Solution][Admin][Policy] Adds policy list functional tests (#129786) --- .../view/components/policy_endpoint_count.tsx | 10 +- .../pages/policy/view/policy_list.test.tsx | 12 +- .../pages/policy/view/policy_list.tsx | 8 +- .../apps/endpoint/endpoint_list.ts | 5 +- .../apps/endpoint/index.ts | 1 + .../apps/endpoint/policy_details.ts | 2 + .../apps/endpoint/policy_list.ts | 111 ++++++++++++++++++ .../page_objects/endpoint_page.ts | 4 + .../page_objects/policy_page.ts | 18 ++- 9 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/policy_endpoint_count.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/policy_endpoint_count.tsx index 08c2f1909339d7..b8b2f98173f21b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/policy_endpoint_count.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/policy_endpoint_count.tsx @@ -25,7 +25,7 @@ export const PolicyEndpointCount = memo< policyId: string; nonLinkCondition: boolean; } ->(({ policyId, nonLinkCondition, children, ...otherProps }) => { +>(({ policyId, nonLinkCondition, 'data-test-subj': dataTestSubj, children, ...otherProps }) => { const filterByPolicyQuery = `(language:kuery,query:'united.endpoint.Endpoint.policy.applied.id : "${policyId}"')`; const { search } = useLocation(); const { getAppUrl } = useAppUrl(); @@ -59,11 +59,15 @@ export const PolicyEndpointCount = memo< const clickHandler = useNavigateByRouterEventHandler(toRoutePathWithBackOptions); if (nonLinkCondition) { - return {children}; + return ( + + {children} + + ); } return ( // eslint-disable-next-line @elastic/eui/href-or-on-click - + {children} ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index 482fe0ba064bdf..3ea50af79a9c3c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -99,15 +99,23 @@ describe('When on the policy list page', () => { expect(policyNameCells).toBeTruthy(); expect(policyNameCells.length).toBe(5); }); - it('should show a avatar for the Created by column', () => { + it('should show an avatar and name for the Created by column', () => { + const expectedAvatarName = policies.items[0].created_by; const createdByCells = renderResult.getAllByTestId('created-by-avatar'); + const firstCreatedByName = renderResult.getAllByTestId('created-by-name')[0]; expect(createdByCells).toBeTruthy(); expect(createdByCells.length).toBe(5); + expect(createdByCells[0].textContent).toEqual(expectedAvatarName.charAt(0)); + expect(firstCreatedByName.textContent).toEqual(expectedAvatarName); }); - it('should show a avatar for the Updated by column', () => { + it('should show an avatar and name for the Updated by column', () => { + const expectedAvatarName = policies.items[0].updated_by; const updatedByCells = renderResult.getAllByTestId('updated-by-avatar'); + const firstUpdatedByName = renderResult.getAllByTestId('updated-by-name')[0]; expect(updatedByCells).toBeTruthy(); expect(updatedByCells.length).toBe(5); + expect(updatedByCells[0].textContent).toEqual(expectedAvatarName.charAt(0)); + expect(firstUpdatedByName.textContent).toEqual(expectedAvatarName); }); it('should show the correct endpoint count', () => { const endpointCount = renderResult.getAllByTestId('policyEndpointCountLink'); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 884a8572ecfe28..67398cbe39a448 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -189,7 +189,9 @@ export const PolicyList = memo(() => { - {name} + + {name} + ); @@ -222,7 +224,9 @@ export const PolicyList = memo(() => { - {name} + + {name} + ); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 48dc4507372294..0168a15f21b6e8 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -82,7 +82,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await deleteAllDocsFromMetadataUnitedIndex(getService); await pageObjects.endpoint.navigateToEndpointList(); }); - it('finds no data in list and prompts onboarding to add policy', async () => { await testSubjects.exists('emptyPolicyTable'); }); @@ -99,7 +98,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { after(async () => { await deleteAllDocsFromMetadataCurrentIndex(getService); await deleteAllDocsFromMetadataUnitedIndex(getService); - await endpointTestResources.unloadEndpointData(indexedData); + if (indexedData) { + await endpointTestResources.unloadEndpointData(indexedData); + } }); it('finds page title', async () => { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index c776dcf91602fd..f74bd3b91cfceb 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -35,6 +35,7 @@ export default function (providerContext: FtrProviderContext) { await endpointTestResources.installOrUpgradeEndpointFleetPackage(); }); loadTestFile(require.resolve('./endpoint_list')); + loadTestFile(require.resolve('./policy_list')); loadTestFile(require.resolve('./policy_details')); loadTestFile(require.resolve('./endpoint_telemetry')); loadTestFile(require.resolve('./trusted_apps_list')); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 9bf6a19b4a5894..c95afc743c8392 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -331,6 +331,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should show event filters card and link should go back to policy', async () => { await testSubjects.existOrFail('eventFilters-fleet-integration-card'); + const eventFiltersCard = await testSubjects.find('eventFilters-fleet-integration-card'); + await pageObjects.ingestManagerCreatePackagePolicy.scrollToCenterOfWindow(eventFiltersCard); await (await testSubjects.find('eventFilters-link-to-exceptions')).click(); await testSubjects.existOrFail('policyDetailsPage'); await (await testSubjects.find('policyDetailsBackLink')).click(); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts new file mode 100644 index 00000000000000..7020babc4520b1 --- /dev/null +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { PolicyTestResourceInfo } from '../../services/endpoint_policy'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const browser = getService('browser'); + const pageObjects = getPageObjects([ + 'common', + 'endpoint', + 'policy', + 'endpointPageUtils', + 'ingestManagerCreatePackagePolicy', + 'trustedApps', + ]); + const testSubjects = getService('testSubjects'); + const policyTestResources = getService('policyTestResources'); + const endpointTestResources = getService('endpointTestResources'); + + describe('When on the Endpoint Policy List Page', () => { + before(async () => { + const endpointPackage = await policyTestResources.getEndpointPackage(); + await endpointTestResources.setMetadataTransformFrequency('1s', endpointPackage.version); + await browser.refresh(); + }); + + describe('with no policies', () => { + it('shows the empty page', async () => { + await pageObjects.policy.navigateToPolicyList(); + await testSubjects.existOrFail('emptyPolicyTable'); + }); + it('navigates to Fleet and ensures the integration page is loaded correctly', async () => { + const fleetButton = await testSubjects.find('onboardingStartButton'); + await fleetButton.click(); + await testSubjects.existOrFail('createPackagePolicy_pageTitle'); + expect(await testSubjects.getVisibleText('createPackagePolicy_pageTitle')).to.equal( + 'Add Endpoint Security integration' + ); + }); + it('navigates back to the policy list page', async () => { + const cancelButton = await testSubjects.find('createPackagePolicy_cancelBackLink'); + cancelButton.click(); + await pageObjects.policy.ensureIsOnListPage(); + }); + }); + describe('with policies', () => { + let indexedData: IndexedHostsAndAlertsResponse; + let policyInfo: PolicyTestResourceInfo; + before(async () => { + indexedData = await endpointTestResources.loadEndpointData(); + policyInfo = await policyTestResources.createPolicy(); + await browser.refresh(); + }); + after(async () => { + if (indexedData) { + await endpointTestResources.unloadEndpointData(indexedData); + } + if (policyInfo) { + await policyInfo.cleanup(); + } + }); + it('shows the policy list table', async () => { + await pageObjects.policy.navigateToPolicyList(); + await testSubjects.existOrFail('policyListTable'); + }); + it('navigates to the policy details page when the policy name is clicked and returns back to the policy list page using the header back button', async () => { + const policyName = (await testSubjects.findAll('policyNameCellLink'))[0]; + await policyName.click(); + await pageObjects.policy.ensureIsOnDetailsPage(); + const backButton = await testSubjects.find('policyDetailsBackLink'); + await backButton.click(); + await pageObjects.policy.ensureIsOnListPage(); + }); + describe('when the endpoint count link is clicked', () => { + it('navigates to the endpoint list page filtered by policy', async () => { + const endpointCount = (await testSubjects.findAll('policyEndpointCountLink'))[0]; + await endpointCount.click(); + await pageObjects.endpoint.ensureIsOnEndpointListPage(); + }); + it('admin searchbar contains the selected policy id', async () => { + const expectedPolicyId = indexedData.integrationPolicies[0].id; + await pageObjects.endpoint.ensureIsOnEndpointListPage(); + expect(await testSubjects.getVisibleText('adminSearchBar')).to.equal( + `united.endpoint.Endpoint.policy.applied.id : "${expectedPolicyId}"` + ); + }); + it('endpoint table shows the endpoints associated with selected policy', async () => { + const expectedPolicyName = indexedData.integrationPolicies[0].name; + await pageObjects.endpoint.ensureIsOnEndpointListPage(); + const policyName = (await testSubjects.findAll('policyNameCellLink'))[0]; + expect(await policyName.getVisibleText()).to.be.equal( + expectedPolicyName.substring(0, expectedPolicyName.indexOf('-')) + ); + }); + it('returns back to the policy list page when the header back button is clicked', async () => { + await pageObjects.endpoint.ensureIsOnEndpointListPage(); + const backButton = await testSubjects.find('endpointListBackLink'); + await backButton.click(); + await pageObjects.policy.ensureIsOnListPage(); + }); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts b/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts index d4e2479d24bc7a..21d7b25cabb34a 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts @@ -25,6 +25,10 @@ export function EndpointPageProvider({ getService, getPageObjects }: FtrProvider await pageObjects.header.waitUntilLoadingHasFinished(); }, + async ensureIsOnEndpointListPage() { + await testSubjects.existOrFail('endpointPage'); + }, + async waitForTableToHaveData(dataTestSubj: string, timeout = 2000) { await retry.waitForWithTimeout('table to have data', timeout, async () => { const tableData = await pageObjects.endpointPageUtils.tableData(dataTestSubj); diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index b5eccd0ef11472..da4fb936d66554 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -14,6 +14,16 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr const retryService = getService('retry'); return { + /** + * Navigates to the Endpoint Policy List page + */ + async navigateToPolicyList() { + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'securitySolutionManagement', + `/policy` + ); + await pageObjects.header.waitUntilLoadingHasFinished(); + }, /** * Navigates to the Endpoint Policy Details page * @@ -27,6 +37,12 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr await pageObjects.header.waitUntilLoadingHasFinished(); }, + /** + * Ensures the current page is the policy list page + */ + async ensureIsOnListPage() { + await testSubjects.existOrFail('policyListPage'); + }, /** * Finds and returns the Policy Details Page Save button */ @@ -127,7 +143,7 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr /** * Used when looking a the Ingest create/edit package policy pages. Finds the endpoint - * custom configuaration component + * custom configuration component * @param onEditPage */ async findPackagePolicyEndpointCustomConfiguration(onEditPage: boolean = false) { From 880c3218b418187e79d2e7f3294ac4fb96588504 Mon Sep 17 00:00:00 2001 From: Hrant Muradyan <69071631+hro-maker@users.noreply.github.com> Date: Thu, 5 May 2022 10:18:27 +0400 Subject: [PATCH 17/83] [Console] Fix Kibana DevTool Copy as CURL does not url encode special chars in indice date math. (#130970) * Fix cURL encoding for ES and Kibana requests. Add unit tests * move changing global object in beforeEach Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__fixtures__/editor_input1.txt | 4 +++ .../models/sense_editor/sense_editor.test.js | 30 +++++++++++++++++++ src/plugins/console/public/lib/es/es.ts | 22 ++++++++++---- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt b/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt index 398a0fdeab61f1..517f22bd8ad6aa 100644 --- a/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt +++ b/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt @@ -31,3 +31,7 @@ POST /_sql?format=txt "query": "SELECT prenom FROM claude_index WHERE prenom = 'claude' ", "fetch_size": 1 } + +GET ,,/_search?pretty + +GET kbn:/api/spaces/space \ No newline at end of file diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js index ff9d245f61275c..4751d3ca29863e 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js @@ -10,6 +10,7 @@ import './sense_editor.test.mocks'; import $ from 'jquery'; import _ from 'lodash'; +import { URL } from 'url'; import { create } from './create'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; @@ -19,6 +20,8 @@ const { collapseLiteralStrings } = XJson; describe('Editor', () => { let input; + let oldUrl; + let olldWindow; beforeEach(function () { // Set up our document body @@ -31,8 +34,19 @@ describe('Editor', () => { input = create(document.querySelector('#ConAppEditor')); $(input.getCoreEditor().getContainer()).show(); input.autocomplete._test.removeChangeListener(); + oldUrl = global.URL; + olldWindow = { ...global.window }; + global.URL = URL; + global.window = Object.create(window); + Object.defineProperty(window, 'location', { + value: { + origin: 'http://localhost:5620', + }, + }); }); afterEach(function () { + global.URL = oldUrl; + global.window = olldWindow; $(input.getCoreEditor().getContainer()).hide(); input.autocomplete._test.addChangeListener(); }); @@ -476,4 +490,20 @@ curl -XPOST "http://localhost:9200/_sql?format=txt" -H "kbn-xsrf: reporting" -H "fetch_size": 1 }'`.trim() ); + + multiReqCopyAsCurlTest( + 'with date math index', + editorInput1, + { start: { lineNumber: 35 }, end: { lineNumber: 35 } }, + ` + curl -XGET "http://localhost:9200/%3Cindex_1-%7Bnow%2Fd-2d%7D%3E%2C%3Cindex_1-%7Bnow%2Fd-1d%7D%3E%2C%3Cindex_1-%7Bnow%2Fd%7D%3E%2F_search?pretty" -H "kbn-xsrf: reporting"`.trim() + ); + + multiReqCopyAsCurlTest( + 'with Kibana API request', + editorInput1, + { start: { lineNumber: 37 }, end: { lineNumber: 37 } }, + ` +curl -XGET "http://localhost:5620/api/spaces/space" -H \"kbn-xsrf: reporting\"`.trim() + ); }); diff --git a/src/plugins/console/public/lib/es/es.ts b/src/plugins/console/public/lib/es/es.ts index 10d0ad95b0496f..5e22c78547b961 100644 --- a/src/plugins/console/public/lib/es/es.ts +++ b/src/plugins/console/public/lib/es/es.ts @@ -7,7 +7,7 @@ */ import type { HttpResponse, HttpSetup } from '@kbn/core/public'; -import { trimStart } from 'lodash'; +import { trimStart, trimEnd } from 'lodash'; import { API_BASE_PATH, KIBANA_API_PREFIX } from '../../../common/constants'; const esVersion: string[] = []; @@ -79,11 +79,23 @@ function getKibanaRequestUrl(path: string) { export function constructUrl(baseUri: string, path: string) { const kibanaRequestUrl = getKibanaRequestUrl(path); + let url = `${trimEnd(baseUri, '/')}/${trimStart(path, '/')}`; if (kibanaRequestUrl) { - return kibanaRequestUrl; + url = kibanaRequestUrl; } - baseUri = baseUri.replace(/\/+$/, ''); - path = path.replace(/^\/+/, ''); - return baseUri + '/' + path; + + const { origin, pathname, search } = new URL(url); + return `${origin}${encodePathname(pathname)}${search ?? ''}`; } + +const encodePathname = (path: string) => { + const decodedPath = new URLSearchParams(`path=${path}`).get('path') ?? ''; + + // Skip if it is valid + if (path === decodedPath) { + return path; + } + + return `/${encodeURIComponent(trimStart(decodedPath, '/'))}`; +}; From 1d1e1f6430f5a39063181da9c6b5069afdb18aa8 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Thu, 5 May 2022 09:44:11 +0200 Subject: [PATCH 18/83] [ci] extract apm traces after in single user benchmarking pipeline (#130777) * [ci] extract apm traces after in single user benchmarking pipeline * add performance-testing-dataset-extractor dependency * use BUILD_ID for queries * bump version for extractor, update script * bump extractor version * fix path * upload artifacts to the public bucket --- .buildkite/pipelines/performance/daily.yml | 7 +++ .../scalability_dataset_extraction.sh | 36 +++++++++++ package.json | 1 + x-pack/test/performance/config.playwright.ts | 1 + yarn.lock | 59 ++++++++++++++++++- 5 files changed, 102 insertions(+), 2 deletions(-) create mode 100755 .buildkite/scripts/steps/functional/scalability_dataset_extraction.sh diff --git a/.buildkite/pipelines/performance/daily.yml b/.buildkite/pipelines/performance/daily.yml index 658ab3a72f1863..07e73c27508a6d 100644 --- a/.buildkite/pipelines/performance/daily.yml +++ b/.buildkite/pipelines/performance/daily.yml @@ -17,6 +17,13 @@ steps: agents: queue: kb-static-ubuntu depends_on: build + key: tests + + - label: ':shipit: Performance Tests dataset extraction for scalability benchmarking' + command: .buildkite/scripts/steps/functional/scalability_dataset_extraction.sh + agents: + queue: n2-2 + depends_on: tests - wait: ~ continue_on_failure: true diff --git a/.buildkite/scripts/steps/functional/scalability_dataset_extraction.sh b/.buildkite/scripts/steps/functional/scalability_dataset_extraction.sh new file mode 100755 index 00000000000000..b2ce23db38fdb7 --- /dev/null +++ b/.buildkite/scripts/steps/functional/scalability_dataset_extraction.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +USER_FROM_VAULT="$(retry 5 5 vault read -field=username secret/kibana-issues/dev/apm_parser_performance)" +PASS_FROM_VAULT="$(retry 5 5 vault read -field=password secret/kibana-issues/dev/apm_parser_performance)" +ES_SERVER_URL="https://kibana-ops-e2e-perf.es.us-central1.gcp.cloud.es.io:9243" +BUILD_ID=${BUILDKITE_BUILD_ID} +GCS_BUCKET="gs://kibana-performance/scalability-tests" + +.buildkite/scripts/bootstrap.sh + +echo "--- Extract APM metrics" +journeys=("login" "ecommerce_dashboard" "flight_dashboard" "web_logs_dashboard" "promotion_tracking_dashboard" "many_fields_discover") + +for i in "${journeys[@]}"; do + JOURNEY_NAME="${i}" + echo "Looking for JOURNEY=${JOURNEY_NAME} and BUILD_ID=${BUILD_ID} in APM traces" + + ./node_modules/.bin/performance-testing-dataset-extractor -u "${USER_FROM_VAULT}" -p "${PASS_FROM_VAULT}" -c "${ES_SERVER_URL}" -b "${BUILD_ID}" -n "${JOURNEY_NAME}" +done + +# archive json files with traces and upload as build artifacts +echo "--- Upload Kibana build, plugins and scalability traces to the public bucket" +mkdir "${BUILD_ID}" +tar -czf "${BUILD_ID}/scalability_traces.tar.gz" output +buildkite-agent artifact upload "${BUILD_ID}/scalability_traces.tar.gz" +buildkite-agent artifact download kibana-default.tar.gz ./"${BUILD_ID}" +buildkite-agent artifact download kibana-default-plugins.tar.gz ./"${BUILD_ID}" +echo "${BUILDKITE_COMMIT}" > "${BUILD_ID}/KIBANA_COMMIT_HASH" +gsutil -m cp -r "${BUILD_ID}" "${GCS_BUCKET}" +echo "--- Update reference to the latest CI build" +echo "${BUILD_ID}" > LATEST +gsutil cp LATEST "${GCS_BUCKET}" \ No newline at end of file diff --git a/package.json b/package.json index 2659301131a7a4..a896137ee34b2c 100644 --- a/package.json +++ b/package.json @@ -474,6 +474,7 @@ "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^6.0.0", + "@elastic/performance-testing-dataset-extractor": "^0.0.3", "@elastic/synthetics": "^1.0.0-beta.22", "@emotion/babel-preset-css-prop": "^11.2.0", "@emotion/jest": "^11.9.0", diff --git a/x-pack/test/performance/config.playwright.ts b/x-pack/test/performance/config.playwright.ts index 0b404d5c03bdb1..44a53d7be80a11 100644 --- a/x-pack/test/performance/config.playwright.ts +++ b/x-pack/test/performance/config.playwright.ts @@ -63,6 +63,7 @@ export default async function ({ readConfigFile, log }: FtrConfigProviderContext performancePhase: process.env.TEST_PERFORMANCE_PHASE, journeyName: process.env.JOURNEY_NAME, testJobId, + testBuildId, }) .filter(([, v]) => !!v) .reduce((acc, [k, v]) => (acc ? `${acc},${k}=${v}` : `${k}=${v}`), ''), diff --git a/yarn.lock b/yarn.lock index 2b4a4c4cebaa66..5b1d85c282b2bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1496,6 +1496,16 @@ dependencies: "@elastic/ecs-helpers" "^1.1.0" +"@elastic/elasticsearch@7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.17.0.tgz#589fb219234cf1b0da23744e82b1d25e2fe9a797" + integrity sha512-5QLPCjd0uLmLj1lSuKSThjNpq39f6NmlTy9ROLFwG5gjyTgpwSqufDeYG/Fm43Xs05uF7WcscoO7eguI3HuuYA== + dependencies: + debug "^4.3.1" + hpagent "^0.1.1" + ms "^2.1.3" + secure-json-parse "^2.4.0" + "@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@8.2.0-canary.2": version "8.2.0-canary.2" resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.2.0-canary.2.tgz#2513926cdbfe7c070e1fa6926f7829171b27cdba" @@ -1618,6 +1628,19 @@ resolved "https://registry.yarnpkg.com/@elastic/numeral/-/numeral-2.5.1.tgz#96acf39c3d599950646ef8ccfd24a3f057cf4932" integrity sha512-Tby6TKjixRFY+atVNeYUdGr9m0iaOq8230KTwn8BbUhkh7LwozfgKq0U98HRX7n63ZL62szl+cDKTYzh5WPCFQ== +"@elastic/performance-testing-dataset-extractor@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@elastic/performance-testing-dataset-extractor/-/performance-testing-dataset-extractor-0.0.3.tgz#c9823154c1d23c0dfec86f7183a5e2327999d0ca" + integrity sha512-ND33m4P1yOLPqnKnwWTcwDNB5dCw5NK9503e2WaZzljoy75RN9Lg5+YsQM7RFZKDs/+yNp7XRCJszeiUOcMFvg== + dependencies: + "@elastic/elasticsearch" "7.17.0" + axios "^0.26.1" + axios-curlirize "1.3.7" + lodash "^4.17.21" + qs "^6.10.3" + tslib "^2.3.1" + yargs "^17.4.0" + "@elastic/react-search-ui-views@1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@elastic/react-search-ui-views/-/react-search-ui-views-1.6.0.tgz#7211d47c29ef0636c853721491b9905ac7ae58da" @@ -8691,6 +8714,11 @@ axe-core@^4.2.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5" integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== +axios-curlirize@1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/axios-curlirize/-/axios-curlirize-1.3.7.tgz#0153c51a5af0e92370169daea33f234d588baad1" + integrity sha512-csSsuMyZj1dv1fL0zRPnDAHWrmlISMvK+wx9WJI/igRVDT4VMgbf2AVenaHghFLfI1nQijXUevYEguYV6u5hjA== + axios@^0.21.1: version "0.21.1" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" @@ -8712,6 +8740,13 @@ axios@^0.25.0: dependencies: follow-redirects "^1.14.7" +axios@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -14758,7 +14793,7 @@ follow-redirects@1.12.1: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6" integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg== -follow-redirects@^1.0.0, follow-redirects@^1.10.0, follow-redirects@^1.14.4, follow-redirects@^1.14.7: +follow-redirects@^1.0.0, follow-redirects@^1.10.0, follow-redirects@^1.14.4, follow-redirects@^1.14.7, follow-redirects@^1.14.8: version "1.14.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== @@ -16219,7 +16254,7 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" -hpagent@^0.1.2: +hpagent@^0.1.1, hpagent@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9" integrity sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ== @@ -23481,6 +23516,13 @@ qs@^6.10.0: dependencies: side-channel "^1.0.4" +qs@^6.10.3: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + dependencies: + side-channel "^1.0.4" + qs@^6.7.0: version "6.9.4" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" @@ -30656,6 +30698,19 @@ yargs@^17.0.1, yargs@^17.2.1, yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.0.0" +yargs@^17.4.0: + version "17.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.1.tgz#ebe23284207bb75cee7c408c33e722bfb27b5284" + integrity sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + yargs@^3.15.0: version "3.32.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" From a0294e59f1c712cd0e46c56e4aa667c9bc4b4f9d Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 5 May 2022 10:53:03 +0200 Subject: [PATCH 19/83] [Cases] Add the severity field to the cases API (#131394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add severity field to create API and migration * Adds integration test for severity field migration * remove exclusive test * Change severity levels * Update integration tests for post case * Add more integration tests * Fix all cases list test * Fix some server test * Fix util server test * Fix client util test * Convert event log's duration from number to string in Kibana (keep as "long" in Elasticsearch) (#130819) * Convert event.duration to string in TypeScript, keep as long in Elasticsearch * Fix jest test * Fix functional tests * Add ecsStringOrNumber to event log schema * Fix jest test * Add utility functions to event log plugin * Use new event log utility functions * PR fixes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * filter o11y rule aggregations (#131301) * [Cloud Posture] Display and save rules per benchmark (#131412) * Adding aria-label for discover data grid select document checkbox (#131277) * Update API docs (#130999) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [CI] Use GCS buckets for bazel remote caching (#131345) * [Actionable Observability] Add license modal to rules table (#131232) * Add fix license link * fix localization * fix CI error * fix more translation issues Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [RAM] Add shareable rule status filter (#130705) * rule state filter * turn off experiment * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Status filter API call * Fix tests * rename state to status, added tests * Address comments and fix tests * Revert experiment flag * Remove unused translations * Addressed comments Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> * [storybook] Watch for changes in packages (#131467) * [storybook] Watch for changes in packages * Update default_config.ts * Improve saved objects migrations failure errors and logs (#131359) * [Unified observability] Add tour step to guided setup (#131149) * [Lens] Improved interval input (#131372) * [Vega] Adjust vega doc for usage of ems files (#130948) * adjust vega doc * Update docs/user/dashboard/vega-reference.asciidoc Co-authored-by: Nick Peihl * Update docs/user/dashboard/vega-reference.asciidoc Co-authored-by: Nick Peihl * Update docs/user/dashboard/vega-reference.asciidoc Co-authored-by: Nick Peihl * Update docs/user/dashboard/vega-reference.asciidoc Co-authored-by: Nick Peihl * Update docs/user/dashboard/vega-reference.asciidoc Co-authored-by: Nick Peihl Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nick Peihl * Excess intersections * Create severity user action * Add severity to create_case user action * Fix and add integration tests * Minor improvements Co-authored-by: Mike Côté Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: mgiota Co-authored-by: Jordan <51442161+JordanSh@users.noreply.github.com> Co-authored-by: Bhavya RM Co-authored-by: Thomas Neirynck Co-authored-by: Brian Seeders Co-authored-by: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Co-authored-by: Clint Andrew Hall Co-authored-by: Christiane (Tina) Heiligers Co-authored-by: Alejandro Fernández Gómez Co-authored-by: Joe Reuter Co-authored-by: Nick Peihl Co-authored-by: Christos Nasikas --- x-pack/plugins/cases/common/api/cases/case.ts | 81 +++++++---- .../common/api/cases/user_actions/common.ts | 1 + .../api/cases/user_actions/create_case.ts | 1 + .../common/api/cases/user_actions/index.ts | 2 + .../common/api/cases/user_actions/severity.ts | 19 +++ .../plugins/cases/common/api/runtime_types.ts | 94 +++++++----- .../all_cases/all_cases_list.test.tsx | 3 +- .../components/user_actions/builder.tsx | 4 + .../plugins/cases/public/containers/mock.ts | 4 + .../cases/server/client/cases/create.ts | 5 +- .../plugins/cases/server/client/cases/mock.ts | 1 + .../plugins/cases/server/client/utils.test.ts | 71 --------- .../plugins/cases/server/common/utils.test.ts | 135 ++++++++++++++++++ x-pack/plugins/cases/server/common/utils.ts | 2 + .../api/__fixtures__/mock_saved_objects.ts | 5 + .../cases/server/saved_object_types/cases.ts | 3 + .../migrations/cases.test.ts | 44 +++++- .../saved_object_types/migrations/cases.ts | 12 +- .../saved_object_types/migrations/utils.ts | 9 +- .../cases/server/services/cases/index.test.ts | 2 + .../cases/server/services/test_utils.ts | 8 +- .../user_actions/builder_factory.test.ts | 36 +++++ .../services/user_actions/builder_factory.ts | 2 + .../user_actions/builders/severity.ts | 22 +++ .../services/user_actions/index.test.ts | 55 +++++++ .../server/services/user_actions/mocks.ts | 4 +- .../server/services/user_actions/types.ts | 4 + .../cases_api_integration/common/lib/mock.ts | 2 + .../tests/common/cases/import_export.ts | 6 + .../tests/common/cases/migrations.ts | 56 +++++--- .../tests/common/cases/patch_cases.ts | 45 ++++++ .../tests/common/cases/post_case.ts | 33 +++++ .../user_actions/get_all_user_actions.ts | 25 ++++ 33 files changed, 629 insertions(+), 167 deletions(-) create mode 100644 x-pack/plugins/cases/common/api/cases/user_actions/severity.ts create mode 100644 x-pack/plugins/cases/server/services/user_actions/builders/severity.ts diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 3f42e5b5c875c8..b3dbe4801f5443 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -39,6 +39,20 @@ export const SettingsRt = rt.type({ syncAlerts: rt.boolean, }); +export enum CaseSeverity { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', + CRITICAL = 'critical', +} + +export const CaseSeverityRt = rt.union([ + rt.literal(CaseSeverity.LOW), + rt.literal(CaseSeverity.MEDIUM), + rt.literal(CaseSeverity.HIGH), + rt.literal(CaseSeverity.CRITICAL), +]); + const CaseBasicRt = rt.type({ /** * The description of the case @@ -68,6 +82,10 @@ const CaseBasicRt = rt.type({ * The plugin owner of the case */ owner: rt.string, + /** + * The severity of the case + */ + severity: CaseSeverityRt, }); /** @@ -106,33 +124,42 @@ export const CaseAttributesRt = rt.intersection([ }), ]); -export const CasePostRequestRt = rt.type({ - /** - * Description of the case - */ - description: rt.string, - /** - * Identifiers for the case. - */ - tags: rt.array(rt.string), - /** - * Title of the case - */ - title: rt.string, - /** - * The external configuration for the case - */ - connector: CaseConnectorRt, - /** - * Sync settings for alerts - */ - settings: SettingsRt, - /** - * The owner here must match the string used when a plugin registers a feature with access to the cases plugin. The user - * creating this case must also be granted access to that plugin's feature. - */ - owner: rt.string, -}); +export const CasePostRequestRt = rt.intersection([ + rt.type({ + /** + * Description of the case + */ + description: rt.string, + /** + * Identifiers for the case. + */ + tags: rt.array(rt.string), + /** + * Title of the case + */ + title: rt.string, + /** + * The external configuration for the case + */ + connector: CaseConnectorRt, + /** + * Sync settings for alerts + */ + settings: SettingsRt, + /** + * The owner here must match the string used when a plugin registers a feature with access to the cases plugin. The user + * creating this case must also be granted access to that plugin's feature. + */ + owner: rt.string, + }), + rt.partial({ + /** + * The severity of the case. The severity is + * default it to "low" if not provided. + */ + severity: CaseSeverityRt, + }), +]); export const CasesFindRequestRt = rt.partial({ /** diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/common.ts b/x-pack/plugins/cases/common/api/cases/user_actions/common.ts index a6d12d135c142a..5665ab524071ae 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions/common.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions/common.ts @@ -17,6 +17,7 @@ export const ActionTypes = { title: 'title', status: 'status', settings: 'settings', + severity: 'severity', create_case: 'create_case', delete_case: 'delete_case', } as const; diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/create_case.ts b/x-pack/plugins/cases/common/api/cases/user_actions/create_case.ts index c491cc519132f3..53d2320b5afd40 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions/create_case.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions/create_case.ts @@ -23,6 +23,7 @@ export const CommonFieldsRt = rt.type({ const CommonPayloadAttributesRt = rt.type({ description: DescriptionUserActionPayloadRt.props.description, status: rt.string, + severity: rt.string, tags: TagsUserActionPayloadRt.props.tags, title: TitleUserActionPayloadRt.props.title, settings: SettingsUserActionPayloadRt.props.settings, diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/index.ts b/x-pack/plugins/cases/common/api/cases/user_actions/index.ts index 3f974d89fc79a6..d19ee5fcbe9f83 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions/index.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions/index.ts @@ -23,6 +23,7 @@ import { TitleUserActionRt } from './title'; import { SettingsUserActionRt } from './settings'; import { StatusUserActionRt } from './status'; import { DeleteCaseUserActionRt } from './delete_case'; +import { SeverityUserActionRt } from './severity'; export * from './common'; export * from './comment'; @@ -43,6 +44,7 @@ const CommonUserActionsRt = rt.union([ TitleUserActionRt, SettingsUserActionRt, StatusUserActionRt, + SeverityUserActionRt, ]); export const UserActionsRt = rt.union([ diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/severity.ts b/x-pack/plugins/cases/common/api/cases/user_actions/severity.ts new file mode 100644 index 00000000000000..2db5a0880dc09a --- /dev/null +++ b/x-pack/plugins/cases/common/api/cases/user_actions/severity.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { CaseSeverityRt } from '../case'; +import { ActionTypes, UserActionWithAttributes } from './common'; + +export const SeverityUserActionPayloadRt = rt.type({ severity: CaseSeverityRt }); + +export const SeverityUserActionRt = rt.type({ + type: rt.literal(ActionTypes.severity), + payload: SeverityUserActionPayloadRt, +}); + +export type SeverityUserAction = UserActionWithAttributes>; diff --git a/x-pack/plugins/cases/common/api/runtime_types.ts b/x-pack/plugins/cases/common/api/runtime_types.ts index c807d4b31b7515..0a31479b29da89 100644 --- a/x-pack/plugins/cases/common/api/runtime_types.ts +++ b/x-pack/plugins/cases/common/api/runtime_types.ts @@ -9,42 +9,18 @@ import { either, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; -import { isObject } from 'lodash/fp'; +import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; type ErrorFactory = (message: string) => Error; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts - * Bug fix for the TODO is in the format_errors package - */ -export const formatErrors = (errors: rt.Errors): string[] => { - const err = errors.map((error) => { - if (error.message != null) { - return error.message; - } else { - const keyContext = error.context - .filter( - (entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== '' - ) - .map((entry) => entry.key) - .join(','); - - const nameContext = error.context.find((entry) => { - // TODO: Put in fix for optional chaining https://github.com/cypress-io/cypress/issues/9298 - if (entry.type && entry.type.name) { - return entry.type.name.length > 0; - } - return false; - }); - const suppliedValue = - keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : ''; - const value = isObject(error.value) ? JSON.stringify(error.value) : error.value; - return `Invalid value "${value}" supplied to "${suppliedValue}"`; - } - }); - - return [...new Set(err)]; -}; +export type GenericIntersectionC = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any, any, any]>; export const createPlainError = (message: string) => new Error(message); @@ -57,6 +33,40 @@ export const decodeOrThrow = (inputValue: I) => pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); +const getProps = ( + codec: + | rt.HasProps + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.RecordC + | GenericIntersectionC +): rt.Props | null => { + if (codec == null) { + return null; + } + switch (codec._tag) { + case 'DictionaryType': + if (codec.codomain.props != null) { + return codec.codomain.props; + } + const dTypes: rt.HasProps[] = codec.codomain.types; + return dTypes.reduce((props, type) => Object.assign(props, getProps(type)), {}); + case 'RefinementType': + case 'ReadonlyType': + return getProps(codec.type); + case 'InterfaceType': + case 'StrictType': + case 'PartialType': + return codec.props; + case 'IntersectionType': + const iTypes = codec.types as rt.HasProps[]; + return iTypes.reduce((props, type) => { + return Object.assign(props, getProps(type) as rt.Props); + }, {} as rt.Props) as rt.Props; + default: + return null; + } +}; + const getExcessProps = (props: rt.Props, r: Record): string[] => { const ex: string[] = []; for (const k of Object.keys(r)) { @@ -67,15 +77,21 @@ const getExcessProps = (props: rt.Props, r: Record): string[] = return ex; }; -export function excess | rt.PartialType>( - codec: C -): C { +export function excess< + C extends rt.InterfaceType | GenericIntersectionC | rt.PartialType +>(codec: C): C { + const codecProps = getProps(codec); + const r = new rt.InterfaceType( codec.name, codec.is, (i, c) => either.chain(rt.UnknownRecord.validate(i, c), (s: Record) => { - const ex = getExcessProps(codec.props, s); + if (codecProps == null) { + return rt.failure(i, c, 'unknown codec'); + } + + const ex = getExcessProps(codecProps, s); return ex.length > 0 ? rt.failure( i, @@ -87,7 +103,7 @@ export function excess | rt.PartialType { username: 'lknope', }, description: 'Security banana Issue', + severity: CaseSeverity.LOW, duration: null, externalService: { connectorId: '123', diff --git a/x-pack/plugins/cases/public/components/user_actions/builder.tsx b/x-pack/plugins/cases/public/components/user_actions/builder.tsx index 5e1c11fbdd2df9..019e37396a7ce4 100644 --- a/x-pack/plugins/cases/public/components/user_actions/builder.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/builder.tsx @@ -20,6 +20,10 @@ export const builderMap: UserActionBuilderMap = { tags: createTagsUserActionBuilder, title: createTitleUserActionBuilder, status: createStatusUserActionBuilder, + // TODO: Build severity user action + severity: () => ({ + build: () => [], + }), pushed: createPushedUserActionBuilder, comment: createCommentUserActionBuilder, description: createDescriptionUserActionBuilder, diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index 8a31d8cac2b1e6..ed9e9ebd1ff8f0 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -31,6 +31,7 @@ import { UserActionTypes, UserActionWithResponse, CommentUserAction, + CaseSeverity, } from '../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; @@ -154,6 +155,7 @@ export const basicCase: Case = { fields: null, }, description: 'Security banana Issue', + severity: CaseSeverity.LOW, duration: null, externalService: null, status: CaseStatuses.open, @@ -247,6 +249,7 @@ export const mockCase: Case = { fields: null, }, duration: null, + severity: CaseSeverity.LOW, description: 'Security banana Issue', externalService: null, status: CaseStatuses.open, @@ -512,6 +515,7 @@ export const getUserAction = ( description: 'a desc', connector: { ...getJiraConnector() }, status: CaseStatuses.open, + severity: CaseSeverity.LOW, title: 'a title', tags: ['a tag'], settings: { syncAlerts: true }, diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index ab9f6a43058009..714c8199d11a56 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -14,12 +14,13 @@ import { SavedObjectsUtils } from '@kbn/core/server'; import { throwErrors, - excess, CaseResponseRt, CaseResponse, CasePostRequest, ActionTypes, CasePostRequestRt, + excess, + CaseSeverity, } from '../../../common/api'; import { MAX_TITLE_LENGTH } from '../../../common/constants'; import { isInvalidTag } from '../../../common/utils/validators'; @@ -85,7 +86,7 @@ export const create = async ( unsecuredSavedObjectsClient, caseId: newCase.id, user, - payload: query, + payload: { ...query, severity: query.severity ?? CaseSeverity.LOW }, owner: newCase.attributes.owner, }); diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 69a5f2d3a587b1..4c0698b209befe 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -240,6 +240,7 @@ export const userActions: CaseUserActionsResponse = [ }, settings: { syncAlerts: true }, status: 'open', + severity: 'low', owner: SECURITY_SOLUTION_OWNER, }, action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53', diff --git a/x-pack/plugins/cases/server/client/utils.test.ts b/x-pack/plugins/cases/server/client/utils.test.ts index 24e1135020a880..88140658c2b2b7 100644 --- a/x-pack/plugins/cases/server/client/utils.test.ts +++ b/x-pack/plugins/cases/server/client/utils.test.ts @@ -5,9 +5,6 @@ * 2.0. */ -import { CaseConnector, ConnectorTypes } from '../../common/api'; -import { newCase } from '../routes/api/__mocks__/request_responses'; -import { transformNewCase } from '../common/utils'; import { buildRangeFilter, sortToSnake } from './utils'; import { toElasticsearchQuery } from '@kbn/es-query'; @@ -38,74 +35,6 @@ describe('utils', () => { }); }); - describe('transformNewCase', () => { - beforeAll(() => { - jest.useFakeTimers('modern'); - jest.setSystemTime(new Date('2020-04-09T09:43:51.778Z')); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - const connector: CaseConnector = { - id: '123', - name: 'My connector', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: null }, - }; - it('transform correctly', () => { - const myCase = { - newCase: { ...newCase, connector }, - user: { - email: 'elastic@elastic.co', - full_name: 'Elastic', - username: 'elastic', - }, - }; - - const res = transformNewCase(myCase); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "connector": Object { - "fields": Object { - "issueType": "Task", - "parent": null, - "priority": "High", - }, - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2020-04-09T09:43:51.778Z", - "created_by": Object { - "email": "elastic@elastic.co", - "full_name": "Elastic", - "username": "elastic", - }, - "description": "A description", - "duration": null, - "external_service": null, - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "tags": Array [ - "new", - "case", - ], - "title": "My new case", - "updated_at": null, - "updated_by": null, - } - `); - }); - }); - describe('buildRangeFilter', () => { it('returns undefined if both the from and or are undefined', () => { const node = buildRangeFilter({}); diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 974c36bd0d8a6b..918a48863cac05 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -9,11 +9,14 @@ import { SavedObject, SavedObjectsFindResponse } from '@kbn/core/server'; import { makeLensEmbeddableFactory } from '@kbn/lens-plugin/server/embeddable/make_lens_embeddable_factory'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { + CaseConnector, CaseResponse, + CaseSeverity, CommentAttributes, CommentRequest, CommentRequestUserType, CommentType, + ConnectorTypes, } from '../../common/api'; import { mockCaseComments, mockCases } from '../routes/api/__fixtures__/mock_saved_objects'; import { @@ -29,7 +32,9 @@ import { extractLensReferencesFromCommentString, getOrUpdateLensReferences, asArray, + transformNewCase, } from './utils'; +import { newCase } from '../routes/api/__mocks__/request_responses'; interface CommentReference { ids: string[]; @@ -67,6 +72,128 @@ function createCommentFindResponse( } describe('common utils', () => { + describe('transformNewCase', () => { + beforeAll(() => { + jest.useFakeTimers('modern'); + jest.setSystemTime(new Date('2020-04-09T09:43:51.778Z')); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + const connector: CaseConnector = { + id: '123', + name: 'My connector', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }; + + it('transform correctly', () => { + const myCase = { + newCase: { ...newCase, connector }, + user: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + }; + + const res = transformNewCase(myCase); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "connector": Object { + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "elastic@elastic.co", + "full_name": "Elastic", + "username": "elastic", + }, + "description": "A description", + "duration": null, + "external_service": null, + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "severity": "low", + "status": "open", + "tags": Array [ + "new", + "case", + ], + "title": "My new case", + "updated_at": null, + "updated_by": null, + } + `); + }); + + it('transform correctly with severity provided', () => { + const myCase = { + newCase: { ...newCase, connector, severity: CaseSeverity.MEDIUM }, + user: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + }; + + const res = transformNewCase(myCase); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "connector": Object { + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "elastic@elastic.co", + "full_name": "Elastic", + "username": "elastic", + }, + "description": "A description", + "duration": null, + "external_service": null, + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "severity": "medium", + "status": "open", + "tags": Array [ + "new", + "case", + ], + "title": "My new case", + "updated_at": null, + "updated_by": null, + } + `); + }); + }); + describe('transformCases', () => { it('transforms correctly', () => { const casesMap = new Map( @@ -110,6 +237,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "defacement", @@ -149,6 +277,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "Data Destruction", @@ -192,6 +321,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "LOLBins", @@ -239,6 +369,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "closed", "tags": Array [ "LOLBins", @@ -303,6 +434,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "LOLBins", @@ -358,6 +490,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "LOLBins", @@ -436,6 +569,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "LOLBins", @@ -489,6 +623,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "defacement", diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index 11e77c5eb45798..bc8dbf8a6e842e 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -19,6 +19,7 @@ import { CaseAttributes, CasePostRequest, CaseResponse, + CaseSeverity, CasesFindResponse, CaseStatuses, CommentAttributes, @@ -56,6 +57,7 @@ export const transformNewCase = ({ }): CaseAttributes => ({ ...newCase, duration: null, + severity: newCase.severity ?? CaseSeverity.LOW, closed_at: null, closed_by: null, created_at: new Date().toISOString(), diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index cc45ef0e2d0690..77e1a64012c6d6 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -8,6 +8,7 @@ import { SavedObject } from '@kbn/core/server'; import { CaseAttributes, + CaseSeverity, CaseStatuses, CommentAttributes, CommentType, @@ -34,6 +35,7 @@ export const mockCases: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'This is a brand new case of a bad meanie defacing data', external_service: null, @@ -73,6 +75,7 @@ export const mockCases: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'Oh no, a bad meanie destroying data!', external_service: null, @@ -112,6 +115,7 @@ export const mockCases: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'Oh no, a bad meanie going LOLBins all over the place!', external_service: null, @@ -155,6 +159,7 @@ export const mockCases: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'Oh no, a bad meanie going LOLBins all over the place!', external_service: null, diff --git a/x-pack/plugins/cases/server/saved_object_types/cases.ts b/x-pack/plugins/cases/server/saved_object_types/cases.ts index ea68fc24f60ca7..9b2ea975c4dcd3 100644 --- a/x-pack/plugins/cases/server/saved_object_types/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/cases.ts @@ -152,6 +152,9 @@ export const createCaseSavedObjectType = ( }, }, }, + severity: { + type: 'keyword', + }, }, }, migrations: caseMigrations, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts index 70e0e91caa57fb..b4d3421643a41a 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts @@ -9,13 +9,14 @@ import { SavedObjectSanitizedDoc } from '@kbn/core/server'; import { CaseAttributes, CaseFullExternalService, + CaseSeverity, ConnectorTypes, NONE_CONNECTOR_ID, } from '../../../common/api'; import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { getNoneCaseConnector } from '../../common/utils'; import { createExternalService, ESCaseConnectorWithId } from '../../services/test_utils'; -import { addDuration, caseConnectorIdMigration, removeCaseType } from './cases'; +import { addDuration, addSeverity, caseConnectorIdMigration, removeCaseType } from './cases'; // eslint-disable-next-line @typescript-eslint/naming-convention const create_7_14_0_case = ({ @@ -496,4 +497,45 @@ describe('case migrations', () => { }); }); }); + + describe('add severity', () => { + it('adds the severity correctly when none is present', () => { + const doc = { + id: '123', + attributes: { + created_at: '2021-11-23T19:00:00Z', + closed_at: '2021-11-23T19:02:00Z', + }, + type: 'abc', + references: [], + } as unknown as SavedObjectSanitizedDoc; + expect(addSeverity(doc)).toEqual({ + ...doc, + attributes: { + ...doc.attributes, + severity: CaseSeverity.LOW, + }, + }); + }); + + it('keeps the existing value if the field already exists', () => { + const doc = { + id: '123', + attributes: { + severity: CaseSeverity.CRITICAL, + created_at: '2021-11-23T19:00:00Z', + closed_at: '2021-11-23T19:02:00Z', + }, + type: 'abc', + references: [], + } as unknown as SavedObjectSanitizedDoc; + expect(addSeverity(doc)).toEqual({ + ...doc, + attributes: { + ...doc.attributes, + severity: CaseSeverity.CRITICAL, + }, + }); + }); + }); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts index 91a462c5c80532..c4961f742abc76 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts @@ -11,7 +11,7 @@ import { cloneDeep, unset } from 'lodash'; import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from '@kbn/core/server'; import { addOwnerToSO, SanitizedCaseOwner } from '.'; import { ESConnectorFields } from '../../services'; -import { CaseAttributes, ConnectorTypes } from '../../../common/api'; +import { CaseAttributes, CaseSeverity, ConnectorTypes } from '../../../common/api'; import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME, @@ -21,6 +21,7 @@ import { transformPushConnectorIdToReference, } from './user_actions/connector_id'; import { CASE_TYPE_INDIVIDUAL } from './constants'; +import { pipeMigrations } from './utils'; interface UnsanitizedCaseConnector { connector_id: string; @@ -114,6 +115,13 @@ export const addDuration = ( return { ...doc, attributes: { ...doc.attributes, duration }, references: doc.references ?? [] }; }; +export const addSeverity = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectSanitizedDoc => { + const severity = doc.attributes.severity ?? CaseSeverity.LOW; + return { ...doc, attributes: { ...doc.attributes, severity }, references: doc.references ?? [] }; +}; + export const caseMigrations = { '7.10.0': ( doc: SavedObjectUnsanitizedDoc @@ -175,5 +183,5 @@ export const caseMigrations = { }, '7.15.0': caseConnectorIdMigration, '8.1.0': removeCaseType, - '8.3.0': addDuration, + '8.3.0': pipeMigrations(addDuration, addSeverity), }; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts index 65c1d42271845b..8996f891559497 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LogMeta, SavedObjectMigrationContext } from '@kbn/core/server'; +import { LogMeta, SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from '@kbn/core/server'; interface MigrationLogMeta extends LogMeta { migrations: { @@ -39,3 +39,10 @@ export function logError({ } ); } + +type CaseMigration = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc; + +export function pipeMigrations(...migrations: Array>): CaseMigration { + return (doc: SavedObjectUnsanitizedDoc) => + migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); +} diff --git a/x-pack/plugins/cases/server/services/cases/index.test.ts b/x-pack/plugins/cases/server/services/cases/index.test.ts index 84c580c8800e30..826a8d06e97f2a 100644 --- a/x-pack/plugins/cases/server/services/cases/index.test.ts +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -166,6 +166,7 @@ describe('CasesService', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "defacement", @@ -519,6 +520,7 @@ describe('CasesService', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "defacement", diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index 617dedd368ab3b..ff86783ae8e9c7 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -10,15 +10,18 @@ import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server'; import { ESConnectorFields } from '.'; import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../common/constants'; import { + CaseAttributes, CaseConnector, CaseExternalServiceBasic, CaseFullExternalService, + CaseSeverity, CaseStatuses, ConnectorTypes, NONE_CONNECTOR_ID, } from '../../common/api'; import { CASE_SAVED_OBJECT, SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './cases/types'; +import { getNoneCaseConnector } from '../common/utils'; /** * This is only a utility interface to help with constructing test cases. After the migration, the ES format will no longer @@ -96,7 +99,7 @@ export const createExternalService = ( ...overrides, }); -export const basicCaseFields = { +export const basicCaseFields: CaseAttributes = { closed_at: null, closed_by: null, created_at: '2019-11-25T21:54:48.952Z', @@ -105,6 +108,7 @@ export const basicCaseFields = { email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'This is a brand new case of a bad meanie defacing data', title: 'Super Bad Security Issue', @@ -116,6 +120,8 @@ export const basicCaseFields = { email: 'testemail@elastic.co', username: 'elastic', }, + connector: getNoneCaseConnector(), + external_service: null, settings: { syncAlerts: true, }, diff --git a/x-pack/plugins/cases/server/services/user_actions/builder_factory.test.ts b/x-pack/plugins/cases/server/services/user_actions/builder_factory.test.ts index 2e2a9e905bb7ee..ab349d690edef4 100644 --- a/x-pack/plugins/cases/server/services/user_actions/builder_factory.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/builder_factory.test.ts @@ -9,6 +9,7 @@ import { SECURITY_SOLUTION_OWNER } from '../../../common'; import { Actions, ActionTypes, + CaseSeverity, CaseStatuses, CommentType, ConnectorTypes, @@ -340,6 +341,40 @@ describe('UserActionBuilder', () => { `); }); + it('builds a severity user action correctly', () => { + const builder = builderFactory.getBuilder(ActionTypes.severity)!; + const userAction = builder.build({ + payload: { severity: CaseSeverity.LOW }, + ...commonArgs, + }); + + expect(userAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "update", + "created_at": "2022-01-09T22:00:00.000Z", + "created_by": Object { + "email": "elastic@elastic.co", + "full_name": "Elastic User", + "username": "elastic", + }, + "owner": "securitySolution", + "payload": Object { + "severity": "low", + }, + "type": "severity", + }, + "references": Array [ + Object { + "id": "123", + "name": "associated-cases", + "type": "cases", + }, + ], + } + `); + }); + it('builds a settings user action correctly', () => { const builder = builderFactory.getBuilder(ActionTypes.settings)!; const userAction = builder.build({ @@ -413,6 +448,7 @@ describe('UserActionBuilder', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "sir", diff --git a/x-pack/plugins/cases/server/services/user_actions/builder_factory.ts b/x-pack/plugins/cases/server/services/user_actions/builder_factory.ts index 5d5f33c2ae4f5a..510b6d12b1fa1f 100644 --- a/x-pack/plugins/cases/server/services/user_actions/builder_factory.ts +++ b/x-pack/plugins/cases/server/services/user_actions/builder_factory.ts @@ -17,6 +17,7 @@ import { TagsUserActionBuilder } from './builders/tags'; import { SettingsUserActionBuilder } from './builders/settings'; import { DeleteCaseUserActionBuilder } from './builders/delete_case'; import { UserActionBuilder } from './abstract_builder'; +import { SeverityUserActionBuilder } from './builders/severity'; const builderMap = { title: TitleUserActionBuilder, @@ -27,6 +28,7 @@ const builderMap = { pushed: PushedUserActionBuilder, tags: TagsUserActionBuilder, status: StatusUserActionBuilder, + severity: SeverityUserActionBuilder, settings: SettingsUserActionBuilder, delete_case: DeleteCaseUserActionBuilder, }; diff --git a/x-pack/plugins/cases/server/services/user_actions/builders/severity.ts b/x-pack/plugins/cases/server/services/user_actions/builders/severity.ts new file mode 100644 index 00000000000000..4abd5856972b42 --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/builders/severity.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Actions, ActionTypes } from '../../../../common/api'; +import { UserActionBuilder } from '../abstract_builder'; +import { UserActionParameters, BuilderReturnValue } from '../types'; + +export class SeverityUserActionBuilder extends UserActionBuilder { + build(args: UserActionParameters<'severity'>): BuilderReturnValue { + return this.buildCommonUserAction({ + ...args, + action: Actions.update, + valueKey: 'severity', + value: args.payload.severity, + type: ActionTypes.severity, + }); + } +} diff --git a/x-pack/plugins/cases/server/services/user_actions/index.test.ts b/x-pack/plugins/cases/server/services/user_actions/index.test.ts index eb1b57622d24d2..44e91bcae09d3e 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.test.ts @@ -13,6 +13,7 @@ import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server'; import { Actions, ActionTypes, + CaseSeverity, CaseStatuses, CaseUserActionAttributes, ConnectorUserAction, @@ -107,6 +108,7 @@ const createCaseUserAction = (): SavedObject => { description: 'a desc', settings: { syncAlerts: false }, status: CaseStatuses.open, + severity: CaseSeverity.LOW, tags: [], owner: SECURITY_SOLUTION_OWNER, }, @@ -447,6 +449,7 @@ describe('CaseUserActionService', () => { payload: casePayload, type: ActionTypes.create_case, }); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( 'cases-user-actions', { @@ -477,6 +480,7 @@ describe('CaseUserActionService', () => { owner: 'securitySolution', settings: { syncAlerts: true }, status: 'open', + severity: 'low', tags: ['sir'], title: 'Case SIR', }, @@ -517,6 +521,33 @@ describe('CaseUserActionService', () => { }); }); + describe('severity', () => { + it('creates an update severity user action', async () => { + await service.createUserAction({ + ...commonArgs, + payload: { severity: CaseSeverity.MEDIUM }, + type: ActionTypes.severity, + }); + + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( + 'cases-user-actions', + { + action: Actions.update, + created_at: '2022-01-09T22:00:00.000Z', + created_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic User', + username: 'elastic', + }, + type: 'severity', + owner: 'securitySolution', + payload: { severity: 'medium' }, + }, + { references: [{ id: '123', name: 'associated-cases', type: 'cases' }] } + ); + }); + }); + describe('push', () => { it('creates a push user action', async () => { await service.createUserAction({ @@ -801,6 +832,30 @@ describe('CaseUserActionService', () => { references: [{ id: '2', name: 'associated-cases', type: 'cases' }], type: 'cases-user-actions', }, + { + attributes: { + action: 'update', + created_at: '2022-01-09T22:00:00.000Z', + created_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic User', + username: 'elastic', + }, + owner: 'securitySolution', + payload: { + severity: 'critical', + }, + type: 'severity', + }, + references: [ + { + id: '2', + name: 'associated-cases', + type: 'cases', + }, + ], + type: 'cases-user-actions', + }, ]); }); }); diff --git a/x-pack/plugins/cases/server/services/user_actions/mocks.ts b/x-pack/plugins/cases/server/services/user_actions/mocks.ts index c745c040ac2ce7..bc35f98bf926ec 100644 --- a/x-pack/plugins/cases/server/services/user_actions/mocks.ts +++ b/x-pack/plugins/cases/server/services/user_actions/mocks.ts @@ -7,7 +7,7 @@ import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; -import { CaseStatuses, CommentType, ConnectorTypes } from '../../../common/api'; +import { CaseSeverity, CaseStatuses, CommentType, ConnectorTypes } from '../../../common/api'; import { createCaseSavedObjectResponse } from '../test_utils'; import { transformSavedObjectToExternalModel } from '../cases/transform'; @@ -30,6 +30,7 @@ export const casePayload = { }, }, settings: { syncAlerts: true }, + severity: CaseSeverity.LOW, owner: SECURITY_SOLUTION_OWNER, }; @@ -69,6 +70,7 @@ export const updatedCases = [ description: 'updated desc', tags: ['one', 'two'], settings: { syncAlerts: false }, + severity: CaseSeverity.CRITICAL, }, references: [], }, diff --git a/x-pack/plugins/cases/server/services/user_actions/types.ts b/x-pack/plugins/cases/server/services/user_actions/types.ts index f681a9186181cc..a60dee552a6bec 100644 --- a/x-pack/plugins/cases/server/services/user_actions/types.ts +++ b/x-pack/plugins/cases/server/services/user_actions/types.ts @@ -9,6 +9,7 @@ import { SavedObjectReference } from '@kbn/core/server'; import { CasePostRequest, CaseSettings, + CaseSeverity, CaseStatuses, CommentUserAction, ConnectorUserAction, @@ -28,6 +29,9 @@ export interface BuilderParameters { status: { parameters: { payload: { status: CaseStatuses } }; }; + severity: { + parameters: { payload: { severity: CaseSeverity } }; + }; tags: { parameters: { payload: { tags: string[] } }; }; diff --git a/x-pack/test/cases_api_integration/common/lib/mock.ts b/x-pack/test/cases_api_integration/common/lib/mock.ts index 34f1d4a9273d2d..08f30f8df024e4 100644 --- a/x-pack/test/cases_api_integration/common/lib/mock.ts +++ b/x-pack/test/cases_api_integration/common/lib/mock.ts @@ -17,6 +17,7 @@ import { CaseStatuses, CommentRequest, CommentRequestActionsType, + CaseSeverity, } from '@kbn/cases-plugin/common/api'; export const defaultUser = { email: null, full_name: null, username: 'elastic' }; @@ -86,6 +87,7 @@ export const postCaseResp = ( ...(id != null ? { id } : {}), comments: [], duration: null, + severity: req.severity ?? CaseSeverity.LOW, totalAlerts: 0, totalComment: 0, closed_by: null, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts index 44da07a845ff7a..2ce441c37e687d 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts @@ -27,6 +27,7 @@ import { CommentUserAction, CreateCaseUserAction, CaseStatuses, + CaseSeverity, } from '@kbn/cases-plugin/common/api'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { @@ -204,6 +205,10 @@ const expectExportToHaveCaseSavedObject = ( expect(createdCaseSO.attributes.connector.name).to.eql(caseRequest.connector.name); expect(createdCaseSO.attributes.connector.fields).to.eql([]); expect(createdCaseSO.attributes.settings).to.eql(caseRequest.settings); + expect(createdCaseSO.attributes.status).to.eql(CaseStatuses.open); + expect(createdCaseSO.attributes.severity).to.eql(CaseSeverity.LOW); + expect(createdCaseSO.attributes.duration).to.eql(null); + expect(createdCaseSO.attributes.tags).to.eql(caseRequest.tags); }; const expectExportToHaveUserActions = (objects: SavedObject[], caseRequest: CasePostRequest) => { @@ -239,6 +244,7 @@ const expectCaseCreateUserAction = ( expect(restParsedCreateCase).to.eql({ ...restCreateCase, status: CaseStatuses.open, + severity: CaseSeverity.LOW, }); expect(restParsedConnector).to.eql(restConnector); }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts index 3c43ac19329868..4d4b9d45b67173 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts @@ -369,7 +369,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); }); - describe('8.3.0 adding duration', () => { + describe('8.3.0', () => { before(async () => { await kibanaServer.importExport.load( 'x-pack/test/functional/fixtures/kbn_archiver/cases/8.2.0/cases_duration.json' @@ -383,34 +383,48 @@ export default function createGetTests({ getService }: FtrProviderContext) { await deleteAllCaseItems(es); }); - it('calculates the correct duration for closed cases', async () => { - const caseInfo = await getCase({ - supertest, - caseId: '4537b380-a512-11ec-b92f-859b9e89e434', + describe('adding duration', () => { + it('calculates the correct duration for closed cases', async () => { + const caseInfo = await getCase({ + supertest, + caseId: '4537b380-a512-11ec-b92f-859b9e89e434', + }); + + expect(caseInfo).to.have.property('duration'); + expect(caseInfo.duration).to.be(120); }); - expect(caseInfo).to.have.property('duration'); - expect(caseInfo.duration).to.be(120); - }); + it('sets the duration to null to open cases', async () => { + const caseInfo = await getCase({ + supertest, + caseId: '7537b580-a512-11ec-b94f-85979e89e434', + }); - it('sets the duration to null to open cases', async () => { - const caseInfo = await getCase({ - supertest, - caseId: '7537b580-a512-11ec-b94f-85979e89e434', + expect(caseInfo).to.have.property('duration'); + expect(caseInfo.duration).to.be(null); }); - expect(caseInfo).to.have.property('duration'); - expect(caseInfo.duration).to.be(null); - }); + it('sets the duration to null to in-progress cases', async () => { + const caseInfo = await getCase({ + supertest, + caseId: '1537b580-a512-11ec-b94f-85979e89e434', + }); - it('sets the duration to null to in-progress cases', async () => { - const caseInfo = await getCase({ - supertest, - caseId: '1537b580-a512-11ec-b94f-85979e89e434', + expect(caseInfo).to.have.property('duration'); + expect(caseInfo.duration).to.be(null); }); + }); - expect(caseInfo).to.have.property('duration'); - expect(caseInfo.duration).to.be(null); + describe('add severity', () => { + it('adds the severity field for existing documents', async () => { + const caseInfo = await getCase({ + supertest, + caseId: '4537b380-a512-11ec-b92f-859b9e89e434', + }); + + expect(caseInfo).to.have.property('severity'); + expect(caseInfo.severity).to.be('low'); + }); }); }); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 9ef1c3d5655e42..80dffef7cd3ee7 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -10,6 +10,7 @@ import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '@kbn/security-solution-plugin/common/constants'; import { + CaseSeverity, CasesResponse, CaseStatuses, CommentType, @@ -170,6 +171,34 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('should patch the severity of a case correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + // the default severity + expect(postedCase.severity).equal(CaseSeverity.LOW); + + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + severity: CaseSeverity.MEDIUM, + }, + ], + }, + }); + + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + + expect(data).to.eql({ + ...postCaseResp(), + severity: CaseSeverity.MEDIUM, + updated_by: defaultUser, + }); + }); + it('should patch a case with new connector', async () => { const postedCase = await createCase(supertest, postCaseReq); const patchedCases = await updateCase({ @@ -297,6 +326,22 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('400s when a wrong severity value is passed', async () => { + await updateCase({ + supertest, + params: { + cases: [ + { + version: 'version', + // @ts-expect-error + severity: 'wont-do', + }, + ], + }, + expectedHttpCode: 400, + }); + }); + it('400s when id is missing', async () => { await updateCase({ supertest, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts index 9cd986a032b24f..d4b52ff6f33949 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -12,6 +12,7 @@ import { ConnectorTypes, ConnectorJiraTypeFields, CaseStatuses, + CaseSeverity, } from '@kbn/cases-plugin/common/api'; import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock'; import { @@ -102,6 +103,32 @@ export default ({ getService }: FtrProviderContext): void => { ); }); + it('should post a case without severity', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql(postCaseResp(null, getPostCaseRequest())); + }); + + it('should post a case with severity', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + severity: CaseSeverity.HIGH, + }) + ); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql( + postCaseResp( + null, + getPostCaseRequest({ + severity: CaseSeverity.HIGH, + }) + ) + ); + }); + it('should create a user action when creating a case', async () => { const postedCase = await createCase(supertest, getPostCaseRequest()); const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); @@ -122,6 +149,7 @@ export default ({ getService }: FtrProviderContext): void => { settings: postedCase.settings, owner: postedCase.owner, status: CaseStatuses.open, + severity: CaseSeverity.LOW, }, }); }); @@ -207,6 +235,11 @@ export default ({ getService }: FtrProviderContext): void => { await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTags).expect(400); }); + it('400s when passing a wrong severity value', async () => { + // @ts-expect-error + await createCase(supertest, { ...getPostCaseRequest(), severity: 'very-severe' }, 400); + }); + it('400s if you passing status for a new case', async () => { const req = getPostCaseRequest(); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 283e6b0c5301bc..aacb5f6c8ae17f 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import { CaseResponse, + CaseSeverity, CaseStatuses, CommentType, ConnectorTypes, @@ -106,6 +107,30 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusUserAction.payload).to.eql({ status: 'closed' }); }); + it('creates a severity update user action when changing the severity', async () => { + const theCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: theCase.id, + version: theCase.version, + severity: CaseSeverity.HIGH, + }, + ], + }, + }); + + const userActions = await getCaseUserActions({ supertest, caseID: theCase.id }); + const statusUserAction = userActions[1]; + + expect(userActions.length).to.eql(2); + expect(statusUserAction.type).to.eql('severity'); + expect(statusUserAction.action).to.eql('update'); + expect(statusUserAction.payload).to.eql({ severity: 'high' }); + }); + it('creates a connector update user action', async () => { const newConnector = { id: '123', From 0650bd381978c450ce08957c7bcd69d5cb88c550 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 5 May 2022 11:24:03 +0200 Subject: [PATCH 20/83] [ML] Functional tests - stabilize outlier saved search tests (#131225) This PR stabilizes the outlier detection with saved searches functional tests. --- x-pack/test/accessibility/apps/ml.ts | 3 +- .../outlier_detection_creation.ts | 6 +-- ...outlier_detection_creation_saved_search.ts | 6 +-- .../ml/data_frame_analytics_creation.ts | 44 ++++++++++++++----- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index a783310d017067..2b99b665dacedb 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -251,8 +251,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.selectJobType(dfaJobType); await ml.testExecution.logTestStep('displays the source data preview'); await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists(); - await ml.testExecution.logTestStep('enables the source data preview histogram charts'); - await ml.dataFrameAnalyticsCreation.enableSourceDataPreviewHistogramCharts(true); + await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewHistogramChartEnabled(true); await ml.testExecution.logTestStep('displays the include fields selection'); await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); await a11y.testAppSnapshot(); diff --git a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts index 8a53528a899224..e9146ce5484223 100644 --- a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts @@ -160,12 +160,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the source data preview'); await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists(); - - await ml.testExecution.logTestStep('enables the source data preview histogram charts'); - await ml.dataFrameAnalyticsCreation.enableSourceDataPreviewHistogramCharts(true); + await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewHistogramChartEnabled(true); await ml.testExecution.logTestStep('displays the source data preview histogram charts'); - await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewHistogramCharts( + await ml.dataFrameAnalyticsCreation.enableAndAssertSourceDataPreviewHistogramCharts( testData.expected.histogramCharts ); diff --git a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts index 89247aed78ac4c..1e428531e6aa96 100644 --- a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts @@ -217,12 +217,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the source data preview'); await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists(); - - await ml.testExecution.logTestStep('enables the source data preview histogram charts'); - await ml.dataFrameAnalyticsCreation.enableSourceDataPreviewHistogramCharts(true); + await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewHistogramChartEnabled(true); await ml.testExecution.logTestStep('displays the source data preview histogram charts'); - await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewHistogramCharts( + await ml.dataFrameAnalyticsCreation.enableAndAssertSourceDataPreviewHistogramCharts( testData.expected.histogramCharts ); diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index 88ef0fdf08c8db..77f1e34e671575 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -49,6 +49,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( const jobTypeAttribute = `mlAnalyticsCreation-${jobType}-option`; await testSubjects.click(jobTypeAttribute); await this.assertJobTypeSelection(jobTypeAttribute); + await headerPage.waitUntilLoadingHasFinished(); }, async assertAdvancedEditorSwitchExists() { @@ -127,29 +128,41 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( await testSubjects.existOrFail('mlAnalyticsCreationDataGridHistogramButton'); }, - async enableSourceDataPreviewHistogramCharts(expectedDefaultButtonState: boolean) { - await this.assertSourceDataPreviewHistogramChartButtonCheckState(expectedDefaultButtonState); - if (expectedDefaultButtonState === false) { + async enableSourceDataPreviewHistogramCharts(shouldBeEnabled: boolean) { + const isEnabled = await this.getSourceDataPreviewHistogramChartButtonCheckState(); + if (isEnabled !== shouldBeEnabled) { await testSubjects.click('mlAnalyticsCreationDataGridHistogramButton'); - await this.assertSourceDataPreviewHistogramChartButtonCheckState(true); + await this.assertSourceDataPreviewHistogramChartEnabled(shouldBeEnabled); } }, - async assertSourceDataPreviewHistogramChartButtonCheckState(expectedCheckState: boolean) { - const actualCheckState = + async assertSourceDataPreviewHistogramChartEnabled(shouldBeEnabled: boolean) { + const isEnabled = await this.getSourceDataPreviewHistogramChartButtonCheckState(); + expect(isEnabled).to.eql( + shouldBeEnabled, + `Source data preview histogram charts should be '${ + shouldBeEnabled ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async getSourceDataPreviewHistogramChartButtonCheckState(): Promise { + return ( (await testSubjects.getAttribute( 'mlAnalyticsCreationDataGridHistogramButton', 'aria-pressed' - )) === 'true'; - expect(actualCheckState).to.eql( - expectedCheckState, - `Chart histogram button check state should be '${expectedCheckState}' (got '${actualCheckState}')` + )) === 'true' ); }, + async scrollSourceDataPreviewIntoView() { + await testSubjects.scrollIntoView('mlAnalyticsCreationDataGrid loaded'); + }, + async assertSourceDataPreviewHistogramCharts( expectedHistogramCharts: Array<{ chartAvailable: boolean; id: string; legend: string }> ) { + await this.scrollSourceDataPreviewIntoView(); // For each chart, get the content of each header cell and assert // the legend text and column id and if the chart should be present or not. await retry.tryForTime(10000, async () => { @@ -178,6 +191,17 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( }); }, + async enableAndAssertSourceDataPreviewHistogramCharts( + expectedHistogramCharts: Array<{ chartAvailable: boolean; id: string; legend: string }> + ) { + await retry.tryForTime(20 * 1000, async () => { + // turn histogram charts off and on before checking + await this.enableSourceDataPreviewHistogramCharts(false); + await this.enableSourceDataPreviewHistogramCharts(true); + await this.assertSourceDataPreviewHistogramCharts(expectedHistogramCharts); + }); + }, + async assertIncludeFieldsSelectionExists() { await testSubjects.existOrFail('mlAnalyticsCreateJobWizardIncludesTable', { timeout: 8000 }); From 58bc0f759e8de873bac9e9fa4607a0b2befa420d Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 5 May 2022 11:34:43 +0200 Subject: [PATCH 21/83] [Discover] Provide direct link from sample data UI to Discover (#130108) * [Discover] Allow to view sample data in Discover * [Discover] Update deps format * [Discover] Define order of items in the context menu * [Discover] Update for tests * [Discover] Add upgrade tests * [Discover] Add a test for ordering appLinks * [Discover] Use existing helpers * [Discover] Add 7 days time range to Discover link * [Discover] Rename the helper Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/discover/common/index.ts | 1 + .../common/services/saved_searches/index.ts | 9 ++ .../saved_searches/saved_searches_url.test.ts | 25 ++++++ .../saved_searches/saved_searches_url.ts | 11 +++ .../saved_searches_utils.test.ts | 16 ---- .../saved_searches/saved_searches_utils.ts | 7 +- src/plugins/discover/server/plugin.ts | 7 ++ .../discover/server/sample_data/index.ts | 9 ++ .../sample_data/register_sample_data.ts | 44 ++++++++++ .../sample_data_view_data_button.test.js.snap | 84 +++++++++++++++++++ .../components/sample_data_set_card.js | 1 + .../sample_data_view_data_button.js | 30 +++---- .../sample_data_view_data_button.test.js | 38 +++++++++ .../lib/sample_dataset_registry_types.ts | 7 ++ .../services/sample_data/routes/list.ts | 6 +- test/functional/page_objects/home_page.ts | 5 ++ .../apps/discover/discover_smoke_tests.ts | 39 +++++++-- 17 files changed, 298 insertions(+), 41 deletions(-) create mode 100644 src/plugins/discover/common/services/saved_searches/index.ts create mode 100644 src/plugins/discover/common/services/saved_searches/saved_searches_url.test.ts create mode 100644 src/plugins/discover/common/services/saved_searches/saved_searches_url.ts create mode 100644 src/plugins/discover/server/sample_data/index.ts create mode 100644 src/plugins/discover/server/sample_data/register_sample_data.ts diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 98ce5fc3b0b2be..173264aee731ee 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +export const APP_ICON = 'discoverApp'; export const DEFAULT_COLUMNS_SETTING = 'defaultColumns'; export const SAMPLE_SIZE_SETTING = 'discover:sampleSize'; export const SORT_DEFAULT_ORDER_SETTING = 'discover:sort:defaultOrder'; diff --git a/src/plugins/discover/common/services/saved_searches/index.ts b/src/plugins/discover/common/services/saved_searches/index.ts new file mode 100644 index 00000000000000..014fdb31ed438a --- /dev/null +++ b/src/plugins/discover/common/services/saved_searches/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getSavedSearchUrl, getSavedSearchFullPathUrl } from './saved_searches_url'; diff --git a/src/plugins/discover/common/services/saved_searches/saved_searches_url.test.ts b/src/plugins/discover/common/services/saved_searches/saved_searches_url.test.ts new file mode 100644 index 00000000000000..81f4498939b98b --- /dev/null +++ b/src/plugins/discover/common/services/saved_searches/saved_searches_url.test.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getSavedSearchUrl, getSavedSearchFullPathUrl } from './saved_searches_url'; + +describe('saved_searches_url', () => { + describe('getSavedSearchUrl', () => { + test('should return valid saved search url', () => { + expect(getSavedSearchUrl()).toBe('#/'); + expect(getSavedSearchUrl('id')).toBe('#/view/id'); + }); + }); + + describe('getSavedSearchFullPathUrl', () => { + test('should return valid full path url', () => { + expect(getSavedSearchFullPathUrl()).toBe('/app/discover#/'); + expect(getSavedSearchFullPathUrl('id')).toBe('/app/discover#/view/id'); + }); + }); +}); diff --git a/src/plugins/discover/common/services/saved_searches/saved_searches_url.ts b/src/plugins/discover/common/services/saved_searches/saved_searches_url.ts new file mode 100644 index 00000000000000..cc5ecdb61f565d --- /dev/null +++ b/src/plugins/discover/common/services/saved_searches/saved_searches_url.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const getSavedSearchUrl = (id?: string) => (id ? `#/view/${encodeURIComponent(id)}` : '#/'); + +export const getSavedSearchFullPathUrl = (id?: string) => `/app/discover${getSavedSearchUrl(id)}`; diff --git a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts index 4a4713badb807b..9b42d7557b05c8 100644 --- a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts +++ b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts @@ -7,8 +7,6 @@ */ import { - getSavedSearchUrl, - getSavedSearchFullPathUrl, fromSavedSearchAttributes, toSavedSearchAttributes, throwErrorOnSavedSearchUrlConflict, @@ -19,20 +17,6 @@ import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks'; import type { SavedSearchAttributes, SavedSearch } from './types'; describe('saved_searches_utils', () => { - describe('getSavedSearchUrl', () => { - test('should return valid saved search url', () => { - expect(getSavedSearchUrl()).toBe('#/'); - expect(getSavedSearchUrl('id')).toBe('#/view/id'); - }); - }); - - describe('getSavedSearchFullPathUrl', () => { - test('should return valid full path url', () => { - expect(getSavedSearchFullPathUrl()).toBe('/app/discover#/'); - expect(getSavedSearchFullPathUrl('id')).toBe('/app/discover#/view/id'); - }); - }); - describe('fromSavedSearchAttributes', () => { test('should convert attributes into SavedSearch', () => { const attributes: SavedSearchAttributes = { diff --git a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts index 4dbb84613ead83..26b3c0b7cf9b5a 100644 --- a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts +++ b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts @@ -8,9 +8,10 @@ import { i18n } from '@kbn/i18n'; import type { SavedSearchAttributes, SavedSearch } from './types'; -export const getSavedSearchUrl = (id?: string) => (id ? `#/view/${encodeURIComponent(id)}` : '#/'); - -export const getSavedSearchFullPathUrl = (id?: string) => `/app/discover${getSavedSearchUrl(id)}`; +export { + getSavedSearchUrl, + getSavedSearchFullPathUrl, +} from '../../../common/services/saved_searches'; export const getSavedSearchUrlConflictMessage = async (savedSearch: SavedSearch) => i18n.translate('discover.savedSearchEmbeddable.legacyURLConflict.errorMessage', { diff --git a/src/plugins/discover/server/plugin.ts b/src/plugins/discover/server/plugin.ts index 9147f533d28d66..888fcf55c23516 100644 --- a/src/plugins/discover/server/plugin.ts +++ b/src/plugins/discover/server/plugin.ts @@ -8,15 +8,18 @@ import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server'; import type { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server'; +import type { HomeServerPluginSetup } from '@kbn/home-plugin/server'; import { getUiSettings } from './ui_settings'; import { capabilitiesProvider } from './capabilities_provider'; import { getSavedSearchObjectType } from './saved_objects'; +import { registerSampleData } from './sample_data'; export class DiscoverServerPlugin implements Plugin { public setup( core: CoreSetup, plugins: { data: DataPluginSetup; + home?: HomeServerPluginSetup; } ) { const getSearchSourceMigrations = plugins.data.search.searchSource.getAllMigrations.bind( @@ -26,6 +29,10 @@ export class DiscoverServerPlugin implements Plugin { core.uiSettings.register(getUiSettings(core.docLinks)); core.savedObjects.registerType(getSavedSearchObjectType(getSearchSourceMigrations)); + if (plugins.home) { + registerSampleData(plugins.home.sampleData); + } + return {}; } diff --git a/src/plugins/discover/server/sample_data/index.ts b/src/plugins/discover/server/sample_data/index.ts new file mode 100644 index 00000000000000..43edd42293edf3 --- /dev/null +++ b/src/plugins/discover/server/sample_data/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { registerSampleData } from './register_sample_data'; diff --git a/src/plugins/discover/server/sample_data/register_sample_data.ts b/src/plugins/discover/server/sample_data/register_sample_data.ts new file mode 100644 index 00000000000000..a1ff9951d9179d --- /dev/null +++ b/src/plugins/discover/server/sample_data/register_sample_data.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { SampleDataRegistrySetup } from '@kbn/home-plugin/server'; +import { APP_ICON } from '../../common'; +import { getSavedSearchFullPathUrl } from '../../common/services/saved_searches'; + +function getDiscoverPathForSampleDataset(objId: string) { + // TODO: remove the time range from the URL query when saved search objects start supporting time range configuration + // https://github.com/elastic/kibana/issues/9761 + return `${getSavedSearchFullPathUrl(objId)}?_g=(time:(from:now-7d,to:now))`; +} + +export function registerSampleData(sampleDataRegistry: SampleDataRegistrySetup) { + const linkLabel = i18n.translate('discover.sampleData.viewLinkLabel', { + defaultMessage: 'Discover', + }); + const { addAppLinksToSampleDataset, getSampleDatasets } = sampleDataRegistry; + const sampleDatasets = getSampleDatasets(); + + sampleDatasets.forEach((sampleDataset) => { + const sampleSavedSearchObject = sampleDataset.savedObjects.find( + (object) => object.type === 'search' + ); + + if (sampleSavedSearchObject) { + addAppLinksToSampleDataset(sampleDataset.id, [ + { + sampleObject: sampleSavedSearchObject, + getPath: getDiscoverPathForSampleDataset, + label: linkLabel, + icon: APP_ICON, + order: -1, + }, + ]); + } + }); +} diff --git a/src/plugins/home/public/application/components/__snapshots__/sample_data_view_data_button.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/sample_data_view_data_button.test.js.snap index d9e341394ee00d..0d634049305ad6 100644 --- a/src/plugins/home/public/application/components/__snapshots__/sample_data_view_data_button.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/sample_data_view_data_button.test.js.snap @@ -57,6 +57,90 @@ exports[`should render popover when appLinks is not empty 1`] = ` `; +exports[`should render popover with ordered appLinks 1`] = ` + + View data + + } + closePopover={[Function]} + data-test-subj="launchSampleDataSetecommerce" + display="inlineBlock" + hasArrow={true} + id="sampleDataLinksecommerce" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + , + "name": "myAppLabel[-1]", + "onClick": [Function], + }, + Object { + "data-test-subj": "viewSampleDataSetecommerce-dashboard", + "href": "root/app/dashboards#/view/722b74f0-b882-11e8-a6d9-e546fe2bba5f", + "icon": , + "name": "Dashboard", + "onClick": [Function], + }, + Object { + "href": "rootapp/myAppPath", + "icon": , + "name": "myAppLabel[3]", + "onClick": [Function], + }, + Object { + "href": "rootapp/myAppPath", + "icon": , + "name": "myAppLabel[5]", + "onClick": [Function], + }, + Object { + "href": "rootapp/myAppPath", + "icon": , + "name": "myAppLabel", + "onClick": [Function], + }, + ], + }, + ] + } + size="m" + /> + +`; + exports[`should render simple button when appLinks is empty 1`] = ` { + const dashboardAppLink = { + path: dashboardPath, + label: i18n.translate('home.sampleDataSetCard.dashboardLinkLabel', { + defaultMessage: 'Dashboard', + }), + icon: 'dashboardApp', + order: 0, + 'data-test-subj': `viewSampleDataSet${this.props.id}-dashboard`, + }; + + const sortedItems = sortBy([dashboardAppLink, ...this.props.appLinks], 'order'); + const items = sortedItems.map(({ path, label, icon, ...rest }) => { return { name: label, icon: , href: this.addBasePath(path), onClick: createAppNavigationHandler(path), + ...(rest['data-test-subj'] ? { 'data-test-subj': rest['data-test-subj'] } : {}), }; }); @@ -75,18 +87,7 @@ export class SampleDataViewDataButton extends React.Component { const panels = [ { id: 0, - items: [ - { - name: i18n.translate('home.sampleDataSetCard.dashboardLinkLabel', { - defaultMessage: 'Dashboard', - }), - icon: , - href: prefixedDashboardPath, - onClick: createAppNavigationHandler(dashboardPath), - 'data-test-subj': `viewSampleDataSet${this.props.id}-dashboard`, - }, - ...additionalItems, - ], + items, }, ]; const popoverButton = ( @@ -124,6 +125,7 @@ SampleDataViewDataButton.propTypes = { path: PropTypes.string.isRequired, label: PropTypes.string.isRequired, icon: PropTypes.string.isRequired, + order: PropTypes.number, }) ).isRequired, }; diff --git a/src/plugins/home/public/application/components/sample_data_view_data_button.test.js b/src/plugins/home/public/application/components/sample_data_view_data_button.test.js index b097b5e3225005..f3cfd5a7a661ee 100644 --- a/src/plugins/home/public/application/components/sample_data_view_data_button.test.js +++ b/src/plugins/home/public/application/components/sample_data_view_data_button.test.js @@ -48,3 +48,41 @@ test('should render popover when appLinks is not empty', () => { ); expect(component).toMatchSnapshot(); // eslint-disable-line }); + +test('should render popover with ordered appLinks', () => { + const appLinks = [ + { + path: 'app/myAppPath', + label: 'myAppLabel[-1]', + icon: 'logoKibana', + order: -1, // to position it above Dashboard link + }, + { + path: 'app/myAppPath', + label: 'myAppLabel', + icon: 'logoKibana', + }, + { + path: 'app/myAppPath', + label: 'myAppLabel[5]', + icon: 'logoKibana', + order: 5, + }, + { + path: 'app/myAppPath', + label: 'myAppLabel[3]', + icon: 'logoKibana', + order: 3, + }, + ]; + + const component = shallow( + + ); + expect(component).toMatchSnapshot(); // eslint-disable-line +}); diff --git a/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts b/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts index 8d26d08460b5b2..9b1212e13b0245 100644 --- a/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts +++ b/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts @@ -58,4 +58,11 @@ export interface AppLinkData { * The icon for this app link. */ icon: string; + /** + * Index of the links (ascending order, smallest will be displayed first). + * Used for ordering in the dropdown. + * + * @remark links without order defined will be displayed last + */ + order?: number; } diff --git a/src/plugins/home/server/services/sample_data/routes/list.ts b/src/plugins/home/server/services/sample_data/routes/list.ts index 39690b3944d0c2..a83ee7a57c4326 100644 --- a/src/plugins/home/server/services/sample_data/routes/list.ts +++ b/src/plugins/home/server/services/sample_data/routes/list.ts @@ -35,12 +35,12 @@ export const createListRoute = ( ?.foundObjectId ?? id; const appLinks = (appLinksMap.get(sampleDataset.id) ?? []).map((data) => { - const { sampleObject, getPath, label, icon } = data; + const { sampleObject, getPath, label, icon, order } = data; if (sampleObject === null) { - return { path: getPath(''), label, icon }; + return { path: getPath(''), label, icon, order }; } const objectId = findObjectId(sampleObject.type, sampleObject.id); - return { path: getPath(objectId), label, icon }; + return { path: getPath(objectId), label, icon, order }; }); const sampleDataStatus = await getSampleDatasetStatus( context, diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index 1e3e6a9634f4c7..4acd8a6e10e95c 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -78,6 +78,11 @@ export class HomePageObject extends FtrService { }); } + async launchSampleDiscover(id: string) { + await this.launchSampleDataSet(id); + await this.find.clickByLinkText('Discover'); + } + async launchSampleDashboard(id: string) { await this.launchSampleDataSet(id); await this.find.clickByLinkText('Dashboard'); diff --git a/x-pack/test/upgrade/apps/discover/discover_smoke_tests.ts b/x-pack/test/upgrade/apps/discover/discover_smoke_tests.ts index 150458919d41dc..1d2df7a703161f 100644 --- a/x-pack/test/upgrade/apps/discover/discover_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/discover/discover_smoke_tests.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getPageObjects, getService }: FtrProviderContext) { +export default function ({ getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'home', 'discover', 'timePicker']); describe('upgrade discover smoke tests', function describeIndexTests() { @@ -18,9 +18,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ]; const discoverTests = [ - { name: 'kibana_sample_data_flights', timefield: true, hits: '' }, - { name: 'kibana_sample_data_logs', timefield: true, hits: '' }, - { name: 'kibana_sample_data_ecommerce', timefield: true, hits: '' }, + { name: 'flights', timefield: true, hits: '' }, + { name: 'logs', timefield: true, hits: '' }, + { name: 'ecommerce', timefield: true, hits: '' }, ]; spaces.forEach(({ space, basePath }) => { @@ -31,7 +31,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { basePath, }); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.discover.selectIndexPattern(name); + await PageObjects.discover.selectIndexPattern(`kibana_sample_data_${name}`); await PageObjects.discover.waitUntilSearchingHasFinished(); if (timefield) { await PageObjects.timePicker.setCommonlyUsedTime('Last_24 hours'); @@ -52,6 +52,35 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); }); + + discoverTests.forEach(({ name, timefield, hits }) => { + describe('space: ' + space + ', name: ' + name, () => { + before(async () => { + await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { + basePath, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.launchSampleDiscover(name); + await PageObjects.header.waitUntilLoadingHasFinished(); + if (timefield) { + await PageObjects.timePicker.setCommonlyUsedTime('Last_24 hours'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + } + }); + it('shows hit count greater than zero', async () => { + const hitCount = await PageObjects.discover.getHitCount(); + if (hits === '') { + expect(hitCount).to.be.greaterThan(0); + } else { + expect(hitCount).to.be.equal(hits); + } + }); + it('shows table rows not empty', async () => { + const tableRows = await PageObjects.discover.getDocTableRows(); + expect(tableRows.length).to.be.greaterThan(0); + }); + }); + }); }); }); } From 234a6365d0eb226c5cc7fb2e98b7579c3ab9c45b Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 5 May 2022 11:39:25 +0100 Subject: [PATCH 22/83] skip flaky suite (#131602) --- .../security_solution_endpoint/apps/endpoint/policy_list.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index 7020babc4520b1..840c36a558ba06 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -78,7 +78,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await backButton.click(); await pageObjects.policy.ensureIsOnListPage(); }); - describe('when the endpoint count link is clicked', () => { + // FLAKY: https://github.com/elastic/kibana/issues/131602 + describe.skip('when the endpoint count link is clicked', () => { it('navigates to the endpoint list page filtered by policy', async () => { const endpointCount = (await testSubjects.findAll('policyEndpointCountLink'))[0]; await endpointCount.click(); From 0f3d63b1aa5b361948ab59a5f9e3debb9ea83eac Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Thu, 5 May 2022 14:24:11 +0200 Subject: [PATCH 23/83] [Fleet] Optimize package installation performance, phase 1 (#130906) --- .../server/services/epm/archive/storage.ts | 2 +- .../elasticsearch/datastream_ilm/install.ts | 64 ++++----- .../elasticsearch/datastream_ilm/remove.ts | 27 +--- .../services/epm/elasticsearch/ilm/install.ts | 44 +++++-- .../elasticsearch/ingest_pipeline/install.ts | 31 ++--- .../elasticsearch/ingest_pipeline/remove.ts | 44 ++----- .../epm/elasticsearch/ml_model/install.ts | 20 ++- .../elasticsearch/template/install.test.ts | 8 +- .../epm/elasticsearch/template/install.ts | 107 ++++++--------- .../epm/elasticsearch/transform/install.ts | 35 ++--- .../elasticsearch/transform/transform.test.ts | 41 +++++- .../fleet/server/services/epm/fields/field.ts | 2 +- .../services/epm/kibana/assets/install.ts | 39 +++++- .../services/epm/package_service.test.ts | 31 ++++- .../server/services/epm/package_service.ts | 5 +- .../epm/packages/_install_package.test.ts | 9 +- .../services/epm/packages/_install_package.ts | 97 +++++++------- .../server/services/epm/packages/assets.ts | 4 +- .../server/services/epm/packages/install.ts | 123 ++++++++++++------ .../server/services/epm/packages/remove.ts | 2 + .../server/services/epm/registry/index.ts | 3 +- .../apis/epm/install_remove_assets.ts | 4 + .../apis/epm/update_assets.ts | 8 +- 23 files changed, 416 insertions(+), 334 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts index cb9f5650550e87..2c313f2f2761d7 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts @@ -123,7 +123,7 @@ export async function saveArchiveEntries(opts: { }) ); - const results = await savedObjectsClient.bulkCreate(bulkBody); + const results = await savedObjectsClient.bulkCreate(bulkBody, { refresh: false }); return results; } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts index 4f18966a613074..c6be2dfedb1df0 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts @@ -13,14 +13,13 @@ import type { InstallablePackage, RegistryDataStream, } from '../../../../../common/types/models'; -import { getInstallation } from '../../packages'; -import { saveInstalledEsRefs } from '../../packages/install'; +import { updateEsAssetReferences } from '../../packages/install'; import { getAsset } from '../transform/common'; import { getESAssetMetadata } from '../meta'; import { retryTransientEsErrors } from '../retry'; -import { deleteIlmRefs, deleteIlms } from './remove'; +import { deleteIlms } from './remove'; interface IlmInstallation { installationName: string; @@ -37,24 +36,39 @@ export const installIlmForDataStream = async ( paths: string[], esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract, - logger: Logger + logger: Logger, + esReferences: EsAssetReference[] ) => { - const installation = await getInstallation({ savedObjectsClient, pkgName: registryPackage.name }); - let previousInstalledIlmEsAssets: EsAssetReference[] = []; - if (installation) { - previousInstalledIlmEsAssets = installation.installed_es.filter( - ({ type, id }) => type === ElasticsearchAssetType.dataStreamIlmPolicy - ); - } + const previousInstalledIlmEsAssets = esReferences.filter( + ({ type }) => type === ElasticsearchAssetType.dataStreamIlmPolicy + ); // delete all previous ilm await deleteIlms( esClient, previousInstalledIlmEsAssets.map((asset) => asset.id) ); + + if (previousInstalledIlmEsAssets.length > 0) { + // remove the saved object reference + esReferences = await updateEsAssetReferences( + savedObjectsClient, + registryPackage.name, + esReferences, + { + assetsToRemove: previousInstalledIlmEsAssets, + } + ); + } + // install the latest dataset const dataStreams = registryPackage.data_streams; - if (!dataStreams?.length) return []; + if (!dataStreams?.length) + return { + installedIlms: [], + esReferences, + }; + const dataStreamIlmPaths = paths.filter((path) => isDataStreamIlm(path)); let installedIlms: EsAssetReference[] = []; if (dataStreamIlmPaths.length > 0) { @@ -77,12 +91,17 @@ export const installIlmForDataStream = async ( return acc; }, []); - await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, ilmRefs); + esReferences = await updateEsAssetReferences( + savedObjectsClient, + registryPackage.name, + esReferences, + { assetsToAdd: ilmRefs } + ); const ilmInstallations: IlmInstallation[] = ilmPathDatasets.map( (ilmPathDataset: IlmPathDataset) => { const content = JSON.parse(getAsset(ilmPathDataset.path).toString('utf-8')); - content.policy._meta = getESAssetMetadata({ packageName: installation?.name }); + content.policy._meta = getESAssetMetadata({ packageName: registryPackage.name }); return { installationName: getIlmNameForInstallation(ilmPathDataset), @@ -98,22 +117,7 @@ export const installIlmForDataStream = async ( installedIlms = await Promise.all(installationPromises).then((results) => results.flat()); } - if (previousInstalledIlmEsAssets.length > 0) { - const currentInstallation = await getInstallation({ - savedObjectsClient, - pkgName: registryPackage.name, - }); - - // remove the saved object reference - await deleteIlmRefs( - savedObjectsClient, - currentInstallation?.installed_es || [], - registryPackage.name, - previousInstalledIlmEsAssets.map((asset) => asset.id), - installedIlms.map((installed) => installed.id) - ); - } - return installedIlms; + return { installedIlms, esReferences }; }; async function handleIlmInstall({ diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/remove.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/remove.ts index 1d98a9339c9076..331088d195d0be 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/remove.ts @@ -5,11 +5,7 @@ * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; - -import { ElasticsearchAssetType } from '../../../../types'; -import type { EsAssetReference } from '../../../../types'; -import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common/constants'; +import type { ElasticsearchClient } from '@kbn/core/server'; export const deleteIlms = async (esClient: ElasticsearchClient, ilmPolicyIds: string[]) => { await Promise.all( @@ -26,24 +22,3 @@ export const deleteIlms = async (esClient: ElasticsearchClient, ilmPolicyIds: st }) ); }; - -export const deleteIlmRefs = async ( - savedObjectsClient: SavedObjectsClientContract, - installedEsAssets: EsAssetReference[], - pkgName: string, - installedEsIdToRemove: string[], - currentInstalledEsIlmIds: string[] -) => { - const seen = new Set(); - const filteredAssets = installedEsAssets.filter(({ type, id }) => { - if (type !== ElasticsearchAssetType.dataStreamIlmPolicy) return true; - const add = - (currentInstalledEsIlmIds.includes(id) || !installedEsIdToRemove.includes(id)) && - !seen.has(id); - seen.add(id); - return add; - }); - return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_es: filteredAssets, - }); -}; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts index 9b64ec89507dc5..3aa86b526addd1 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts @@ -5,12 +5,13 @@ * 2.0. */ -import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; -import type { InstallablePackage } from '../../../../types'; +import type { EsAssetReference, InstallablePackage } from '../../../../types'; import { ElasticsearchAssetType } from '../../../../types'; import { getAsset, getPathParts } from '../../archive'; +import { updateEsAssetReferences } from '../../packages/install'; import { getESAssetMetadata } from '../meta'; import { retryTransientEsErrors } from '../retry'; @@ -18,25 +19,40 @@ export async function installILMPolicy( packageInfo: InstallablePackage, paths: string[], esClient: ElasticsearchClient, - logger: Logger -) { + savedObjectsClient: SavedObjectsClientContract, + logger: Logger, + esReferences: EsAssetReference[] +): Promise { const ilmPaths = paths.filter((path) => isILMPolicy(path)); - if (!ilmPaths.length) return; - await Promise.all( - ilmPaths.map(async (path) => { - const body = JSON.parse(getAsset(path).toString('utf-8')); + if (!ilmPaths.length) return esReferences; + + const ilmPolicies = ilmPaths.map((path) => { + const body = JSON.parse(getAsset(path).toString('utf-8')); + + body.policy._meta = getESAssetMetadata({ packageName: packageInfo.name }); + + const { file } = getPathParts(path); + const name = file.substr(0, file.lastIndexOf('.')); - body.policy._meta = getESAssetMetadata({ packageName: packageInfo.name }); + return { name, body }; + }); - const { file } = getPathParts(path); - const name = file.substr(0, file.lastIndexOf('.')); + esReferences = await updateEsAssetReferences(savedObjectsClient, packageInfo.name, esReferences, { + assetsToAdd: ilmPolicies.map((policy) => ({ + type: ElasticsearchAssetType.ilmPolicy, + id: policy.name, + })), + }); + + await Promise.all( + ilmPolicies.map(async (policy) => { try { await retryTransientEsErrors( () => esClient.transport.request({ method: 'PUT', - path: '/_ilm/policy/' + name, - body, + path: '/_ilm/policy/' + policy.name, + body: policy.body, }), { logger } ); @@ -45,6 +61,8 @@ export async function installILMPolicy( } }) ); + + return esReferences; } const isILMPolicy = (path: string) => { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index c6830d5bb9a03c..49dae4d86b6395 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -12,8 +12,7 @@ import { ElasticsearchAssetType } from '../../../../types'; import type { EsAssetReference, RegistryDataStream, InstallablePackage } from '../../../../types'; import { getAsset, getPathParts } from '../../archive'; import type { ArchiveEntry } from '../../archive'; -import { saveInstalledEsRefs } from '../../packages/install'; -import { getInstallationObject } from '../../packages'; +import { updateEsAssetReferences } from '../../packages/install'; import { FLEET_FINAL_PIPELINE_CONTENT, FLEET_FINAL_PIPELINE_ID, @@ -24,8 +23,6 @@ import { appendMetadataToIngestPipeline } from '../meta'; import { retryTransientEsErrors } from '../retry'; -import { deletePipelineRefs } from './remove'; - interface RewriteSubstitution { source: string; target: string; @@ -44,7 +41,8 @@ export const installPipelines = async ( paths: string[], esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract, - logger: Logger + logger: Logger, + esReferences: EsAssetReference[] ) => { // unlike other ES assets, pipeline names are versioned so after a template is updated // it can be created pointing to the new template, without removing the old one and effecting data @@ -67,7 +65,7 @@ export const installPipelines = async ( const nameForInstallation = getPipelineNameForInstallation({ pipelineName: name, dataStream, - packageVersion: installablePackage.version, + packageVersion: pkgVersion, }); return { id: nameForInstallation, type: ElasticsearchAssetType.ingestPipeline }; }); @@ -80,27 +78,17 @@ export const installPipelines = async ( const { name } = getNameAndExtension(path); const nameForInstallation = getPipelineNameForInstallation({ pipelineName: name, - packageVersion: installablePackage.version, + packageVersion: pkgVersion, }); return { id: nameForInstallation, type: ElasticsearchAssetType.ingestPipeline }; }); pipelineRefs = [...pipelineRefs, ...topLevelPipelineRefs]; - // check that we don't duplicate the pipeline refs if the user is reinstalling - const installedPkg = await getInstallationObject({ - savedObjectsClient, - pkgName, + esReferences = await updateEsAssetReferences(savedObjectsClient, pkgName, esReferences, { + assetsToAdd: pipelineRefs, }); - if (!installedPkg) throw new Error("integration wasn't found while installing pipelines"); - // remove the current pipeline refs, if any exist, associated with this version before saving new ones so no duplicates occur - await deletePipelineRefs( - savedObjectsClient, - installedPkg.attributes.installed_es, - pkgName, - pkgVersion - ); - await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, pipelineRefs); + const pipelines = dataStreams ? dataStreams.reduce>>((acc, dataStream) => { if (dataStream.ingest_pipeline) { @@ -130,7 +118,8 @@ export const installPipelines = async ( ); } - return await Promise.all(pipelines).then((results) => results.flat()); + await Promise.all(pipelines); + return esReferences; }; export function rewriteIngestPipeline( diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts index e9d693bdbfaa8e..7e2b6c121bbabb 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts @@ -10,54 +10,38 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/ import { appContextService } from '../../..'; import { ElasticsearchAssetType } from '../../../../types'; import { IngestManagerError } from '../../../../errors'; -import { getInstallation } from '../../packages/get'; -import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import type { EsAssetReference } from '../../../../../common'; +import { updateEsAssetReferences } from '../../packages/install'; export const deletePreviousPipelines = async ( esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract, pkgName: string, - previousPkgVersion: string + previousPkgVersion: string, + esReferences: EsAssetReference[] ) => { const logger = appContextService.getLogger(); - const installation = await getInstallation({ savedObjectsClient, pkgName }); - if (!installation) return; - const installedEsAssets = installation.installed_es; - const installedPipelines = installedEsAssets.filter( + const installedPipelines = esReferences.filter( ({ type, id }) => type === ElasticsearchAssetType.ingestPipeline && id.includes(previousPkgVersion) ); - const deletePipelinePromises = installedPipelines.map(({ type, id }) => { - return deletePipeline(esClient, id); - }); - try { - await Promise.all(deletePipelinePromises); - } catch (e) { - logger.error(e); - } try { - await deletePipelineRefs(savedObjectsClient, installedEsAssets, pkgName, previousPkgVersion); + await Promise.all( + installedPipelines.map(({ type, id }) => { + return deletePipeline(esClient, id); + }) + ); } catch (e) { logger.error(e); } -}; -export const deletePipelineRefs = async ( - savedObjectsClient: SavedObjectsClientContract, - installedEsAssets: EsAssetReference[], - pkgName: string, - pkgVersion: string -) => { - const filteredAssets = installedEsAssets.filter(({ type, id }) => { - if (type !== ElasticsearchAssetType.ingestPipeline) return true; - if (!id.includes(pkgVersion)) return true; - return false; - }); - return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_es: filteredAssets, + return await updateEsAssetReferences(savedObjectsClient, pkgName, esReferences, { + assetsToRemove: esReferences.filter(({ type, id }) => { + return type === ElasticsearchAssetType.ingestPipeline && id.includes(previousPkgVersion); + }), }); }; + export async function deletePipeline(esClient: ElasticsearchClient, id: string): Promise { // '*' shouldn't ever appear here, but it still would delete all ingest pipelines if (id && id !== '*') { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts index 13b3de989e6200..630433e18ce391 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts @@ -8,13 +8,14 @@ import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { errors } from '@elastic/elasticsearch'; -import { saveInstalledEsRefs } from '../../packages/install'; import { getPathParts } from '../../archive'; import { ElasticsearchAssetType } from '../../../../../common/types/models'; import type { EsAssetReference, InstallablePackage } from '../../../../../common/types/models'; import { retryTransientEsErrors } from '../retry'; +import { updateEsAssetReferences } from '../../packages/install'; + import { getAsset } from './common'; interface MlModelInstallation { @@ -27,11 +28,11 @@ export const installMlModel = async ( paths: string[], esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract, - logger: Logger + logger: Logger, + esReferences: EsAssetReference[] ) => { const mlModelPath = paths.find((path) => isMlModel(path)); - const installedMlModels: EsAssetReference[] = []; if (mlModelPath !== undefined) { const content = getAsset(mlModelPath).toString('utf-8'); const pathParts = mlModelPath.split('/'); @@ -43,17 +44,22 @@ export const installMlModel = async ( }; // get and save ml model refs before installing ml model - await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, [mlModelRef]); + esReferences = await updateEsAssetReferences( + savedObjectsClient, + installablePackage.name, + esReferences, + { assetsToAdd: [mlModelRef] } + ); const mlModel: MlModelInstallation = { installationName: modelId, content, }; - const result = await handleMlModelInstall({ esClient, logger, mlModel }); - installedMlModels.push(result); + await handleMlModelInstall({ esClient, logger, mlModel }); } - return installedMlModels; + + return esReferences; }; const isMlModel = (path: string) => { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts index 48f070434530a8..998d0f9fb1ae57 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts @@ -180,15 +180,11 @@ describe('EPM install', () => { packageName: pkg.name, }); - const removeAliases = ( - esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest - ).body; - expect(removeAliases?.template?.aliases).not.toBeDefined(); - const sentTemplate = ( - esClient.indices.putIndexTemplate.mock.calls[1][0] as estypes.IndicesPutIndexTemplateRequest + esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest ).body; expect(sentTemplate).toBeDefined(); + expect(sentTemplate?.template?.aliases).not.toBeDefined(); expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixUnset); expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); }); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 6d953835dfe6cd..2d2e5b2ffea2a3 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -16,17 +16,17 @@ import type { RegistryElasticsearch, InstallablePackage, IndexTemplate, - PackageInfo, IndexTemplateMappings, TemplateMapEntry, TemplateMap, + EsAssetReference, } from '../../../../types'; import { loadFieldsFromYaml, processFields } from '../../fields/field'; import type { Field } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; import { getAsset, getPathParts } from '../../archive'; -import { removeAssetTypesFromInstalledEs, saveInstalledEsRefs } from '../../packages/install'; +import { updateEsAssetReferences } from '../../packages/install'; import { FLEET_COMPONENT_TEMPLATES, PACKAGE_TEMPLATE_SUFFIX, @@ -36,8 +36,6 @@ import { import { getESAssetMetadata } from '../meta'; import { retryTransientEsErrors } from '../retry'; -import { getPackageInfo } from '../../packages'; - import { generateMappings, generateTemplateName, @@ -54,8 +52,12 @@ export const installTemplates = async ( esClient: ElasticsearchClient, logger: Logger, paths: string[], - savedObjectsClient: SavedObjectsClientContract -): Promise => { + savedObjectsClient: SavedObjectsClientContract, + esReferences: EsAssetReference[] +): Promise<{ + installedTemplates: IndexTemplateEntry[]; + installedEsReferences: EsAssetReference[]; +}> => { // install any pre-built index template assets, // atm, this is only the base package's global index templates // Install component templates first, as they are used by the index templates @@ -63,24 +65,27 @@ export const installTemplates = async ( await installPreBuiltTemplates(paths, esClient, logger); // remove package installation's references to index templates - await removeAssetTypesFromInstalledEs(savedObjectsClient, installablePackage.name, [ - ElasticsearchAssetType.indexTemplate, - ElasticsearchAssetType.componentTemplate, - ]); + esReferences = await updateEsAssetReferences( + savedObjectsClient, + installablePackage.name, + esReferences, + { + assetsToRemove: esReferences.filter( + ({ type }) => + type === ElasticsearchAssetType.indexTemplate || + type === ElasticsearchAssetType.componentTemplate + ), + } + ); + // build templates per data stream from yml files const dataStreams = installablePackage.data_streams; - if (!dataStreams) return []; - - const packageInfo = await getPackageInfo({ - savedObjectsClient, - pkgName: installablePackage.name, - pkgVersion: installablePackage.version, - }); + if (!dataStreams) return { installedTemplates: [], installedEsReferences: esReferences }; const installedTemplatesNested = await Promise.all( dataStreams.map((dataStream) => installTemplateForDataStream({ - pkg: packageInfo, + pkg: installablePackage, esClient, logger, dataStream, @@ -93,13 +98,14 @@ export const installTemplates = async ( const installedIndexTemplateRefs = getAllTemplateRefs(installedTemplates); // add package installation's references to index templates - await saveInstalledEsRefs( + esReferences = await updateEsAssetReferences( savedObjectsClient, installablePackage.name, - installedIndexTemplateRefs + esReferences, + { assetsToAdd: installedIndexTemplateRefs } ); - return installedTemplates; + return { installedTemplates, installedEsReferences: esReferences }; }; const installPreBuiltTemplates = async ( @@ -192,7 +198,7 @@ export async function installTemplateForDataStream({ logger, dataStream, }: { - pkg: PackageInfo; + pkg: InstallablePackage; esClient: ElasticsearchClient; logger: Logger; dataStream: RegistryDataStream; @@ -315,19 +321,20 @@ async function installDataStreamComponentTemplates(params: { await Promise.all( templateEntries.map(async ([name, body]) => { if (isUserSettingsTemplate(name)) { - // look for existing user_settings template - const result = await retryTransientEsErrors( - () => esClient.cluster.getComponentTemplate({ name }, { ignore: [404] }), - { logger } - ); - const hasUserSettingsTemplate = result.component_templates?.length === 1; - if (!hasUserSettingsTemplate) { - // only add if one isn't already present + try { + // Attempt to create custom component templates, ignore if they already exist const { clusterPromise } = putComponentTemplate(esClient, logger, { body, name, + create: true, }); - return clusterPromise; + return await clusterPromise; + } catch (e) { + if (e?.statusCode === 400 && e.body?.error?.reason.includes('already exists')) { + // ignore + } else { + throw e; + } } } else { const { clusterPromise } = putComponentTemplate(esClient, logger, { body, name }); @@ -410,44 +417,6 @@ export async function installTemplate({ }); } - // Datastream now throw an error if the aliases field is present so ensure that we remove that field. - const getTemplateRes = await retryTransientEsErrors( - () => - esClient.indices.getIndexTemplate( - { - name: templateName, - }, - { - ignore: [404], - } - ), - { logger } - ); - - const existingIndexTemplate = getTemplateRes?.index_templates?.[0]; - if ( - existingIndexTemplate && - existingIndexTemplate.name === templateName && - existingIndexTemplate?.index_template?.template?.aliases - ) { - const updateIndexTemplateParams = { - name: templateName, - body: { - ...existingIndexTemplate.index_template, - template: { - ...existingIndexTemplate.index_template.template, - // Remove the aliases field - aliases: undefined, - }, - }, - }; - - await retryTransientEsErrors( - () => esClient.indices.putIndexTemplate(updateIndexTemplateParams, { ignore: [404] }), - { logger } - ); - } - const defaultSettings = buildDefaultSettings({ templateName, packageName, diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts index fea12f4b139c66..ab8f60e172dcbd 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts @@ -8,7 +8,7 @@ import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { errors } from '@elastic/elasticsearch'; -import { saveInstalledEsRefs } from '../../packages/install'; +import { updateEsAssetReferences } from '../../packages/install'; import { getPathParts } from '../../archive'; import { ElasticsearchAssetType } from '../../../../../common/types/models'; import type { EsAssetReference, InstallablePackage } from '../../../../../common/types/models'; @@ -18,7 +18,7 @@ import { getESAssetMetadata } from '../meta'; import { retryTransientEsErrors } from '../retry'; -import { deleteTransforms, deleteTransformRefs } from './remove'; +import { deleteTransforms } from './remove'; import { getAsset } from './common'; interface TransformInstallation { @@ -31,12 +31,14 @@ export const installTransform = async ( paths: string[], esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract, - logger: Logger + logger: Logger, + esReferences?: EsAssetReference[] ) => { const installation = await getInstallation({ savedObjectsClient, pkgName: installablePackage.name, }); + esReferences = esReferences ?? installation?.installed_es ?? []; let previousInstalledTransformEsAssets: EsAssetReference[] = []; if (installation) { previousInstalledTransformEsAssets = installation.installed_es.filter( @@ -71,7 +73,14 @@ export const installTransform = async ( }, []); // get and save transform refs before installing transforms - await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, transformRefs); + esReferences = await updateEsAssetReferences( + savedObjectsClient, + installablePackage.name, + esReferences, + { + assetsToAdd: transformRefs, + } + ); const transforms: TransformInstallation[] = transformPaths.map((path: string) => { const content = JSON.parse(getAsset(path).toString('utf-8')); @@ -95,21 +104,17 @@ export const installTransform = async ( } if (previousInstalledTransformEsAssets.length > 0) { - const currentInstallation = await getInstallation({ + esReferences = await updateEsAssetReferences( savedObjectsClient, - pkgName: installablePackage.name, - }); - - // remove the saved object reference - await deleteTransformRefs( - savedObjectsClient, - currentInstallation?.installed_es || [], installablePackage.name, - previousInstalledTransformEsAssets.map((asset) => asset.id), - installedTransforms.map((installed) => installed.id) + esReferences, + { + assetsToRemove: previousInstalledTransformEsAssets, + } ); } - return installedTransforms; + + return { installedTransforms, esReferences }; }; export const isTransform = (path: string) => { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts index 16384b8bfba196..74e49031861c16 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts @@ -34,8 +34,10 @@ import { appContextService } from '../../../app_context'; import { getESAssetMetadata } from '../meta'; -import { installTransform } from './install'; +import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../constants'; + import { getAsset } from './common'; +import { installTransform } from './install'; describe('test transform install', () => { let esClient: ReturnType; @@ -46,6 +48,12 @@ describe('test transform install', () => { (getInstallation as jest.MockedFunction).mockReset(); (getInstallationObject as jest.MockedFunction).mockReset(); savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.update.mockImplementation(async (type, id, attributes) => ({ + type: PACKAGES_SAVED_OBJECT_TYPE, + id: 'endpoint', + attributes, + references: [], + })); }); afterEach(() => { @@ -158,7 +166,8 @@ describe('test transform install', () => { ], esClient, savedObjectsClient, - loggerMock.create() + loggerMock.create(), + previousInstallation.installed_es ); expect(esClient.transform.getTransform.mock.calls).toEqual([ @@ -255,6 +264,9 @@ describe('test transform install', () => { }, ], }, + { + refresh: false, + }, ], [ 'epm-packages', @@ -266,15 +278,18 @@ describe('test transform install', () => { type: 'ingest_pipeline', }, { - id: 'endpoint.metadata_current-default-0.16.0-dev.0', + id: 'endpoint.metadata-default-0.16.0-dev.0', type: 'transform', }, { - id: 'endpoint.metadata-default-0.16.0-dev.0', + id: 'endpoint.metadata_current-default-0.16.0-dev.0', type: 'transform', }, ], }, + { + refresh: false, + }, ], ]); }); @@ -331,7 +346,8 @@ describe('test transform install', () => { ['endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json'], esClient, savedObjectsClient, - loggerMock.create() + loggerMock.create(), + previousInstallation.installed_es ); const meta = getESAssetMetadata({ packageName: 'endpoint' }); @@ -363,6 +379,9 @@ describe('test transform install', () => { { id: 'endpoint.metadata_current-default-0.16.0-dev.0', type: 'transform' }, ], }, + { + refresh: false, + }, ], ]); }); @@ -443,7 +462,8 @@ describe('test transform install', () => { [], esClient, savedObjectsClient, - loggerMock.create() + loggerMock.create(), + previousInstallation.installed_es ); expect(esClient.transform.getTransform.mock.calls).toEqual([ @@ -492,6 +512,9 @@ describe('test transform install', () => { { installed_es: [], }, + { + refresh: false, + }, ], ]); }); @@ -559,7 +582,8 @@ describe('test transform install', () => { ['endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json'], esClient, savedObjectsClient, - loggerMock.create() + loggerMock.create(), + previousInstallation.installed_es ); const meta = getESAssetMetadata({ packageName: 'endpoint' }); @@ -586,6 +610,9 @@ describe('test transform install', () => { { id: 'endpoint.metadata_current-default-0.16.0-dev.0', type: 'transform' }, ], }, + { + refresh: false, + }, ], ]); }); diff --git a/x-pack/plugins/fleet/server/services/epm/fields/field.ts b/x-pack/plugins/fleet/server/services/epm/fields/field.ts index f1ad96504594e9..3f1a8d8b2b7baa 100644 --- a/x-pack/plugins/fleet/server/services/epm/fields/field.ts +++ b/x-pack/plugins/fleet/server/services/epm/fields/field.ts @@ -262,7 +262,7 @@ const isFields = (path: string) => { */ export const loadFieldsFromYaml = async ( - pkg: PackageInfo, + pkg: Pick, datasetName?: string ): Promise => { // Fetch all field definition files diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index ce8d7e7be2bb91..1462cd61c4bd38 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -21,9 +21,11 @@ import { partition } from 'lodash'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import { getAsset, getPathParts } from '../../archive'; import { KibanaAssetType, KibanaSavedObjectType } from '../../../../types'; -import type { AssetType, AssetReference, AssetParts } from '../../../../types'; +import type { AssetType, AssetReference, AssetParts, Installation } from '../../../../types'; import { savedObjectTypes } from '../../packages'; import { indexPatternTypes, getIndexPatternSavedObjects } from '../index_pattern/install'; +import { saveKibanaAssetsRefs } from '../../packages/install'; +import { deleteKibanaSavedObjectsAssets } from '../../packages/remove'; type SavedObjectsImporterContract = Pick; const formatImportErrorsForLog = (errors: SavedObjectsImportFailure[]) => @@ -121,6 +123,41 @@ export async function installKibanaAssets(options: { return installedAssets; } + +export async function installKibanaAssetsAndReferences({ + savedObjectsClient, + savedObjectsImporter, + logger, + pkgName, + paths, + installedPkg, +}: { + savedObjectsClient: SavedObjectsClientContract; + savedObjectsImporter: Pick; + logger: Logger; + pkgName: string; + paths: string[]; + installedPkg?: SavedObject; +}) { + const kibanaAssets = await getKibanaAssets(paths); + if (installedPkg) await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg }); + // save new kibana refs before installing the assets + const installedKibanaAssetsRefs = await saveKibanaAssetsRefs( + savedObjectsClient, + pkgName, + kibanaAssets + ); + + await installKibanaAssets({ + logger, + savedObjectsImporter, + pkgName, + kibanaAssets, + }); + + return installedKibanaAssetsRefs; +} + export const deleteKibanaInstalledRefs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts index 31bf9e47a4ae00..782af2860d2e3f 100644 --- a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts @@ -50,6 +50,7 @@ function getTest( spy: jest.SpyInstance; spyArgs: any[]; spyResponse: any; + expectedReturnValue: any; }; switch (testKey) { @@ -65,6 +66,7 @@ function getTest( }, ], spyResponse: { name: 'getInstallation test' }, + expectedReturnValue: { name: 'getInstallation test' }, }; break; case testKeys[1]: @@ -82,6 +84,7 @@ function getTest( }, ], spyResponse: { name: 'ensureInstalledPackage test' }, + expectedReturnValue: { name: 'ensureInstalledPackage test' }, }; break; case testKeys[2]: @@ -91,6 +94,7 @@ function getTest( spy: jest.spyOn(epmRegistry, 'fetchFindLatestPackageOrThrow'), spyArgs: ['package name'], spyResponse: { name: 'fetchFindLatestPackage test' }, + expectedReturnValue: { name: 'fetchFindLatestPackage test' }, }; break; case testKeys[3]: @@ -103,6 +107,10 @@ function getTest( packageInfo: { name: 'getRegistryPackage test' }, paths: ['/some/test/path'], }, + expectedReturnValue: { + packageInfo: { name: 'getRegistryPackage test' }, + paths: ['/some/test/path'], + }, }; break; case testKeys[4]: @@ -122,7 +130,14 @@ function getTest( args: [pkg, paths], spy: jest.spyOn(epmTransformsInstall, 'installTransform'), spyArgs: [pkg, paths, mocks.esClient, mocks.soClient, mocks.logger], - spyResponse: [ + spyResponse: { + installedTransforms: [ + { + name: 'package name', + }, + ], + }, + expectedReturnValue: [ { name: 'package name', }, @@ -176,10 +191,13 @@ describe('PackageService', () => { soClient: mockSoClient, logger: mockLogger, }; - const { method, args, spy, spyArgs, spyResponse } = getTest(mockClients, testKey); + const { method, args, spy, spyArgs, spyResponse, expectedReturnValue } = getTest( + mockClients, + testKey + ); spy.mockResolvedValue(spyResponse); - await expect(method(...args)).resolves.toEqual(spyResponse); + await expect(method(...args)).resolves.toEqual(expectedReturnValue); expect(spy).toHaveBeenCalledWith(...spyArgs); }); }); @@ -193,10 +211,13 @@ describe('PackageService', () => { soClient: mockSoClient, logger: mockLogger, }; - const { method, args, spy, spyArgs, spyResponse } = getTest(mockClients, testKey); + const { method, args, spy, spyArgs, spyResponse, expectedReturnValue } = getTest( + mockClients, + testKey + ); spy.mockResolvedValue(spyResponse); - await expect(method(...args)).resolves.toEqual(spyResponse); + await expect(method(...args)).resolves.toEqual(expectedReturnValue); expect(spy).toHaveBeenCalledWith(...spyArgs); }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.ts b/x-pack/plugins/fleet/server/services/epm/package_service.ts index 573ca3508e9475..e16d4954f0b9d5 100644 --- a/x-pack/plugins/fleet/server/services/epm/package_service.ts +++ b/x-pack/plugins/fleet/server/services/epm/package_service.ts @@ -146,14 +146,15 @@ class PackageClientImpl implements PackageClient { return installedAssets; } - #reinstallTransforms(packageInfo: InstallablePackage, paths: string[]) { - return installTransform( + async #reinstallTransforms(packageInfo: InstallablePackage, paths: string[]) { + const { installedTransforms } = await installTransform( packageInfo, paths, this.internalEsClient, this.internalSoClient, this.logger ); + return installedTransforms; } #runPreflight() { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts index c0e44043459022..db9803ea70f3a3 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts @@ -21,16 +21,15 @@ jest.mock('./install'); jest.mock('./get'); import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; -import { installKibanaAssets } from '../kibana/assets/install'; +import { installKibanaAssetsAndReferences } from '../kibana/assets/install'; import { _installPackage } from './_install_package'; const mockedUpdateCurrentWriteIndices = updateCurrentWriteIndices as jest.MockedFunction< typeof updateCurrentWriteIndices >; -const mockedGetKibanaAssets = installKibanaAssets as jest.MockedFunction< - typeof installKibanaAssets ->; +const mockedInstallKibanaAssetsAndReferences = + installKibanaAssetsAndReferences as jest.MockedFunction; function sleep(millis: number) { return new Promise((resolve) => setTimeout(resolve, millis)); @@ -50,7 +49,7 @@ describe('_installPackage', () => { }); it('handles errors from installKibanaAssets', async () => { // force errors from this function - mockedGetKibanaAssets.mockImplementation(async () => { + mockedInstallKibanaAssetsAndReferences.mockImplementation(async () => { throw new Error('mocked async error A: should be caught'); }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 796269eee38b1d..24c324e6b7cd00 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -29,9 +29,8 @@ import { isTopLevelPipeline, deletePreviousPipelines, } from '../elasticsearch/ingest_pipeline'; -import { getAllTemplateRefs } from '../elasticsearch/template/install'; import { installILMPolicy } from '../elasticsearch/ilm/install'; -import { installKibanaAssets, getKibanaAssets } from '../kibana/assets/install'; +import { installKibanaAssetsAndReferences } from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; import { installTransform } from '../elasticsearch/transform/install'; import { installMlModel } from '../elasticsearch/ml_model'; @@ -40,8 +39,7 @@ import { saveArchiveEntries } from '../archive/storage'; import { ConcurrentInstallOperationError } from '../../../errors'; import { packagePolicyService } from '../..'; -import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './install'; -import { deleteKibanaSavedObjectsAssets } from './remove'; +import { createInstallation } from './install'; import { withPackageSpan } from './utils'; // this is only exported for testing @@ -106,47 +104,59 @@ export async function _installPackage({ }); } - const installedKibanaAssetsRefs = await withPackageSpan('Install Kibana assets', async () => { - const kibanaAssets = await getKibanaAssets(paths); - if (installedPkg) await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg }); - // save new kibana refs before installing the assets - const assetRefs = await saveKibanaAssetsRefs(savedObjectsClient, pkgName, kibanaAssets); - - await installKibanaAssets({ - logger, + const kibanaAssetPromise = withPackageSpan('Install Kibana assets', () => + installKibanaAssetsAndReferences({ + savedObjectsClient, savedObjectsImporter, pkgName, - kibanaAssets, - }); + paths, + installedPkg, + logger, + }) + ); + // Necessary to avoid async promise rejection warning + // See https://stackoverflow.com/questions/40920179/should-i-refrain-from-handling-promise-rejection-asynchronously + kibanaAssetPromise.catch(() => {}); - return assetRefs; - }); + // Use a shared array that is updated by each operation. This allows each operation to accurately update the + // installation object with it's references without requiring a refresh of the SO index on each update (faster). + let esReferences = installedPkg?.attributes.installed_es ?? []; // the rest of the installation must happen in sequential order // currently only the base package has an ILM policy // at some point ILM policies can be installed/modified // per data stream and we should then save them - await withPackageSpan('Install ILM policies', () => - installILMPolicy(packageInfo, paths, esClient, logger) + esReferences = await withPackageSpan('Install ILM policies', () => + installILMPolicy(packageInfo, paths, esClient, savedObjectsClient, logger, esReferences) ); - const installedDataStreamIlm = await withPackageSpan('Install Data Stream ILM policies', () => - installIlmForDataStream(packageInfo, paths, esClient, savedObjectsClient, logger) - ); + ({ esReferences } = await withPackageSpan('Install Data Stream ILM policies', () => + installIlmForDataStream( + packageInfo, + paths, + esClient, + savedObjectsClient, + logger, + esReferences + ) + )); // installs ml models - const installedMlModel = await withPackageSpan('Install ML models', () => - installMlModel(packageInfo, paths, esClient, savedObjectsClient, logger) + esReferences = await withPackageSpan('Install ML models', () => + installMlModel(packageInfo, paths, esClient, savedObjectsClient, logger, esReferences) ); // installs versionized pipelines without removing currently installed ones - const installedPipelines = await withPackageSpan('Install ingest pipelines', () => - installPipelines(packageInfo, paths, esClient, savedObjectsClient, logger) + esReferences = await withPackageSpan('Install ingest pipelines', () => + installPipelines(packageInfo, paths, esClient, savedObjectsClient, logger, esReferences) ); + // install or update the templates referencing the newly installed pipelines - const installedTemplates = await withPackageSpan('Install index templates', () => - installTemplates(packageInfo, esClient, logger, paths, savedObjectsClient) - ); + const { installedTemplates, installedEsReferences: esReferencesAfterTemplates } = + await withPackageSpan('Install index templates', () => + installTemplates(packageInfo, esClient, logger, paths, savedObjectsClient, esReferences) + ); + esReferences = esReferencesAfterTemplates; try { await removeLegacyTemplates({ packageInfo, esClient, logger }); @@ -159,9 +169,9 @@ export async function _installPackage({ updateCurrentWriteIndices(esClient, logger, installedTemplates) ); - const installedTransforms = await withPackageSpan('Install transforms', () => - installTransform(packageInfo, paths, esClient, savedObjectsClient, logger) - ); + ({ esReferences } = await withPackageSpan('Install transforms', () => + installTransform(packageInfo, paths, esClient, savedObjectsClient, logger, esReferences) + )); // If this is an update or retrying an update, delete the previous version's pipelines // Top-level pipeline assets will not be removed on upgrade as of ml model package addition which requires previous @@ -171,28 +181,30 @@ export async function _installPackage({ (installType === 'update' || installType === 'reupdate') && installedPkg ) { - await withPackageSpan('Delete previous ingest pipelines', () => + esReferences = await withPackageSpan('Delete previous ingest pipelines', () => deletePreviousPipelines( esClient, savedObjectsClient, pkgName, - installedPkg.attributes.version + installedPkg!.attributes.version, + esReferences ) ); } // pipelines from a different version may have installed during a failed update if (installType === 'rollback' && installedPkg) { - await await withPackageSpan('Delete previous ingest pipelines', () => + esReferences = await withPackageSpan('Delete previous ingest pipelines', () => deletePreviousPipelines( esClient, savedObjectsClient, pkgName, - installedPkg.attributes.install_version + installedPkg!.attributes.install_version, + esReferences ) ); } - const installedTemplateRefs = getAllTemplateRefs(installedTemplates); + const installedKibanaAssetsRefs = await kibanaAssetPromise; const packageAssetResults = await withPackageSpan('Update archive entries', () => saveArchiveEntries({ savedObjectsClient, @@ -208,11 +220,9 @@ export async function _installPackage({ }) ); - // update to newly installed version when all assets are successfully installed - if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion); - const updatedPackage = await withPackageSpan('Update install status', () => savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + version: pkgVersion, install_version: pkgVersion, install_status: 'installed', package_assets: packageAssetRefs, @@ -233,14 +243,7 @@ export async function _installPackage({ }); } - return [ - ...installedKibanaAssetsRefs, - ...installedPipelines, - ...installedDataStreamIlm, - ...installedTemplateRefs, - ...installedTransforms, - ...installedMlModel, - ]; + return [...installedKibanaAssetsRefs, ...esReferences]; } catch (err) { if (savedObjectsClient.errors.isConflictError(err)) { throw new ConcurrentInstallOperationError( diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts index c939ce093a65c9..0621d05d21497b 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts @@ -17,7 +17,7 @@ import type { ArchiveEntry } from '../archive'; // and different package and version structure export function getAssets( - packageInfo: PackageInfo, + packageInfo: Pick, filter = (path: string): boolean => true, datasetName?: string ): string[] { @@ -52,7 +52,7 @@ export function getAssets( // ASK: Does getAssetsData need an installSource now? // if so, should it be an Installation vs InstallablePackage or add another argument? export async function getAssetsData( - packageInfo: PackageInfo, + packageInfo: Pick, filter = (path: string): boolean => true, datasetName?: string ): Promise { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 9ae549982399c1..c7fc01c89eb062 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -17,6 +17,8 @@ import type { import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; +import pRetry from 'p-retry'; + import { generateESIndexPatterns } from '../elasticsearch/template/template'; import type { BulkInstallPackageInfo, @@ -29,13 +31,7 @@ import { IngestManagerError, PackageOutdatedError } from '../../../errors'; import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants'; import type { KibanaAssetType } from '../../../types'; import { licenseService } from '../..'; -import type { - Installation, - AssetType, - EsAssetReference, - InstallType, - InstallResult, -} from '../../../types'; +import type { Installation, EsAssetReference, InstallType, InstallResult } from '../../../types'; import { appContextService } from '../../app_context'; import * as Registry from '../registry'; import { @@ -271,10 +267,13 @@ async function installPackageFromRegistry({ installType, }); - // get latest package version - const latestPackage = await Registry.fetchFindLatestPackageOrThrow(pkgName, { - ignoreConstraints, - }); + // get latest package version and requested version in parallel for performance + const [latestPackage, { paths, packageInfo }] = await Promise.all([ + Registry.fetchFindLatestPackageOrThrow(pkgName, { + ignoreConstraints, + }), + Registry.getRegistryPackage(pkgName, pkgVersion), + ]); // let the user install if using the force flag or needing to reinstall or install a previous version due to failed update const installOutOfDateVersionOk = @@ -319,9 +318,6 @@ async function installPackageFromRegistry({ ); } - // get package info - const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); - if (!licenseService.hasAtLeast(packageInfo.license || 'basic')) { const err = new Error(`Requires ${packageInfo.license} license`); sendEvent({ @@ -632,22 +628,60 @@ export const saveKibanaAssetsRefs = async ( kibanaAssets: Record ) => { const assetRefs = Object.values(kibanaAssets).flat().map(toAssetReference); - await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_kibana: assetRefs, - }); + // Because Kibana assets are installed in parallel with ES assets with refresh: false, we almost always run into an + // issue that causes a conflict error due to this issue: https://github.com/elastic/kibana/issues/126240. This is safe + // to retry constantly until it succeeds to optimize this critical user journey path as much as possible. + pRetry( + () => + savedObjectsClient.update( + PACKAGES_SAVED_OBJECT_TYPE, + pkgName, + { + installed_kibana: assetRefs, + }, + { refresh: false } + ), + { retries: 20 } // Use a number of retries higher than the number of es asset update operations + ); + return assetRefs; }; -export const saveInstalledEsRefs = async ( +/** + * Utility function for updating the installed_es field of a package + */ +export const updateEsAssetReferences = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - installedAssets: EsAssetReference[] -) => { - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installedAssetsToSave = installedPkg?.attributes.installed_es.concat(installedAssets); + currentAssets: EsAssetReference[], + { + assetsToAdd = [], + assetsToRemove = [], + refresh = false, + }: { + assetsToAdd?: EsAssetReference[]; + assetsToRemove?: EsAssetReference[]; + /** + * Whether or not the update should force a refresh on the SO index. + * Defaults to `false` for faster updates, should only be `wait_for` if the update needs to be queried back from ES + * immediately. + */ + refresh?: 'wait_for' | false; + } +): Promise => { + const withAssetsRemoved = currentAssets.filter(({ type, id }) => { + if ( + assetsToRemove.some( + ({ type: removeType, id: removeId }) => removeType === type && removeId === id + ) + ) { + return false; + } + return true; + }); const deduplicatedAssets = - installedAssetsToSave?.reduce((acc, currentAsset) => { + [...withAssetsRemoved, ...assetsToAdd].reduce((acc, currentAsset) => { const foundAsset = acc.find((asset: EsAssetReference) => asset.id === currentAsset.id); if (!foundAsset) { return acc.concat([currentAsset]); @@ -656,27 +690,30 @@ export const saveInstalledEsRefs = async ( } }, [] as EsAssetReference[]) || []; - await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_es: deduplicatedAssets, - }); - return installedAssets; -}; - -export const removeAssetTypesFromInstalledEs = async ( - savedObjectsClient: SavedObjectsClientContract, - pkgName: string, - assetTypes: AssetType[] -) => { - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installedAssets = installedPkg?.attributes.installed_es; - if (!installedAssets?.length) return; - const installedAssetsToSave = installedAssets?.filter( - (asset) => !assetTypes.includes(asset.type) - ); + const { + attributes: { installed_es: updatedAssets }, + } = + // Because Kibana assets are installed in parallel with ES assets with refresh: false, we almost always run into an + // issue that causes a conflict error due to this issue: https://github.com/elastic/kibana/issues/126240. This is safe + // to retry constantly until it succeeds to optimize this critical user journey path as much as possible. + await pRetry( + () => + savedObjectsClient.update( + PACKAGES_SAVED_OBJECT_TYPE, + pkgName, + { + installed_es: deduplicatedAssets, + }, + { + refresh, + } + ), + // Use a lower number of retries for ES assets since they're installed in serial and can only conflict with + // the single Kibana update call. + { retries: 5 } + ); - return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_es: installedAssetsToSave, - }); + return updatedAssets ?? []; }; export async function ensurePackagesCompletedInstall( diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 7edf5b6020be8c..95e65acfebef65 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -130,6 +130,8 @@ function deleteESAssets( return deleteTransforms(esClient, [id]); } else if (assetType === ElasticsearchAssetType.dataStreamIlmPolicy) { return deleteIlms(esClient, [id]); + } else if (assetType === ElasticsearchAssetType.ilmPolicy) { + return deleteIlms(esClient, [id]); } else if (assetType === ElasticsearchAssetType.mlModel) { return deleteMlModel(esClient, [id]); } diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index 2ae531f63379de..1074e975d3f6ff 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -152,7 +152,8 @@ export async function fetchFindLatestPackageOrUndefined( export async function fetchInfo(pkgName: string, pkgVersion: string): Promise { const registryUrl = getRegistryUrl(); try { - const res = await fetchUrl(`${registryUrl}/package/${pkgName}/${pkgVersion}`).then(JSON.parse); + // Trailing slash avoids 301 redirect / extra hop + const res = await fetchUrl(`${registryUrl}/package/${pkgName}/${pkgVersion}/`).then(JSON.parse); return res; } catch (err) { diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index 8f2b3effd94ed9..16f8fc04aa92fc 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -569,6 +569,10 @@ const expectAssetsInstalled = ({ id: 'metrics-all_assets.test_metrics-all_assets', type: 'data_stream_ilm_policy', }, + { + id: 'all_assets', + type: 'ilm_policy', + }, { id: 'logs-all_assets.test_logs', type: 'index_template', diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index b73ca9537990cd..9758107cee83d2 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -403,13 +403,17 @@ export default function (providerContext: FtrProviderContext) { ], installed_es: [ { - id: 'logs-all_assets.test_logs-all_assets', - type: 'data_stream_ilm_policy', + id: 'all_assets', + type: 'ilm_policy', }, { id: 'default', type: 'ml_model', }, + { + id: 'logs-all_assets.test_logs-all_assets', + type: 'data_stream_ilm_policy', + }, { id: 'logs-all_assets.test_logs-0.2.0', type: 'ingest_pipeline', From 6dc4f7b3cfd6e6ef6f00437e602f501d745062be Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Thu, 5 May 2022 15:32:00 +0300 Subject: [PATCH 24/83] [Usage Collection] remove daily rollups for ui counters and application usage (#130794) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- ...grations_state_action_machine.test.ts.snap | 30 +++ .../migrations/core/unused_types.ts | 2 + .../collectors/application_usage/README.md | 7 +- .../collectors/application_usage/constants.ts | 5 - .../collectors/application_usage/index.ts | 1 - .../application_usage/rollups/daily.test.ts | 203 ------------------ .../application_usage/rollups/daily.ts | 143 ------------ .../application_usage/rollups/index.ts | 1 - .../telemetry_application_usage_collector.ts | 19 +- .../server/collectors/index.ts | 6 +- .../__fixtures__/ui_counter_saved_objects.ts | 51 ----- .../server/collectors/ui_counters/index.ts | 2 - .../register_ui_counters_collector.test.ts | 98 +-------- .../register_ui_counters_collector.ts | 134 +++--------- .../ui_counters/rollups/constants.ts | 22 -- .../collectors/ui_counters/rollups/index.ts | 9 - .../ui_counters/rollups/register_rollups.ts | 25 --- .../ui_counters/rollups/rollups.test.ts | 192 ----------------- .../collectors/ui_counters/rollups/rollups.ts | 90 -------- .../ui_counter_saved_object_type.ts | 30 --- .../kibana_usage_collection/server/plugin.ts | 6 +- 21 files changed, 71 insertions(+), 1005 deletions(-) delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/ui_counters/__fixtures__/ui_counter_saved_objects.ts delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/constants.ts delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/index.ts delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/register_rollups.ts delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.test.ts delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.ts delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/ui_counters/ui_counter_saved_object_type.ts diff --git a/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap index d26021d28b0e5a..e6e1fc2cdc21d7 100644 --- a/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap +++ b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap @@ -139,6 +139,11 @@ Object { "type": "tsvb-validation-telemetry", }, }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, Object { "bool": Object { "must": Array [ @@ -305,6 +310,11 @@ Object { "type": "tsvb-validation-telemetry", }, }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, Object { "bool": Object { "must": Array [ @@ -475,6 +485,11 @@ Object { "type": "tsvb-validation-telemetry", }, }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, Object { "bool": Object { "must": Array [ @@ -649,6 +664,11 @@ Object { "type": "tsvb-validation-telemetry", }, }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, Object { "bool": Object { "must": Array [ @@ -860,6 +880,11 @@ Object { "type": "tsvb-validation-telemetry", }, }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, Object { "bool": Object { "must": Array [ @@ -1037,6 +1062,11 @@ Object { "type": "tsvb-validation-telemetry", }, }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, Object { "bool": Object { "must": Array [ diff --git a/src/core/server/saved_objects/migrations/core/unused_types.ts b/src/core/server/saved_objects/migrations/core/unused_types.ts index fd4b8a09600d7f..076bdb489cf498 100644 --- a/src/core/server/saved_objects/migrations/core/unused_types.ts +++ b/src/core/server/saved_objects/migrations/core/unused_types.ts @@ -33,6 +33,8 @@ export const REMOVED_TYPES: string[] = [ 'siem-detection-engine-rule-status', // Was removed in 7.16 'timelion-sheet', + // Removed in 8.3 https://github.com/elastic/kibana/issues/127745 + 'ui-counter', ].sort(); // When migrating from the outdated index we use a read query which excludes diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md b/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md index 1f7344a8012277..9b2c6690626fd3 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md @@ -120,10 +120,9 @@ This collection occurs by default for every application registered via the menti In order to keep the count of the events, this collector uses 3 Saved Objects: -1. `application_usage_transactional`: It stores each individually reported event. Grouped by `timestamp` and `appId`. The reason for having these documents instead of editing `application_usage_daily` documents on very report is to provide faster response to the requests to `/api/ui_counters/_report` (creating new documents instead of finding and editing existing ones) and to avoid conflicts when multiple users reach to the API concurrently. -2. `application_usage_daily`: Periodically, documents from `application_usage_transactional` are aggregated to daily summaries and deleted. Also grouped by `timestamp` and `appId` for the main view concatenated with `viewId` for other views. -3. `application_usage_totals`: It stores the sum of all the events older than 90 days old, grouped by `appId` for the main view concatenated with `viewId` for other views. +1. `application_usage_transactional`: It stores each individually reported event. Grouped by `timestamp` and `appId`. +2. `application_usage_totals`: It stores the sum of all the events older than 90 days old, grouped by `appId` for the main view concatenated with `viewId` for other views. -All the types use the shared fields `appId: 'keyword'`, `viewId: 'keyword'`, `numberOfClicks: 'long'` and `minutesOnScreen: 'float'`, but they are currently not added in the mappings because we don't use them for search purposes, and we need to be thoughtful with the number of mapped fields in the SavedObjects index ([#43673](https://github.com/elastic/kibana/issues/43673)). `application_usage_transactional` and `application_usage_daily` also store `timestamp: { type: 'date' }`. +All the types use the shared fields `appId: 'keyword'`, `viewId: 'keyword'`, `numberOfClicks: 'long'` and `minutesOnScreen: 'float'`, but they are currently not added in the mappings because we don't use them for search purposes, and we need to be thoughtful with the number of mapped fields in the SavedObjects index ([#43673](https://github.com/elastic/kibana/issues/43673)). The SO type `application_usage_transactional` also stores `timestamp: { type: 'date' }`. Rollups uses `appId` in the savedObject id for the default view. For other views `viewId` is concatenated. This keeps backwards compatiblity with previously stored documents on the clusters without requiring any form of migration. diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts index f072f044925bfa..1706ec195e577a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts @@ -11,11 +11,6 @@ */ export const ROLL_TOTAL_INDICES_INTERVAL = 24 * 60 * 60 * 1000; -/** - * Roll daily indices every 24h - */ -export const ROLL_DAILY_INDICES_INTERVAL = 24 * 60 * 60 * 1000; - /** * Start rolling indices after 5 minutes up */ diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts index 2d2d07d9d18941..676f5fddc16e1f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts @@ -7,4 +7,3 @@ */ export { registerApplicationUsageCollector } from './telemetry_application_usage_collector'; -export { rollDailyData as migrateTransactionalDocs } from './rollups'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts deleted file mode 100644 index 9c0fab85844bb0..00000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; -import { SavedObjectsErrorHelpers } from '@kbn/core/server'; -import { SAVED_OBJECTS_DAILY_TYPE, SAVED_OBJECTS_TRANSACTIONAL_TYPE } from '../saved_objects_types'; -import { rollDailyData } from './daily'; - -describe('rollDailyData', () => { - const logger = loggingSystemMock.createLogger(); - - test('returns false if no savedObjectsClient initialised yet', async () => { - await expect(rollDailyData(logger, undefined)).resolves.toBe(false); - }); - - test('handle empty results', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case SAVED_OBJECTS_TRANSACTIONAL_TYPE: - return { saved_objects: [], total: 0, page, per_page: perPage }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(true); - expect(savedObjectClient.get).not.toBeCalled(); - expect(savedObjectClient.bulkCreate).not.toBeCalled(); - expect(savedObjectClient.delete).not.toBeCalled(); - }); - - test('migrate some docs', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - let timesCalled = 0; - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case SAVED_OBJECTS_TRANSACTIONAL_TYPE: - if (timesCalled++ > 0) { - return { saved_objects: [], total: 0, page, per_page: perPage }; - } - return { - saved_objects: [ - { - id: 'test-id-1', - type, - score: 0, - references: [], - attributes: { - appId: 'appId', - timestamp: '2020-01-01T10:31:00.000Z', - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }, - { - id: 'test-id-2', - type, - score: 0, - references: [], - attributes: { - appId: 'appId', - timestamp: '2020-01-01T11:31:00.000Z', - minutesOnScreen: 2.5, - numberOfClicks: 2, - }, - }, - { - id: 'test-id-3', - type, - score: 0, - references: [], - attributes: { - appId: 'appId', - viewId: 'appId_viewId', - timestamp: '2020-01-01T11:31:00.000Z', - minutesOnScreen: 1, - numberOfClicks: 5, - }, - }, - ], - total: 3, - page, - per_page: perPage, - }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - - savedObjectClient.get.mockImplementation(async (type, id) => { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - }); - - await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(true); - expect(savedObjectClient.get).toHaveBeenCalledTimes(2); - expect(savedObjectClient.get).toHaveBeenNthCalledWith( - 1, - SAVED_OBJECTS_DAILY_TYPE, - 'appId:2020-01-01' - ); - expect(savedObjectClient.get).toHaveBeenNthCalledWith( - 2, - SAVED_OBJECTS_DAILY_TYPE, - 'appId:2020-01-01:appId_viewId' - ); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith( - [ - { - type: SAVED_OBJECTS_DAILY_TYPE, - id: 'appId:2020-01-01', - attributes: { - appId: 'appId', - viewId: undefined, - timestamp: '2020-01-01T00:00:00.000Z', - minutesOnScreen: 3.0, - numberOfClicks: 3, - }, - }, - { - type: SAVED_OBJECTS_DAILY_TYPE, - id: 'appId:2020-01-01:appId_viewId', - attributes: { - appId: 'appId', - viewId: 'appId_viewId', - timestamp: '2020-01-01T00:00:00.000Z', - minutesOnScreen: 1.0, - numberOfClicks: 5, - }, - }, - ], - { overwrite: true } - ); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(3); - expect(savedObjectClient.delete).toHaveBeenNthCalledWith( - 1, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, - 'test-id-1' - ); - expect(savedObjectClient.delete).toHaveBeenNthCalledWith( - 2, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, - 'test-id-2' - ); - expect(savedObjectClient.delete).toHaveBeenNthCalledWith( - 3, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, - 'test-id-3' - ); - }); - - test('error getting the daily document', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - let timesCalled = 0; - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case SAVED_OBJECTS_TRANSACTIONAL_TYPE: - if (timesCalled++ > 0) { - return { saved_objects: [], total: 0, page, per_page: perPage }; - } - return { - saved_objects: [ - { - id: 'test-id-1', - type, - score: 0, - references: [], - attributes: { - appId: 'appId', - timestamp: '2020-01-01T10:31:00.000Z', - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }, - ], - total: 1, - page, - per_page: perPage, - }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - - savedObjectClient.get.mockImplementation(async (type, id) => { - throw new Error('Something went terribly wrong'); - }); - - await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(false); - expect(savedObjectClient.get).toHaveBeenCalledTimes(1); - expect(savedObjectClient.get).toHaveBeenCalledWith( - SAVED_OBJECTS_DAILY_TYPE, - 'appId:2020-01-01' - ); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(0); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(0); - }); -}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts deleted file mode 100644 index 7cd326eeec3467..00000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import moment from 'moment'; -import type { Logger } from '@kbn/logging'; -import { ISavedObjectsRepository, SavedObject, SavedObjectsErrorHelpers } from '@kbn/core/server'; -import { getDailyId } from '@kbn/usage-collection-plugin/common/application_usage'; -import { - ApplicationUsageDaily, - ApplicationUsageTransactional, - SAVED_OBJECTS_DAILY_TYPE, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, -} from '../saved_objects_types'; - -/** - * For Rolling the daily data, we only care about the stored attributes and the version (to avoid overwriting via concurrent requests) - */ -type ApplicationUsageDailyWithVersion = Pick< - SavedObject, - 'version' | 'attributes' ->; - -/** - * Aggregates all the transactional events into daily aggregates - * @param logger - * @param savedObjectsClient - */ -export async function rollDailyData( - logger: Logger, - savedObjectsClient?: ISavedObjectsRepository -): Promise { - if (!savedObjectsClient) { - return false; - } - - try { - let toCreate: Map; - do { - toCreate = new Map(); - const { saved_objects: rawApplicationUsageTransactional } = - await savedObjectsClient.find({ - type: SAVED_OBJECTS_TRANSACTIONAL_TYPE, - perPage: 1000, // Process 1000 at a time as a compromise of speed and overload - }); - - for (const doc of rawApplicationUsageTransactional) { - const { - attributes: { appId, viewId, minutesOnScreen, numberOfClicks, timestamp }, - } = doc; - const dayId = moment(timestamp).format('YYYY-MM-DD'); - - const dailyId = getDailyId({ dayId, appId, viewId }); - - const existingDoc = - toCreate.get(dailyId) || - (await getDailyDoc(savedObjectsClient, dailyId, appId, viewId, dayId)); - toCreate.set(dailyId, { - ...existingDoc, - attributes: { - ...existingDoc.attributes, - minutesOnScreen: existingDoc.attributes.minutesOnScreen + minutesOnScreen, - numberOfClicks: existingDoc.attributes.numberOfClicks + numberOfClicks, - }, - }); - } - if (toCreate.size > 0) { - await savedObjectsClient.bulkCreate( - [...toCreate.entries()].map(([id, { attributes, version }]) => ({ - type: SAVED_OBJECTS_DAILY_TYPE, - id, - attributes, - version, // Providing version to ensure via conflict matching that only 1 Kibana instance (or interval) is taking care of the updates - })), - { overwrite: true } - ); - const promiseStatuses = await Promise.allSettled( - rawApplicationUsageTransactional.map( - ({ id }) => savedObjectsClient.delete(SAVED_OBJECTS_TRANSACTIONAL_TYPE, id) // There is no bulkDelete :( - ) - ); - const rejectedPromises = promiseStatuses.filter( - (settledResult): settledResult is PromiseRejectedResult => - settledResult.status === 'rejected' - ); - if (rejectedPromises.length > 0) { - throw new Error( - `Failed to delete some items in ${SAVED_OBJECTS_TRANSACTIONAL_TYPE}: ${JSON.stringify( - rejectedPromises.map(({ reason }) => reason) - )}` - ); - } - } - } while (toCreate.size > 0); - return true; - } catch (err) { - logger.debug(`Failed to rollup transactional to daily entries`); - logger.debug(err); - return false; - } -} - -/** - * Gets daily doc from the SavedObjects repository. Creates a new one if not found - * @param savedObjectsClient - * @param id The ID of the document to retrieve (typically, `${appId}:${dayId}`) - * @param appId The application ID - * @param viewId The application view ID - * @param dayId The date of the document in the format YYYY-MM-DD - */ -async function getDailyDoc( - savedObjectsClient: ISavedObjectsRepository, - id: string, - appId: string, - viewId: string, - dayId: string -): Promise { - try { - const { attributes, version } = await savedObjectsClient.get( - SAVED_OBJECTS_DAILY_TYPE, - id - ); - return { attributes, version }; - } catch (err) { - if (SavedObjectsErrorHelpers.isNotFoundError(err)) { - return { - attributes: { - appId, - viewId, - // Concatenating the day in YYYY-MM-DD form to T00:00:00Z to reduce the TZ effects - timestamp: moment(`${moment(dayId).format('YYYY-MM-DD')}T00:00:00Z`).toISOString(), - minutesOnScreen: 0, - numberOfClicks: 0, - }, - }; - } - throw err; - } -} diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts index 8f3d83613aa9d7..484036841b8f7a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -export { rollDailyData } from './daily'; export { rollTotals } from './total'; export { serializeKey } from './utils'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts index 15856c21760ce1..5a75cea43d88c0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts @@ -19,12 +19,8 @@ import { SAVED_OBJECTS_TOTAL_TYPE, } from './saved_objects_types'; import { applicationUsageSchema } from './schema'; -import { rollTotals, rollDailyData, serializeKey } from './rollups'; -import { - ROLL_TOTAL_INDICES_INTERVAL, - ROLL_DAILY_INDICES_INTERVAL, - ROLL_INDICES_START, -} from './constants'; +import { rollTotals, serializeKey } from './rollups'; +import { ROLL_TOTAL_INDICES_INTERVAL, ROLL_INDICES_START } from './constants'; import { ApplicationUsageTelemetryReport, ApplicationUsageViews } from './types'; export const transformByApplicationViews = ( @@ -60,17 +56,6 @@ export function registerApplicationUsageCollector( rollTotals(logger, getSavedObjectsClient()) ); - const dailyRollingSub = timer(ROLL_INDICES_START, ROLL_DAILY_INDICES_INTERVAL).subscribe( - async () => { - const success = await rollDailyData(logger, getSavedObjectsClient()); - // we only need to roll the transactional documents once to assure BWC - // once we rolling succeeds, we can stop. - if (success) { - dailyRollingSub.unsubscribe(); - } - } - ); - const collector = usageCollection.makeUsageCollector( { type: 'application_usage', diff --git a/src/plugins/kibana_usage_collection/server/collectors/index.ts b/src/plugins/kibana_usage_collection/server/collectors/index.ts index e4ed24611bfa8c..6de234b5de434e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/index.ts @@ -19,11 +19,7 @@ export { registerCspCollector } from './csp'; export { registerCoreUsageCollector } from './core'; export { registerLocalizationUsageCollector } from './localization'; export { registerConfigUsageCollector } from './config_usage'; -export { - registerUiCountersUsageCollector, - registerUiCounterSavedObjectType, - registerUiCountersRollups, -} from './ui_counters'; +export { registerUiCountersUsageCollector } from './ui_counters'; export { registerUsageCountersRollups, registerUsageCountersUsageCollector, diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/__fixtures__/ui_counter_saved_objects.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/__fixtures__/ui_counter_saved_objects.ts deleted file mode 100644 index ebc958c7be8c6a..00000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/__fixtures__/ui_counter_saved_objects.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { UICounterSavedObject } from '../ui_counter_saved_object_type'; -export const rawUiCounters: UICounterSavedObject[] = [ - { - type: 'ui-counter', - id: 'Kibana_home:23102020:click:different_type', - attributes: { - count: 1, - }, - references: [], - updated_at: '2020-11-24T11:27:57.067Z', - version: 'WzI5NDRd', - }, - { - type: 'ui-counter', - id: 'Kibana_home:25102020:loaded:intersecting_event', - attributes: { - count: 1, - }, - references: [], - updated_at: '2020-10-25T11:27:57.067Z', - version: 'WzI5NDRd', - }, - { - type: 'ui-counter', - id: 'Kibana_home:23102020:loaded:intersecting_event', - attributes: { - count: 3, - }, - references: [], - updated_at: '2020-10-23T11:27:57.067Z', - version: 'WzI5NDRd', - }, - { - type: 'ui-counter', - id: 'Kibana_home:24112020:click:only_reported_in_ui_counters', - attributes: { - count: 1, - }, - references: [], - updated_at: '2020-11-24T11:27:57.067Z', - version: 'WzI5NDRd', - }, -]; diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/index.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/index.ts index 795e4a75aa236e..cc547266c618df 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/index.ts @@ -7,5 +7,3 @@ */ export { registerUiCountersUsageCollector } from './register_ui_counters_collector'; -export { registerUiCounterSavedObjectType } from './ui_counter_saved_object_type'; -export { registerUiCountersRollups } from './rollups'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.test.ts index 9d702be86aa48f..0e84df3325d3d7 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.test.ts @@ -6,16 +6,9 @@ * Side Public License, v 1. */ -import { - transformRawUiCounterObject, - transformRawUsageCounterObject, - createFetchUiCounters, -} from './register_ui_counters_collector'; -import { BehaviorSubject } from 'rxjs'; -import { rawUiCounters } from './__fixtures__/ui_counter_saved_objects'; +import { transformRawUsageCounterObject, fetchUiCounters } from './register_ui_counters_collector'; import { rawUsageCounters } from './__fixtures__/usage_counter_saved_objects'; import { savedObjectsClientMock } from '@kbn/core/server/mocks'; -import { UI_COUNTER_SAVED_OBJECT_TYPE } from './ui_counter_saved_object_type'; import { USAGE_COUNTERS_SAVED_OBJECT_TYPE } from '@kbn/usage-collection-plugin/server'; describe('transformRawUsageCounterObject', () => { @@ -63,84 +56,16 @@ describe('transformRawUsageCounterObject', () => { }); }); -describe('transformRawUiCounterObject', () => { - it('transforms ui counters savedObject raw entries', () => { - const result = rawUiCounters.map(transformRawUiCounterObject); - expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "appName": "Kibana_home", - "counterType": "click", - "eventName": "different_type", - "fromTimestamp": "2020-11-24T00:00:00Z", - "lastUpdatedAt": "2020-11-24T11:27:57.067Z", - "total": 1, - }, - Object { - "appName": "Kibana_home", - "counterType": "loaded", - "eventName": "intersecting_event", - "fromTimestamp": "2020-10-25T00:00:00Z", - "lastUpdatedAt": "2020-10-25T11:27:57.067Z", - "total": 1, - }, - Object { - "appName": "Kibana_home", - "counterType": "loaded", - "eventName": "intersecting_event", - "fromTimestamp": "2020-10-23T00:00:00Z", - "lastUpdatedAt": "2020-10-23T11:27:57.067Z", - "total": 3, - }, - Object { - "appName": "Kibana_home", - "counterType": "click", - "eventName": "only_reported_in_ui_counters", - "fromTimestamp": "2020-11-24T00:00:00Z", - "lastUpdatedAt": "2020-11-24T11:27:57.067Z", - "total": 1, - }, - ] - `); - }); -}); - -describe('createFetchUiCounters', () => { - let stopUsingUiCounterIndicies$: BehaviorSubject; +describe('fetchUiCounters', () => { const soClientMock = savedObjectsClientMock.create(); beforeEach(() => { jest.clearAllMocks(); - stopUsingUiCounterIndicies$ = new BehaviorSubject(false); - }); - - it('does not query ui_counters saved objects if stopUsingUiCounterIndicies$ is complete', async () => { - // @ts-expect-error incomplete mock implementation - soClientMock.find.mockImplementation(async ({ type }) => { - switch (type) { - case USAGE_COUNTERS_SAVED_OBJECT_TYPE: - return { saved_objects: rawUsageCounters }; - default: - throw new Error(`unexpected type ${type}`); - } - }); - - stopUsingUiCounterIndicies$.complete(); - // @ts-expect-error incomplete mock implementation - const { dailyEvents } = await createFetchUiCounters(stopUsingUiCounterIndicies$)({ - soClient: soClientMock, - }); - - const transforemdUsageCounters = rawUsageCounters.map(transformRawUsageCounterObject); - expect(soClientMock.find).toBeCalledTimes(1); - expect(dailyEvents).toEqual(transforemdUsageCounters.filter(Boolean)); }); - it('merges saved objects from both ui_counters and usage_counters saved objects', async () => { + it('returns saved objects only from usage_counters saved objects', async () => { // @ts-expect-error incomplete mock implementation soClientMock.find.mockImplementation(async ({ type }) => { switch (type) { - case UI_COUNTER_SAVED_OBJECT_TYPE: - return { saved_objects: rawUiCounters }; case USAGE_COUNTERS_SAVED_OBJECT_TYPE: return { saved_objects: rawUsageCounters }; default: @@ -149,10 +74,10 @@ describe('createFetchUiCounters', () => { }); // @ts-expect-error incomplete mock implementation - const { dailyEvents } = await createFetchUiCounters(stopUsingUiCounterIndicies$)({ + const { dailyEvents } = await fetchUiCounters({ soClient: soClientMock, }); - expect(dailyEvents).toHaveLength(7); + expect(dailyEvents).toHaveLength(4); const intersectingEntry = dailyEvents.find( ({ eventName, fromTimestamp }) => eventName === 'intersecting_event' && fromTimestamp === '2020-10-23T00:00:00Z' @@ -179,16 +104,7 @@ describe('createFetchUiCounters', () => { expect(invalidCountEntry).toBe(undefined); expect(nonUiCountersEntry).toBe(undefined); expect(zeroCountEntry).toBe(undefined); - expect(onlyFromUICountersEntry).toMatchInlineSnapshot(` - Object { - "appName": "Kibana_home", - "counterType": "click", - "eventName": "only_reported_in_ui_counters", - "fromTimestamp": "2020-11-24T00:00:00Z", - "lastUpdatedAt": "2020-11-24T11:27:57.067Z", - "total": 1, - } - `); + expect(onlyFromUICountersEntry).toBe(undefined); expect(onlyFromUsageCountersEntry).toMatchInlineSnapshot(` Object { "appName": "myApp", @@ -206,7 +122,7 @@ describe('createFetchUiCounters', () => { "eventName": "intersecting_event", "fromTimestamp": "2020-10-23T00:00:00Z", "lastUpdatedAt": "2020-10-23T11:27:57.067Z", - "total": 63, + "total": 60, } `); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.ts index 5d741a6df8e3d0..c2e17c24de4888 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.ts @@ -7,8 +7,6 @@ */ import moment from 'moment'; -import { mergeWith } from 'lodash'; -import type { Subject } from 'rxjs'; import { CollectorFetchContext, @@ -16,18 +14,9 @@ import { USAGE_COUNTERS_SAVED_OBJECT_TYPE, UsageCountersSavedObject, UsageCountersSavedObjectAttributes, - serializeCounterKey, } from '@kbn/usage-collection-plugin/server'; -import { - deserializeUiCounterName, - serializeUiCounterName, -} from '@kbn/usage-collection-plugin/common/ui_counters'; -import { - UICounterSavedObject, - UICounterSavedObjectAttributes, - UI_COUNTER_SAVED_OBJECT_TYPE, -} from './ui_counter_saved_object_type'; +import { deserializeUiCounterName } from '@kbn/usage-collection-plugin/common/ui_counters'; interface UiCounterEvent { appName: string; @@ -42,32 +31,6 @@ export interface UiCountersUsage { dailyEvents: UiCounterEvent[]; } -export function transformRawUiCounterObject( - rawUiCounter: UICounterSavedObject -): UiCounterEvent | undefined { - const { - id, - attributes: { count }, - updated_at: lastUpdatedAt, - } = rawUiCounter; - if (typeof count !== 'number' || count < 1) { - return; - } - - const [appName, , counterType, ...restId] = id.split(':'); - const eventName = restId.join(':'); - const fromTimestamp = moment(lastUpdatedAt).utc().startOf('day').format(); - - return { - appName, - eventName, - lastUpdatedAt, - fromTimestamp, - counterType, - total: count, - }; -} - export function transformRawUsageCounterObject( rawUsageCounter: UsageCountersSavedObject ): UiCounterEvent | undefined { @@ -93,80 +56,33 @@ export function transformRawUsageCounterObject( }; } -export const createFetchUiCounters = (stopUsingUiCounterIndicies$: Subject) => - async function fetchUiCounters({ soClient }: CollectorFetchContext) { - const { saved_objects: rawUsageCounters } = - await soClient.find({ - type: USAGE_COUNTERS_SAVED_OBJECT_TYPE, - fields: ['count', 'counterName', 'counterType', 'domainId'], - filter: `${USAGE_COUNTERS_SAVED_OBJECT_TYPE}.attributes.domainId: uiCounter`, - perPage: 10000, - }); - - const skipFetchingUiCounters = stopUsingUiCounterIndicies$.isStopped; - const result = - skipFetchingUiCounters || - (await soClient.find({ - type: UI_COUNTER_SAVED_OBJECT_TYPE, - fields: ['count'], - perPage: 10000, - })); +export async function fetchUiCounters({ soClient }: CollectorFetchContext) { + const { saved_objects: rawUsageCounters } = + await soClient.find({ + type: USAGE_COUNTERS_SAVED_OBJECT_TYPE, + fields: ['count', 'counterName', 'counterType', 'domainId'], + filter: `${USAGE_COUNTERS_SAVED_OBJECT_TYPE}.attributes.domainId: uiCounter`, + perPage: 10000, + }); - const rawUiCounters = typeof result === 'object' ? result.saved_objects : []; - const dailyEventsFromUiCounters = rawUiCounters.reduce((acc, raw) => { - try { - const event = transformRawUiCounterObject(raw); - if (event) { - const { appName, eventName, counterType } = event; - const key = serializeCounterKey({ - domainId: 'uiCounter', - counterName: serializeUiCounterName({ appName, eventName }), - counterType, - date: event.lastUpdatedAt, - }); - - acc[key] = event; - } - } catch (_) { - // swallow error; allows sending successfully transformed objects. - } - return acc; - }, {} as Record); - - const dailyEventsFromUsageCounters = rawUsageCounters.reduce((acc, raw) => { - try { - const event = transformRawUsageCounterObject(raw); - if (event) { - acc[raw.id] = event; - } - } catch (_) { - // swallow error; allows sending successfully transformed objects. - } - return acc; - }, {} as Record); - - const mergedDailyCounters = mergeWith( - dailyEventsFromUsageCounters, - dailyEventsFromUiCounters, - (value: UiCounterEvent | undefined, srcValue: UiCounterEvent): UiCounterEvent => { - if (!value) { - return srcValue; + return { + dailyEvents: Object.values( + rawUsageCounters.reduce((acc, raw) => { + try { + const event = transformRawUsageCounterObject(raw); + if (event) { + acc[raw.id] = event; + } + } catch (_) { + // swallow error; allows sending successfully transformed objects. } - - return { - ...srcValue, - total: srcValue.total + value.total, - }; - } - ); - - return { dailyEvents: Object.values(mergedDailyCounters) }; + return acc; + }, {} as Record) + ), }; +} -export function registerUiCountersUsageCollector( - usageCollection: UsageCollectionSetup, - stopUsingUiCounterIndicies$: Subject -) { +export function registerUiCountersUsageCollector(usageCollection: UsageCollectionSetup) { const collector = usageCollection.makeUsageCollector({ type: 'ui_counters', schema: { @@ -197,7 +113,7 @@ export function registerUiCountersUsageCollector( }, }, }, - fetch: createFetchUiCounters(stopUsingUiCounterIndicies$), + fetch: fetchUiCounters, isReady: () => true, }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/constants.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/constants.ts deleted file mode 100644 index 1301c4b57d3802..00000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/constants.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/** - * Roll indices every 24h - */ -export const ROLL_INDICES_INTERVAL = 24 * 60 * 60 * 1000; - -/** - * Start rolling indices after 5 minutes up - */ -export const ROLL_INDICES_START = 5 * 60 * 1000; - -/** - * Number of days to keep the UI counters saved object documents - */ -export const UI_COUNTERS_KEEP_DOCS_FOR_DAYS = 3; diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/index.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/index.ts deleted file mode 100644 index c4ce88e1a851a2..00000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { registerUiCountersRollups } from './register_rollups'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/register_rollups.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/register_rollups.ts deleted file mode 100644 index 859a50e01401a0..00000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/register_rollups.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Subject, timer } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; -import { Logger, ISavedObjectsRepository } from '@kbn/core/server'; -import { ROLL_INDICES_INTERVAL, ROLL_INDICES_START } from './constants'; -import { rollUiCounterIndices } from './rollups'; - -export function registerUiCountersRollups( - logger: Logger, - stopRollingUiCounterIndicies$: Subject, - getSavedObjectsClient: () => ISavedObjectsRepository | undefined -) { - timer(ROLL_INDICES_START, ROLL_INDICES_INTERVAL) - .pipe(takeUntil(stopRollingUiCounterIndicies$)) - .subscribe(() => - rollUiCounterIndices(logger, stopRollingUiCounterIndicies$, getSavedObjectsClient()) - ); -} diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.test.ts deleted file mode 100644 index e5414ed0d50013..00000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import moment from 'moment'; -import * as Rx from 'rxjs'; -import { isSavedObjectOlderThan, rollUiCounterIndices } from './rollups'; -import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; -import { SavedObjectsFindResult } from '@kbn/core/server'; - -import { - UICounterSavedObjectAttributes, - UI_COUNTER_SAVED_OBJECT_TYPE, -} from '../ui_counter_saved_object_type'; -import { UI_COUNTERS_KEEP_DOCS_FOR_DAYS } from './constants'; - -const createMockSavedObjectDoc = (updatedAt: moment.Moment, id: string) => - ({ - id, - type: 'ui-counter', - attributes: { - count: 3, - }, - references: [], - updated_at: updatedAt.format(), - version: 'WzI5LDFd', - score: 0, - } as SavedObjectsFindResult); - -describe('isSavedObjectOlderThan', () => { - it(`returns true if doc is older than x days`, () => { - const numberOfDays = 1; - const startDate = moment().format(); - const doc = createMockSavedObjectDoc(moment().subtract(2, 'days'), 'some-id'); - const result = isSavedObjectOlderThan({ - numberOfDays, - startDate, - doc, - }); - expect(result).toBe(true); - }); - - it(`returns false if doc is exactly x days old`, () => { - const numberOfDays = 1; - const startDate = moment().format(); - const doc = createMockSavedObjectDoc(moment().subtract(1, 'days'), 'some-id'); - const result = isSavedObjectOlderThan({ - numberOfDays, - startDate, - doc, - }); - expect(result).toBe(false); - }); - - it(`returns false if doc is younger than x days`, () => { - const numberOfDays = 2; - const startDate = moment().format(); - const doc = createMockSavedObjectDoc(moment().subtract(1, 'days'), 'some-id'); - const result = isSavedObjectOlderThan({ - numberOfDays, - startDate, - doc, - }); - expect(result).toBe(false); - }); -}); - -describe('rollUiCounterIndices', () => { - let logger: ReturnType; - let savedObjectClient: ReturnType; - let stopUsingUiCounterIndicies$: Rx.Subject; - - beforeEach(() => { - logger = loggingSystemMock.createLogger(); - savedObjectClient = savedObjectsRepositoryMock.create(); - stopUsingUiCounterIndicies$ = new Rx.Subject(); - }); - - it('returns undefined if no savedObjectsClient initialised yet', async () => { - await expect( - rollUiCounterIndices(logger, stopUsingUiCounterIndicies$, undefined) - ).resolves.toBe(undefined); - expect(logger.warn).toHaveBeenCalledTimes(0); - }); - - it('does not delete any documents on empty saved objects', async () => { - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case UI_COUNTER_SAVED_OBJECT_TYPE: - return { saved_objects: [], total: 0, page, per_page: perPage }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - await expect( - rollUiCounterIndices(logger, stopUsingUiCounterIndicies$, savedObjectClient) - ).resolves.toEqual([]); - expect(savedObjectClient.find).toBeCalled(); - expect(savedObjectClient.delete).not.toBeCalled(); - expect(logger.warn).toHaveBeenCalledTimes(0); - }); - it('calls Subject complete() on empty saved objects', async () => { - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case UI_COUNTER_SAVED_OBJECT_TYPE: - return { saved_objects: [], total: 0, page, per_page: perPage }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - await expect( - rollUiCounterIndices(logger, stopUsingUiCounterIndicies$, savedObjectClient) - ).resolves.toEqual([]); - expect(stopUsingUiCounterIndicies$.isStopped).toBe(true); - }); - - it(`deletes documents older than ${UI_COUNTERS_KEEP_DOCS_FOR_DAYS} days`, async () => { - const mockSavedObjects = [ - createMockSavedObjectDoc(moment().subtract(5, 'days'), 'doc-id-1'), - createMockSavedObjectDoc(moment().subtract(1, 'days'), 'doc-id-2'), - createMockSavedObjectDoc(moment().subtract(6, 'days'), 'doc-id-3'), - ]; - - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case UI_COUNTER_SAVED_OBJECT_TYPE: - return { saved_objects: mockSavedObjects, total: 0, page, per_page: perPage }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - await expect( - rollUiCounterIndices(logger, stopUsingUiCounterIndicies$, savedObjectClient) - ).resolves.toHaveLength(2); - expect(savedObjectClient.find).toBeCalled(); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(2); - expect(savedObjectClient.delete).toHaveBeenNthCalledWith( - 1, - UI_COUNTER_SAVED_OBJECT_TYPE, - 'doc-id-1' - ); - expect(savedObjectClient.delete).toHaveBeenNthCalledWith( - 2, - UI_COUNTER_SAVED_OBJECT_TYPE, - 'doc-id-3' - ); - expect(logger.warn).toHaveBeenCalledTimes(0); - }); - - it(`logs warnings on savedObject.find failure`, async () => { - savedObjectClient.find.mockImplementation(async () => { - throw new Error(`Expected error!`); - }); - await expect( - rollUiCounterIndices(logger, stopUsingUiCounterIndicies$, savedObjectClient) - ).resolves.toEqual(undefined); - expect(savedObjectClient.find).toBeCalled(); - expect(savedObjectClient.delete).not.toBeCalled(); - expect(logger.warn).toHaveBeenCalledTimes(2); - }); - - it(`logs warnings on savedObject.delete failure`, async () => { - const mockSavedObjects = [createMockSavedObjectDoc(moment().subtract(5, 'days'), 'doc-id-1')]; - - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case UI_COUNTER_SAVED_OBJECT_TYPE: - return { saved_objects: mockSavedObjects, total: 0, page, per_page: perPage }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - savedObjectClient.delete.mockImplementation(async () => { - throw new Error(`Expected error!`); - }); - await expect( - rollUiCounterIndices(logger, stopUsingUiCounterIndicies$, savedObjectClient) - ).resolves.toEqual(undefined); - expect(savedObjectClient.find).toBeCalled(); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(1); - expect(savedObjectClient.delete).toHaveBeenNthCalledWith( - 1, - UI_COUNTER_SAVED_OBJECT_TYPE, - 'doc-id-1' - ); - expect(logger.warn).toHaveBeenCalledTimes(2); - }); -}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.ts deleted file mode 100644 index ca472fe0825f9c..00000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ISavedObjectsRepository, Logger } from '@kbn/core/server'; -import moment from 'moment'; -import type { Subject } from 'rxjs'; - -import { UI_COUNTERS_KEEP_DOCS_FOR_DAYS } from './constants'; -import { - UICounterSavedObject, - UI_COUNTER_SAVED_OBJECT_TYPE, -} from '../ui_counter_saved_object_type'; - -export function isSavedObjectOlderThan({ - numberOfDays, - startDate, - doc, -}: { - numberOfDays: number; - startDate: moment.Moment | string | number; - doc: Pick; -}): boolean { - const { updated_at: updatedAt } = doc; - const today = moment(startDate).startOf('day'); - const updateDay = moment(updatedAt).startOf('day'); - - const diffInDays = today.diff(updateDay, 'days'); - if (diffInDays > numberOfDays) { - return true; - } - - return false; -} - -export async function rollUiCounterIndices( - logger: Logger, - stopUsingUiCounterIndicies$: Subject, - savedObjectsClient?: ISavedObjectsRepository -) { - if (!savedObjectsClient) { - return; - } - - const now = moment(); - - try { - const { saved_objects: rawUiCounterDocs } = await savedObjectsClient.find( - { - type: UI_COUNTER_SAVED_OBJECT_TYPE, - perPage: 1000, // Process 1000 at a time as a compromise of speed and overload - } - ); - - if (rawUiCounterDocs.length === 0) { - /** - * @deprecated 7.13 to be removed in 8.0.0 - * Stop triggering rollups when we've rolled up all documents. - * - * This Saved Object registry is no longer used. - * Migration from one SO registry to another is not yet supported. - * In a future release we can remove this piece of code and - * migrate any docs to the Usage Counters Saved object. - * - * @removeBy 8.0.0 - */ - - stopUsingUiCounterIndicies$.complete(); - } - - const docsToDelete = rawUiCounterDocs.filter((doc) => - isSavedObjectOlderThan({ - numberOfDays: UI_COUNTERS_KEEP_DOCS_FOR_DAYS, - startDate: now, - doc, - }) - ); - - return await Promise.all( - docsToDelete.map(({ id }) => savedObjectsClient.delete(UI_COUNTER_SAVED_OBJECT_TYPE, id)) - ); - } catch (err) { - logger.warn(`Failed to rollup UI Counters saved objects.`); - logger.warn(err); - } -} diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/ui_counter_saved_object_type.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/ui_counter_saved_object_type.ts deleted file mode 100644 index 2d4e680a61a2f2..00000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/ui_counter_saved_object_type.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { SavedObject, SavedObjectAttributes, SavedObjectsServiceSetup } from '@kbn/core/server'; - -export interface UICounterSavedObjectAttributes extends SavedObjectAttributes { - count: number; -} - -export type UICounterSavedObject = SavedObject; - -export const UI_COUNTER_SAVED_OBJECT_TYPE = 'ui-counter'; - -export function registerUiCounterSavedObjectType(savedObjectsSetup: SavedObjectsServiceSetup) { - savedObjectsSetup.registerType({ - name: UI_COUNTER_SAVED_OBJECT_TYPE, - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: { - count: { type: 'integer' }, - }, - }, - }); -} diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 4442757e2df684..34bf0293113071 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -37,8 +37,6 @@ import { registerCoreUsageCollector, registerLocalizationUsageCollector, registerUiCountersUsageCollector, - registerUiCounterSavedObjectType, - registerUiCountersRollups, registerConfigUsageCollector, registerUsageCountersRollups, registerUsageCountersUsageCollector, @@ -125,9 +123,7 @@ export class KibanaUsageCollectionPlugin implements Plugin { const getUiSettingsClient = () => this.uiSettingsClient; const getCoreUsageDataService = () => this.coreUsageData!; - registerUiCounterSavedObjectType(coreSetup.savedObjects); - registerUiCountersRollups(this.logger.get('ui-counters'), pluginStop$, getSavedObjectsClient); - registerUiCountersUsageCollector(usageCollection, pluginStop$); + registerUiCountersUsageCollector(usageCollection); registerUsageCountersRollups(this.logger.get('usage-counters-rollup'), getSavedObjectsClient); registerUsageCountersUsageCollector(usageCollection); From e923d92b3ca067b8b6058c634e524e5ffd6f07be Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 May 2022 14:53:43 +0200 Subject: [PATCH 25/83] Introduces StreamAggregator to Synththrace (#130902) * Introduces StreamAggregator This allows us to write 'true' stream processing aggregations. Implementations of `StreamAggregator` can self bootstrap new datastreams/timeseries and route data to this new locations * if a stream aggregator returns dimensions setup timeseries otherwise datastream * rename worker.ts to allign with new naming rules * Pick fields from ApmFields (cherry picked from commit 0147c683d2ccda2953fcbf5ef24a801cdf34a5dd) * include service.environment and transaction.type as dimensions (cherry picked from commit 2f0b6044eef768349613fabf8a250cfc0375bc7b) * rename service.latency to transaction.duration.aggregate (cherry picked from commit f4d8b17302be9dd56e4c518fcc8919a998b1c40b) * removed unnecessary intermediate method createFieldsFromState() in favor of flush() (cherry picked from commit 6e3f5cd6dc898214740d1b483c7dc29839514695) * ensure we flush previously held range if current event exceeds max window age (cherry picked from commit 55a52f1d592a67511782c7522c69836a615c0d93) * move the processor.name to 'metric' for now (cherry picked from commit 480bbe4120937c4e2cd597ac61a9ee279df42a89) * clean aggregator stream with wildcard for namespace (cherry picked from commit 9fb7905dfbaa9b3cd906411ec721e8794655fc98) * add apm-ui as codeowners of synthtrace * metricset is not always set should not throw an error when determining writetarget * safeguard check for max window age * safeguard incrementing state Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + .../aggregators/service_latency_aggregator.ts | 183 ++++++++++++++++++ .../apm/client/apm_synthtrace_es_client.ts | 71 ++++++- .../src/lib/stream_aggregator.ts | 27 +++ .../src/lib/stream_processor.ts | 36 +++- .../src/scripts/run_synthtrace.ts | 7 +- .../src/scripts/utils/synthtrace_worker.ts | 4 + 7 files changed, 311 insertions(+), 18 deletions(-) create mode 100644 packages/elastic-apm-synthtrace/src/lib/apm/aggregators/service_latency_aggregator.ts create mode 100644 packages/elastic-apm-synthtrace/src/lib/stream_aggregator.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7f7c048717f028..156a306b12e890 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -128,6 +128,7 @@ /src/apm.js @elastic/kibana-core @vigneshshanmugam /packages/kbn-apm-config-loader/ @elastic/kibana-core @vigneshshanmugam /src/core/types/elasticsearch @elastic/apm-ui +/packages/elastic-apm-synthtrace/ @elastic/apm-ui #CC# /src/plugins/apm_oss/ @elastic/apm-ui #CC# /x-pack/plugins/observability/ @elastic/apm-ui diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/aggregators/service_latency_aggregator.ts b/packages/elastic-apm-synthtrace/src/lib/apm/aggregators/service_latency_aggregator.ts new file mode 100644 index 00000000000000..e28ba234b2a49e --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/apm/aggregators/service_latency_aggregator.ts @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { random } from 'lodash'; +import { Client } from '@elastic/elasticsearch'; +import { ApmFields } from '../apm_fields'; +import { Fields } from '../../entity'; +import { StreamAggregator } from '../../stream_aggregator'; + +type LatencyState = { + count: number; + min: number; + max: number; + sum: number; + timestamp: number; +} & Pick; + +export type ServiceFields = Fields & + Pick< + ApmFields, + | 'timestamp.us' + | 'ecs.version' + | 'metricset.name' + | 'observer' + | 'processor.event' + | 'processor.name' + | 'service.name' + | 'service.version' + | 'service.environment' + | 'transaction.type' + > & + Partial<{ + 'transaction.duration.aggregate': { + min: number; + max: number; + sum: number; + value_count: number; + }; + }>; + +export class ServiceLatencyAggregator implements StreamAggregator { + public readonly name; + + constructor() { + this.name = 'service-latency'; + } + + getDataStreamName(): string { + return 'metrics-apm.service'; + } + + getMappings(): Record { + return { + properties: { + '@timestamp': { + type: 'date', + format: 'date_optional_time||epoch_millis', + }, + transaction: { + type: 'object', + properties: { + type: { type: 'keyword', time_series_dimension: true }, + duration: { + type: 'object', + properties: { + aggregate: { + type: 'aggregate_metric_double', + metrics: ['min', 'max', 'sum', 'value_count'], + default_metric: 'sum', + time_series_metric: 'gauge', + }, + }, + }, + }, + }, + service: { + type: 'object', + properties: { + name: { type: 'keyword', time_series_dimension: true }, + environment: { type: 'keyword', time_series_dimension: true }, + }, + }, + }, + }; + } + + getDimensions(): string[] { + return ['service.name', 'service.environment', 'transaction.type']; + } + + getWriteTarget(document: Record): string | null { + const eventType = document.metricset?.name; + if (eventType === 'service') return 'metrics-apm.service-default'; + return null; + } + + private state: Record = {}; + + private processedComponent: number = 0; + + process(event: ApmFields): Fields[] | null { + if (!event['@timestamp']) return null; + const service = event['service.name']!; + const environment = event['service.environment'] ?? 'production'; + const transactionType = event['transaction.type'] ?? 'request'; + const key = `${service}-${environment}-${transactionType}`; + const addToState = (timestamp: number) => { + if (!this.state[key]) { + this.state[key] = { + timestamp, + count: 0, + min: 0, + max: 0, + sum: 0, + 'service.name': service, + 'service.environment': environment, + 'transaction.type': transactionType, + }; + } + const duration = Number(event['transaction.duration.us']); + if (duration >= 0) { + const state = this.state[key]; + + state.count++; + state.sum += duration; + if (duration > state.max) state.max = duration; + if (duration < state.min) state.min = Math.min(0, duration); + } + }; + + // ensure we flush current state first if event falls out of the current max window age + if (this.state[key]) { + const diff = Math.abs(event['@timestamp'] - this.state[key].timestamp); + if (diff >= 1000 * 60) { + const fields = this.createServiceFields(key); + delete this.state[key]; + addToState(event['@timestamp']); + return [fields]; + } + } + + addToState(event['@timestamp']); + // if cardinality is too high force emit of current state + if (Object.keys(this.state).length === 1000) { + return this.flush(); + } + + return null; + } + + flush(): Fields[] { + const fields = Object.keys(this.state).map((key) => this.createServiceFields(key)); + this.state = {}; + return fields; + } + + private createServiceFields(key: string): ServiceFields { + this.processedComponent = ++this.processedComponent % 1000; + const component = Date.now() % 100; + const state = this.state[key]; + return { + '@timestamp': state.timestamp + random(0, 100) + component + this.processedComponent, + 'metricset.name': 'service', + 'processor.event': 'metric', + 'service.name': state['service.name'], + 'service.environment': state['service.environment'], + 'transaction.type': state['transaction.type'], + 'transaction.duration.aggregate': { + min: state.min, + max: state.max, + sum: state.sum, + value_count: state.count, + }, + }; + } + + async bootstrapElasticsearch(esClient: Client): Promise {} +} diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client.ts b/packages/elastic-apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client.ts index 6e7fb5ffdb1bcf..91bec0ba49c526 100644 --- a/packages/elastic-apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client.ts @@ -7,6 +7,7 @@ */ import { Client } from '@elastic/elasticsearch'; +import { IndicesIndexSettings } from '@elastic/elasticsearch/lib/api/types'; import { cleanWriteTargets } from '../../utils/clean_write_targets'; import { getApmWriteTargets } from '../utils/get_apm_write_targets'; import { Logger } from '../../utils/create_logger'; @@ -15,6 +16,7 @@ import { EntityIterable } from '../../entity_iterable'; import { StreamProcessor } from '../../stream_processor'; import { EntityStreams } from '../../entity_streams'; import { Fields } from '../../entity'; +import { StreamAggregator } from '../../stream_aggregator'; export interface StreamToBulkOptions { concurrency?: number; @@ -57,7 +59,7 @@ export class ApmSynthtraceEsClient { return info.version.number; } - async clean() { + async clean(dataStreams?: string[]) { return this.getWriteTargets().then(async (writeTargets) => { const indices = Object.values(writeTargets); this.logger.info(`Attempting to clean: ${indices}`); @@ -68,7 +70,7 @@ export class ApmSynthtraceEsClient { logger: this.logger, }); } - for (const name of indices) { + for (const name of indices.concat(dataStreams ?? [])) { const dataStream = await this.client.indices.getDataStream({ name }, { ignore: [404] }); if (dataStream.data_streams && dataStream.data_streams.length > 0) { this.logger.debug(`Deleting datastream: ${name}`); @@ -149,7 +151,6 @@ export class ApmSynthtraceEsClient { streamProcessor?: StreamProcessor ) { const dataStream = Array.isArray(events) ? new EntityStreams(events) : events; - const sp = streamProcessor != null ? streamProcessor @@ -165,7 +166,7 @@ export class ApmSynthtraceEsClient { await this.logger.perf('enumerate_scenario', async () => { // @ts-ignore // We just want to enumerate - for await (item of sp.streamToDocumentAsync(sp.toDocument, dataStream)) { + for await (item of sp.streamToDocumentAsync((e) => sp.toDocument(e), dataStream)) { if (yielded === 0) { options.itemStartStopCallback?.apply(this, [item, false]); yielded++; @@ -185,7 +186,7 @@ export class ApmSynthtraceEsClient { flushBytes: 500000, // TODO https://github.com/elastic/elasticsearch-js/issues/1610 // having to map here is awkward, it'd be better to map just before serialization. - datasource: sp.streamToDocumentAsync(sp.toDocument, dataStream), + datasource: sp.streamToDocumentAsync((e) => sp.toDocument(e), dataStream), onDrop: (doc) => { this.logger.info(JSON.stringify(doc, null, 2)); }, @@ -197,11 +198,12 @@ export class ApmSynthtraceEsClient { options?.itemStartStopCallback?.apply(this, [item, false]); yielded++; } - const index = options?.mapToIndex - ? options?.mapToIndex(item) - : !this.forceLegacyIndices - ? StreamProcessor.getDataStreamForEvent(item, writeTargets) - : StreamProcessor.getIndexForEvent(item, writeTargets); + let index = options?.mapToIndex ? options?.mapToIndex(item) : null; + if (!index) { + index = !this.forceLegacyIndices + ? sp.getDataStreamForEvent(item, writeTargets) + : StreamProcessor.getIndexForEvent(item, writeTargets); + } return { create: { _index: index } }; }, }); @@ -211,4 +213,53 @@ export class ApmSynthtraceEsClient { await this.refresh(); } } + + async createDataStream(aggregator: StreamAggregator) { + const datastreamName = aggregator.getDataStreamName(); + const mappings = aggregator.getMappings(); + const dimensions = aggregator.getDimensions(); + + const indexSettings: IndicesIndexSettings = { lifecycle: { name: 'metrics' } }; + if (dimensions.length > 0) { + indexSettings.mode = 'time_series'; + indexSettings.routing_path = dimensions; + } + + await this.client.cluster.putComponentTemplate({ + name: `${datastreamName}-mappings`, + template: { + mappings, + }, + _meta: { + description: `Mappings for ${datastreamName}-*`, + }, + }); + this.logger.info(`Created mapping component template for ${datastreamName}-*`); + + await this.client.cluster.putComponentTemplate({ + name: `${datastreamName}-settings`, + template: { + settings: { + index: indexSettings, + }, + }, + _meta: { + description: `Settings for ${datastreamName}-*`, + }, + }); + this.logger.info(`Created settings component template for ${datastreamName}-*`); + + await this.client.indices.putIndexTemplate({ + name: `${datastreamName}-index_template`, + index_patterns: [`${datastreamName}-*`], + data_stream: {}, + composed_of: [`${datastreamName}-mappings`, `${datastreamName}-settings`], + priority: 500, + }); + this.logger.info(`Created index template for ${datastreamName}-*`); + + await this.client.indices.createDataStream({ name: datastreamName + '-default' }); + + await aggregator.bootstrapElasticsearch(this.client); + } } diff --git a/packages/elastic-apm-synthtrace/src/lib/stream_aggregator.ts b/packages/elastic-apm-synthtrace/src/lib/stream_aggregator.ts new file mode 100644 index 00000000000000..3076b105a10fd3 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/stream_aggregator.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { Client } from '@elastic/elasticsearch'; +import { ApmFields, Fields } from '..'; + +export interface StreamAggregator { + name: string; + + getWriteTarget(document: Record): string | null; + + process(event: TFields): Fields[] | null; + + flush(): Fields[]; + + bootstrapElasticsearch(esClient: Client): Promise; + + getDataStreamName(): string; + + getDimensions(): string[]; + + getMappings(): Record; +} diff --git a/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts b/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts index 17ced20f5d7ed7..e1cb332996e236 100644 --- a/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts +++ b/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts @@ -17,10 +17,12 @@ import { dedot } from './utils/dedot'; import { ApmElasticsearchOutputWriteTargets } from './apm/utils/get_apm_write_targets'; import { Logger } from './utils/create_logger'; import { Fields } from './entity'; +import { StreamAggregator } from './stream_aggregator'; export interface StreamProcessorOptions { version?: string; - processors: Array<(events: TFields[]) => TFields[]>; + processors?: Array<(events: TFields[]) => TFields[]>; + streamAggregators?: Array>; flushInterval?: string; // defaults to 10k maxBufferSize?: number; @@ -39,6 +41,8 @@ export class StreamProcessor { getBreakdownMetrics, ]; public static defaultFlushInterval: number = 10000; + private readonly processors: Array<(events: TFields[]) => TFields[]>; + private readonly streamAggregators: Array>; constructor(private readonly options: StreamProcessorOptions) { [this.intervalAmount, this.intervalUnit] = this.options.flushInterval @@ -47,6 +51,8 @@ export class StreamProcessor { this.name = this.options?.name ?? 'StreamProcessor'; this.version = this.options.version ?? '8.0.0'; this.versionMajor = Number.parseInt(this.version.split('.')[0], 10); + this.processors = options.processors ?? []; + this.streamAggregators = options.streamAggregators ?? []; } private readonly intervalAmount: number; private readonly intervalUnit: any; @@ -73,6 +79,15 @@ export class StreamProcessor { yield StreamProcessor.enrich(event, this.version, this.versionMajor); sourceEventsYielded++; + for (const aggregator of this.streamAggregators) { + const aggregatedEvents = aggregator.process(event); + if (aggregatedEvents) { + yield* aggregatedEvents.map((d) => + StreamProcessor.enrich(d, this.version, this.versionMajor) + ); + } + } + if (sourceEventsYielded % maxBufferSize === 0) { if (this.options?.processedCallback) { this.options.processedCallback(maxBufferSize); @@ -96,7 +111,7 @@ export class StreamProcessor { this.options.logger?.debug( `${this.name} flush ${localBuffer.length} documents ${order}: ${e} => ${f}` ); - for (const processor of this.options.processors) { + for (const processor of this.processors) { yield* processor(localBuffer).map((d) => StreamProcessor.enrich(d, this.version, this.versionMajor) ); @@ -116,13 +131,16 @@ export class StreamProcessor { this.options.logger?.info( `${this.name} processing remaining buffer: ${localBuffer.length} items left` ); - for (const processor of this.options.processors) { + for (const processor of this.processors) { yield* processor(localBuffer).map((d) => StreamProcessor.enrich(d, this.version, this.versionMajor) ); } this.options.processedCallback?.apply(this, [localBuffer.length]); } + for (const aggregator of this.streamAggregators) { + yield* aggregator.flush(); + } } private calculateFlushAfter(eventDate: number | null, order: 'asc' | 'desc') { @@ -186,10 +204,7 @@ export class StreamProcessor { return newDoc; } - static getDataStreamForEvent( - d: Record, - writeTargets: ApmElasticsearchOutputWriteTargets - ) { + getDataStreamForEvent(d: Record, writeTargets: ApmElasticsearchOutputWriteTargets) { if (!d.processor?.event) { throw Error("'processor.event' is not set on document, can not determine target index"); } @@ -204,6 +219,13 @@ export class StreamProcessor { } } } + for (const aggregator of this.streamAggregators) { + const target = aggregator.getWriteTarget(d); + if (target) { + dataStream = target; + break; + } + } return dataStream; } diff --git a/packages/elastic-apm-synthtrace/src/scripts/run_synthtrace.ts b/packages/elastic-apm-synthtrace/src/scripts/run_synthtrace.ts index 7ba3251def9830..5e007d9adeac4d 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/run_synthtrace.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/run_synthtrace.ts @@ -14,6 +14,8 @@ import { startLiveDataUpload } from './utils/start_live_data_upload'; import { parseRunCliFlags } from './utils/parse_run_cli_flags'; import { getCommonServices } from './utils/get_common_services'; import { ApmSynthtraceKibanaClient } from '../lib/apm/client/apm_synthtrace_kibana_client'; +import { StreamAggregator } from '../lib/stream_aggregator'; +import { ServiceLatencyAggregator } from '../lib/apm/aggregators/service_latency_aggregator'; function options(y: Argv) { return y @@ -186,8 +188,9 @@ yargs(process.argv.slice(2)) await apmEsClient.updateComponentTemplates(runOptions.numShards); } + const aggregators: StreamAggregator[] = [new ServiceLatencyAggregator()]; if (argv.clean) { - await apmEsClient.clean(); + await apmEsClient.clean(aggregators.map((a) => a.getDataStreamName() + '-*')); } if (runOptions.gcpRepository) { await apmEsClient.registerGcpRepository(runOptions.gcpRepository); @@ -205,6 +208,8 @@ yargs(process.argv.slice(2)) )}` ); + for (const aggregator of aggregators) await apmEsClient.createDataStream(aggregator); + if (runOptions.maxDocs !== 0) await startHistoricalDataUpload(apmEsClient, logger, runOptions, from, to, version); diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/synthtrace_worker.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/synthtrace_worker.ts index 4e4ef8a02ff719..76b6e2ce6b6d8f 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/synthtrace_worker.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/synthtrace_worker.ts @@ -15,6 +15,8 @@ import { LogLevel } from '../../lib/utils/create_logger'; import { StreamProcessor } from '../../lib/stream_processor'; import { Scenario } from '../scenario'; import { EntityIterable, Fields } from '../..'; +import { StreamAggregator } from '../../lib/stream_aggregator'; +import { ServiceLatencyAggregator } from '../../lib/apm/aggregators/service_latency_aggregator'; // logging proxy to main thread, ensures we see real time logging const l = { @@ -61,9 +63,11 @@ async function setup() { parentPort?.postMessage({ workerIndex, lastTimestamp: item['@timestamp'] }); } }; + const aggregators: StreamAggregator[] = [new ServiceLatencyAggregator()]; streamProcessor = new StreamProcessor({ version, processors: StreamProcessor.apmProcessors, + streamAggregators: aggregators, maxSourceEvents: runOptions.maxDocs, logger: l, processedCallback: (processedDocuments) => { From 8942cba40b67573e344dc1c4a34877b6c618cbde Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 5 May 2022 15:04:58 +0200 Subject: [PATCH 26/83] [Fleet] Alternative way of fetching data stream stats (#130973) --- .../fleet/common/constants/data_streams.ts | 14 ++ .../server/routes/data_streams/handlers.ts | 206 ++++++++++++------ .../fleet/server/routes/data_streams/index.ts | 3 +- 3 files changed, 156 insertions(+), 67 deletions(-) create mode 100644 x-pack/plugins/fleet/common/constants/data_streams.ts diff --git a/x-pack/plugins/fleet/common/constants/data_streams.ts b/x-pack/plugins/fleet/common/constants/data_streams.ts new file mode 100644 index 00000000000000..bb880af9b3df87 --- /dev/null +++ b/x-pack/plugins/fleet/common/constants/data_streams.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +export const GetDataStreamsListRequestSchema = { + params: schema.object({ + use_terms_enum: schema.boolean({ defaultValue: false }), + }), +}; diff --git a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts index 2d01344a930aa1..ad3b356828d723 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts @@ -6,13 +6,16 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { keyBy, keys, merge } from 'lodash'; -import type { RequestHandler } from '@kbn/core/server'; +import type { RequestHandler, ElasticsearchClient } from '@kbn/core/server'; + +import type { TypeOf } from '@kbn/config-schema'; import type { DataStream } from '../../types'; import { KibanaSavedObjectType } from '../../../common'; import type { GetDataStreamsResponse } from '../../../common'; import { getPackageSavedObjects } from '../../services/epm/packages/get'; import { defaultIngestErrorHandler } from '../../errors'; +import type { GetDataStreamsListRequestSchema } from '../../../common/constants/data_streams'; const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*,traces-*-*,synthetics-*-*'; @@ -37,11 +40,139 @@ interface ESDataStreamInfo { hidden: boolean; } +async function getMetadataFromTermsEnum({ + dataStreamName, + esClient, +}: { + dataStreamName: string; + esClient: ElasticsearchClient; +}) { + const [maxEventIngestedResponse, namespaceResponse, datasetResponse, typeResponse] = + await Promise.all([ + esClient.search({ + size: 1, + index: dataStreamName, + sort: { + // @ts-expect-error Type '{ 'event.ingested': string; }' is not assignable to type 'string | string[] | undefined'. + 'event.ingested': 'desc', + }, + _source: false, + fields: ['event.ingested'], + }), + esClient.termsEnum({ + index: dataStreamName, + field: 'data_stream.namespace', + }), + esClient.termsEnum({ + index: dataStreamName, + field: 'data_stream.dataset', + }), + esClient.termsEnum({ + index: dataStreamName, + field: 'data_stream.type', + }), + ]); + + const maxIngested = new Date( + maxEventIngestedResponse.hits.hits[0]?.fields!['event.ingested'] + ).getTime(); + + const namespace = namespaceResponse.terms[0] ?? ''; + const dataset = datasetResponse.terms[0] ?? ''; + const type = typeResponse.terms[0] ?? ''; + + return { + maxIngested, + namespace, + dataset, + type, + }; +} + +async function getMetadataFromAggregations({ + dataStreamName, + esClient, +}: { + dataStreamName: string; + esClient: ElasticsearchClient; +}) { + // Query backing indices to extract data stream dataset, namespace, and type values + const { aggregations: dataStreamAggs } = await esClient.search({ + index: dataStreamName, + body: { + size: 0, + query: { + bool: { + filter: [ + { + exists: { + field: 'data_stream.namespace', + }, + }, + { + exists: { + field: 'data_stream.dataset', + }, + }, + ], + }, + }, + aggs: { + maxIngestedTimestamp: { + max: { + field: 'event.ingested', + }, + }, + dataset: { + terms: { + field: 'data_stream.dataset', + size: 1, + }, + }, + namespace: { + terms: { + field: 'data_stream.namespace', + size: 1, + }, + }, + type: { + terms: { + field: 'data_stream.type', + size: 1, + }, + }, + }, + }, + }); + + const { maxIngestedTimestamp } = dataStreamAggs as Record< + string, + estypes.AggregationsRateAggregate + >; + const { dataset, namespace, type } = dataStreamAggs as Record< + string, + estypes.AggregationsMultiBucketAggregateBase<{ key?: string; value?: number }> + >; + + const maxIngested = maxIngestedTimestamp?.value; + + return { + maxIngested, + dataset: (dataset.buckets as Array<{ key?: string; value?: number }>)[0]?.key || '', + namespace: (namespace.buckets as Array<{ key?: string; value?: number }>)[0]?.key || '', + type: (type.buckets as Array<{ key?: string; value?: number }>)[0]?.key || '', + }; +} + export const getListHandler: RequestHandler = async (context, request, response) => { // Query datastreams as the current user as the Kibana internal user may not have all the required permission const { savedObjects, elasticsearch } = await context.core; const esClient = elasticsearch.client.asCurrentUser; + const { use_terms_enum: useTermsEnum } = request.params as TypeOf< + typeof GetDataStreamsListRequestSchema['params'] + >; + const body: GetDataStreamsResponse = { data_streams: [], }; @@ -127,75 +258,18 @@ export const getListHandler: RequestHandler = async (context, request, response) dashboards: [], }; - // Query backing indices to extract data stream dataset, namespace, and type values - const { aggregations: dataStreamAggs } = await esClient.search({ - index: dataStream.name, - body: { - size: 0, - query: { - bool: { - filter: [ - { - exists: { - field: 'data_stream.namespace', - }, - }, - { - exists: { - field: 'data_stream.dataset', - }, - }, - ], - }, - }, - aggs: { - maxIngestedTimestamp: { - max: { - field: 'event.ingested', - }, - }, - dataset: { - terms: { - field: 'data_stream.dataset', - size: 1, - }, - }, - namespace: { - terms: { - field: 'data_stream.namespace', - size: 1, - }, - }, - type: { - terms: { - field: 'data_stream.type', - size: 1, - }, - }, - }, - }, - }); - - const { maxIngestedTimestamp } = dataStreamAggs as Record< - string, - estypes.AggregationsRateAggregate - >; - const { dataset, namespace, type } = dataStreamAggs as Record< - string, - estypes.AggregationsMultiBucketAggregateBase<{ key?: string; value?: number }> - >; + const { maxIngested, namespace, dataset, type } = useTermsEnum + ? await getMetadataFromTermsEnum({ dataStreamName: dataStream.name, esClient }) + : await getMetadataFromAggregations({ dataStreamName: dataStream.name, esClient }); // some integrations e.g custom logs don't have event.ingested - if (maxIngestedTimestamp?.value) { - dataStreamResponse.last_activity_ms = maxIngestedTimestamp?.value; + if (maxIngested) { + dataStreamResponse.last_activity_ms = maxIngested; } - dataStreamResponse.dataset = - (dataset.buckets as Array<{ key?: string; value?: number }>)[0]?.key || ''; - dataStreamResponse.namespace = - (namespace.buckets as Array<{ key?: string; value?: number }>)[0]?.key || ''; - dataStreamResponse.type = - (type.buckets as Array<{ key?: string; value?: number }>)[0]?.key || ''; + dataStreamResponse.dataset = dataset; + dataStreamResponse.namespace = namespace; + dataStreamResponse.type = type; // Find package saved object const pkgName = dataStreamResponse.package; diff --git a/x-pack/plugins/fleet/server/routes/data_streams/index.ts b/x-pack/plugins/fleet/server/routes/data_streams/index.ts index ddefc537ba207b..d7491d87e2a17a 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/index.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { GetDataStreamsListRequestSchema } from '../../../common/constants/data_streams'; import { DATA_STREAM_API_ROUTES } from '../../constants'; import type { FleetAuthzRouter } from '../security'; @@ -15,7 +16,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => { router.get( { path: DATA_STREAM_API_ROUTES.LIST_PATTERN, - validate: false, + validate: GetDataStreamsListRequestSchema, fleetAuthz: { fleet: { all: true }, }, From 3545f313f137d557fc9699dcb6717e459521a619 Mon Sep 17 00:00:00 2001 From: mgiota Date: Thu, 5 May 2022 15:51:45 +0200 Subject: [PATCH 27/83] Sync status filtering with urlbar (#131523) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/pages/rules/index.tsx | 22 +++++++++++-------- .../rules/state_container/state_container.tsx | 18 +++++++++++++++ .../use_rules_page_state_container.tsx | 6 +++-- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/rules/index.tsx b/x-pack/plugins/observability/public/pages/rules/index.tsx index 801ea24fb46c3d..a409754a51a141 100644 --- a/x-pack/plugins/observability/public/pages/rules/index.tsx +++ b/x-pack/plugins/observability/public/pages/rules/index.tsx @@ -23,11 +23,11 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { deleteRules, RuleTableItem, + RuleStatus, enableRule, disableRule, snoozeRule, useLoadRuleTypes, - RuleStatus, unsnoozeRule, } from '@kbn/triggers-actions-ui-plugin/public'; import { RuleExecutionStatus, ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; @@ -83,7 +83,6 @@ function RulesPage() { application: { capabilities }, notifications: { toasts }, } = useKibana().services; - const { lastResponse, setLastResponse } = useRulesPageStateContainer(); const documentationLink = docLinks.links.observability.createAlerts; const ruleTypeRegistry = triggersActionsUi.ruleTypeRegistry; const canExecuteActions = hasExecuteActionsCapability(capabilities); @@ -94,8 +93,9 @@ function RulesPage() { }); const [inputText, setInputText] = useState(); const [searchText, setSearchText] = useState(); - const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]); const [typesFilter, setTypesFilter] = useState([]); + const { lastResponse, setLastResponse } = useRulesPageStateContainer(); + const { status, setStatus } = useRulesPageStateContainer(); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); const [rulesToDelete, setRulesToDelete] = useState([]); const [createRuleFlyoutVisibility, setCreateRuleFlyoutVisibility] = useState(false); @@ -111,7 +111,7 @@ function RulesPage() { const { rulesState, setRulesState, reload, noData, initialLoad } = useFetchRules({ searchText, ruleLastResponseFilter: lastResponse, - ruleStatusesFilter, + ruleStatusesFilter: status, typesFilter, page, setPage, @@ -289,6 +289,13 @@ function RulesPage() { [] ); + const setRuleStatusFilter = useCallback( + (ids: RuleStatus[]) => { + setStatus(ids); + }, + [setStatus] + ); + const setExecutionStatusFilter = useCallback( (ids: string[]) => { setLastResponse(ids); @@ -311,9 +318,6 @@ function RulesPage() { return ; } - // const nextSearchParams = new URLSearchParams(history.location.search); - // const xx = [...nextSearchParams.getAll('executionStatus')] || []; - // console.log(xx, '!!'); return ( <> @@ -357,8 +361,8 @@ function RulesPage() { {triggersActionsUi.getRuleStatusFilter({ - selectedStatuses: ruleStatusesFilter, - onChange: setRuleStatusesFilter, + selectedStatuses: status, + onChange: setRuleStatusFilter, })} diff --git a/x-pack/plugins/observability/public/pages/rules/state_container/state_container.tsx b/x-pack/plugins/observability/public/pages/rules/state_container/state_container.tsx index b36ffca96972e2..039218add35084 100644 --- a/x-pack/plugins/observability/public/pages/rules/state_container/state_container.tsx +++ b/x-pack/plugins/observability/public/pages/rules/state_container/state_container.tsx @@ -9,19 +9,23 @@ import { createStateContainer, createStateContainerReactHelpers, } from '@kbn/kibana-utils-plugin/public'; +import { RuleStatus } from '@kbn/triggers-actions-ui-plugin/public'; interface RulesPageContainerState { lastResponse: string[]; + status: RuleStatus[]; } const defaultState: RulesPageContainerState = { lastResponse: [], + status: [], }; interface RulesPageStateTransitions { setLastResponse: ( state: RulesPageContainerState ) => (lastResponse: string[]) => RulesPageContainerState; + setStatus: (state: RulesPageContainerState) => (status: RuleStatus[]) => RulesPageContainerState; } const transitions: RulesPageStateTransitions = { @@ -39,6 +43,20 @@ const transitions: RulesPageStateTransitions = { }); return { ...state, lastResponse: filteredIds }; }, + setStatus: (state) => (status) => { + const filteredIds = status; + status.forEach((id) => { + const isPreviouslyChecked = state.status.includes(id); + if (!isPreviouslyChecked) { + filteredIds.concat(id); + } else { + filteredIds.filter((val) => { + return val !== id; + }); + } + }); + return { ...state, status: filteredIds }; + }, }; const rulesPageStateContainer = createStateContainer(defaultState, transitions); diff --git a/x-pack/plugins/observability/public/pages/rules/state_container/use_rules_page_state_container.tsx b/x-pack/plugins/observability/public/pages/rules/state_container/use_rules_page_state_container.tsx index 6b44dc8ae31d55..cd20de3f95c291 100644 --- a/x-pack/plugins/observability/public/pages/rules/state_container/use_rules_page_state_container.tsx +++ b/x-pack/plugins/observability/public/pages/rules/state_container/use_rules_page_state_container.tsx @@ -27,12 +27,14 @@ export function useRulesPageStateContainer() { useUrlStateSyncEffect(stateContainer); - const { setLastResponse } = stateContainer.transitions; - const { lastResponse } = useContainerSelector(stateContainer, (state) => state); + const { setLastResponse, setStatus } = stateContainer.transitions; + const { lastResponse, status } = useContainerSelector(stateContainer, (state) => state); return { lastResponse, + status, setLastResponse, + setStatus, }; } From 5ab0fa580d6b707e5aaa570acd25f7e57fc686b2 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 5 May 2022 09:54:03 -0400 Subject: [PATCH 28/83] [ResponseOps] [RAM] Align flyout pagination with discover (#131193) * Initial version * Uncomment for now * PR feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../alerts_flyout/alerts_flyout.test.tsx | 33 ++++-- .../alerts_flyout/alerts_flyout.tsx | 110 +++++++++++------- .../alerts_table/alerts_table.test.tsx | 14 +-- .../sections/alerts_table/alerts_table.tsx | 12 +- .../alerts_table/hooks/use_pagination.test.ts | 28 ++--- .../alerts_table/hooks/use_pagination.ts | 17 +-- .../apps/triggers_actions_ui/alerts_table.ts | 96 ++++++++------- 7 files changed, 183 insertions(+), 127 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.test.tsx index cbb2ad745a3b2d..08b68bd342a5be 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.test.tsx @@ -11,16 +11,17 @@ import { AlertsFlyout } from './alerts_flyout'; import { AlertsField } from '../../../../types'; const onClose = jest.fn(); -const onPaginateNext = jest.fn(); -const onPaginatePrevious = jest.fn(); +const onPaginate = jest.fn(); const props = { alert: { [AlertsField.name]: ['one'], [AlertsField.reason]: ['two'], }, + flyoutIndex: 0, + alertsCount: 4, + isLoading: false, onClose, - onPaginateNext, - onPaginatePrevious, + onPaginate, }; describe('AlertsFlyout', () => { @@ -34,19 +35,31 @@ describe('AlertsFlyout', () => { await nextTick(); wrapper.update(); }); - expect(wrapper.find('[data-test-subj="alertsFlyoutTitle"]').first().text()).toBe('one'); + expect(wrapper.find('[data-test-subj="alertsFlyoutName"]').first().text()).toBe('one'); expect(wrapper.find('[data-test-subj="alertsFlyoutReason"]').first().text()).toBe('two'); }); - it('should allow pagination', async () => { + it('should allow pagination with next', async () => { const wrapper = mountWithIntl(); await act(async () => { await nextTick(); wrapper.update(); }); - wrapper.find('[data-test-subj="alertsFlyoutPaginatePrevious"]').first().simulate('click'); - expect(onPaginatePrevious).toHaveBeenCalled(); - wrapper.find('[data-test-subj="alertsFlyoutPaginateNext"]').first().simulate('click'); - expect(onPaginateNext).toHaveBeenCalled(); + wrapper.find('[data-test-subj="pagination-button-next"]').first().simulate('click'); + expect(onPaginate).toHaveBeenCalledWith(1); + }); + + it('should allow pagination with previous', async () => { + const customProps = { + ...props, + flyoutIndex: 1, + }; + const wrapper = mountWithIntl(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('[data-test-subj="pagination-button-previous"]').first().simulate('click'); + expect(onPaginate).toHaveBeenCalledWith(0); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.tsx index 51174ca7b9a800..44236a8d993f5d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.tsx @@ -15,81 +15,111 @@ import { EuiTitle, EuiText, EuiHorizontalRule, - EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem, - EuiButton, + EuiPagination, + EuiProgress, + EuiLoadingContent, } from '@elastic/eui'; import { AlertsField, AlertsData } from '../../../../types'; -const REASON_LABEL = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.reason', +const SAMPLE_TITLE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.sampleTitle', { - defaultMessage: 'Reason', + defaultMessage: 'Sample title', } ); -const NEXT_LABEL = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.next', +const NAME_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.name', { - defaultMessage: 'Next', + defaultMessage: 'Name', } ); -const PREVIOUS_LABEL = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.previous', + +const REASON_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.reason', + { + defaultMessage: 'Reason', + } +); + +const PAGINATION_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.paginationLabel', { - defaultMessage: 'Previous', + defaultMessage: 'Alert navigation', } ); interface AlertsFlyoutProps { alert: AlertsData; + flyoutIndex: number; + alertsCount: number; + isLoading: boolean; onClose: () => void; - onPaginateNext: () => void; - onPaginatePrevious: () => void; + onPaginate: (pageIndex: number) => void; } export const AlertsFlyout: React.FunctionComponent = ({ alert, + flyoutIndex, + alertsCount, + isLoading, onClose, - onPaginateNext, - onPaginatePrevious, + onPaginate, }: AlertsFlyoutProps) => { return ( + {isLoading && } - -

{get(alert, AlertsField.name)}

+ +

{SAMPLE_TITLE_LABEL}

+ + + + + +
- -

{REASON_LABEL}

-
- - - {get(alert, AlertsField.reason)} - - - -
- - + - - {PREVIOUS_LABEL} - + +

{NAME_LABEL}

+
+ + {isLoading ? ( + + ) : ( + + {get(alert, AlertsField.name, [])[0]} + + )}
- - {NEXT_LABEL} - + +

{REASON_LABEL}

+
+ + {isLoading ? ( + + ) : ( + + {get(alert, AlertsField.reason, [])[0]} + + )}
-
+ + +
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx index 6aa3c172206685..6a8c6a0ff96808 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx @@ -132,16 +132,16 @@ describe('AlertsTable', () => { const result = await wrapper.findAllByTestId('alertsFlyout'); expect(result.length).toBe(1); - expect(wrapper.queryByTestId('alertsFlyoutTitle')?.textContent).toBe('one'); + expect(wrapper.queryByTestId('alertsFlyoutName')?.textContent).toBe('one'); expect(wrapper.queryByTestId('alertsFlyoutReason')?.textContent).toBe('two'); // Should paginate too - userEvent.click(wrapper.queryAllByTestId('alertsFlyoutPaginateNext')[0]); - expect(wrapper.queryByTestId('alertsFlyoutTitle')?.textContent).toBe('three'); + userEvent.click(wrapper.queryAllByTestId('pagination-button-next')[0]); + expect(wrapper.queryByTestId('alertsFlyoutName')?.textContent).toBe('three'); expect(wrapper.queryByTestId('alertsFlyoutReason')?.textContent).toBe('four'); - userEvent.click(wrapper.queryAllByTestId('alertsFlyoutPaginatePrevious')[0]); - expect(wrapper.queryByTestId('alertsFlyoutTitle')?.textContent).toBe('one'); + userEvent.click(wrapper.queryAllByTestId('pagination-button-previous')[0]); + expect(wrapper.queryByTestId('alertsFlyoutName')?.textContent).toBe('one'); expect(wrapper.queryByTestId('alertsFlyoutReason')?.textContent).toBe('two'); }); @@ -152,10 +152,10 @@ describe('AlertsTable', () => { const result = await wrapper.findAllByTestId('alertsFlyout'); expect(result.length).toBe(1); - userEvent.click(wrapper.queryAllByTestId('alertsFlyoutPaginateNext')[0]); + userEvent.click(wrapper.queryAllByTestId('pagination-button-next')[0]); expect(fetchAlertsData.onPageChange).toHaveBeenCalledWith({ pageIndex: 1, pageSize: 1 }); - userEvent.click(wrapper.queryAllByTestId('alertsFlyoutPaginatePrevious')[0]); + userEvent.click(wrapper.queryAllByTestId('pagination-button-previous')[0]); expect(fetchAlertsData.onPageChange).toHaveBeenCalledWith({ pageIndex: 0, pageSize: 1 }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx index da05b4c175bdd9..dca547e65ae271 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx @@ -39,14 +39,14 @@ const emptyConfiguration = { const AlertsTable: React.FunctionComponent = (props: AlertsTableProps) => { const [rowClasses, setRowClasses] = useState({}); - const { activePage, alertsCount, onPageChange, onSortChange } = props.useFetchAlertsData(); + const { activePage, alertsCount, onPageChange, onSortChange, isLoading } = + props.useFetchAlertsData(); const { sortingColumns, onSort } = useSorting(onSortChange); const { pagination, onChangePageSize, onChangePageIndex, - onPaginateFlyoutNext, - onPaginateFlyoutPrevious, + onPaginateFlyout, flyoutAlertIndex, setFlyoutAlertIndex, } = usePagination({ @@ -122,9 +122,11 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.test.ts index 6073d907f161d5..70bc4c4ade8fb4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.test.ts @@ -58,31 +58,31 @@ describe('usePagination', () => { expect(result.current.flyoutAlertIndex).toBe(-1); act(() => { - result.current.onPaginateFlyoutNext(); + result.current.onPaginateFlyout(0); }); expect(result.current.flyoutAlertIndex).toBe(0); act(() => { - result.current.onPaginateFlyoutNext(); + result.current.onPaginateFlyout(1); }); expect(result.current.flyoutAlertIndex).toBe(1); act(() => { - result.current.onPaginateFlyoutPrevious(); + result.current.onPaginateFlyout(0); }); expect(result.current.flyoutAlertIndex).toBe(0); }); - it('should paginate the flyout when we need to change the page index', () => { + it('should paginate the flyout when we need to change the page index going back', () => { const { result } = renderHook(() => usePagination({ onPageChange, pageIndex: 0, pageSize: 1, alertsCount }) ); act(() => { - result.current.onPaginateFlyoutPrevious(); + result.current.onPaginateFlyout(-2); }); // It should reset to the first alert in the table @@ -90,25 +90,21 @@ describe('usePagination', () => { // It should go to the last page expect(result.current.pagination).toStrictEqual({ pageIndex: 4, pageSize: 1 }); + }); - act(() => { - result.current.onPaginateFlyoutNext(); - }); - - // It should reset to the first alert in the table - expect(result.current.flyoutAlertIndex).toBe(0); - - // It should go to the first page - expect(result.current.pagination).toStrictEqual({ pageIndex: 0, pageSize: 1 }); + it('should paginate the flyout when we need to change the page index going forward', () => { + const { result } = renderHook(() => + usePagination({ onPageChange, pageIndex: 0, pageSize: 1, alertsCount }) + ); act(() => { - result.current.onPaginateFlyoutNext(); + result.current.onPaginateFlyout(1); }); // It should reset to the first alert in the table expect(result.current.flyoutAlertIndex).toBe(0); - // It should go to the second page + // It should go to the first page expect(result.current.pagination).toStrictEqual({ pageIndex: 1, pageSize: 1 }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts index 484775d9877dd4..76f4f0fa546c4d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts @@ -58,19 +58,20 @@ export function usePagination({ onPageChange, pageIndex, pageSize, alertsCount } }, [pagination, alertsCount, onChangePageIndex] ); - const onPaginateFlyoutNext = useCallback(() => { - paginateFlyout(flyoutAlertIndex + 1); - }, [paginateFlyout, flyoutAlertIndex]); - const onPaginateFlyoutPrevious = useCallback(() => { - paginateFlyout(flyoutAlertIndex - 1); - }, [paginateFlyout, flyoutAlertIndex]); + + const onPaginateFlyout = useCallback( + (nextPageIndex: number) => { + nextPageIndex -= pagination.pageSize * pagination.pageIndex; + paginateFlyout(nextPageIndex); + }, + [paginateFlyout, pagination.pageSize, pagination.pageIndex] + ); return { pagination, onChangePageSize, onChangePageIndex, - onPaginateFlyoutNext, - onPaginateFlyoutPrevious, + onPaginateFlyout, flyoutAlertIndex, setFlyoutAlertIndex, }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_table.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_table.ts index 436770696dbab5..56026093c88dd1 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_table.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_table.ts @@ -87,41 +87,48 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); }); - it('should open a flyout and paginate through the flyout', async () => { - await PageObjects.common.navigateToUrlWithBrowserHistory('triggersActions', '/alerts'); - await waitTableIsLoaded(); - await testSubjects.click('expandColumnCellOpenFlyoutButton-0'); - await waitFlyoutOpen(); - - expect(await testSubjects.getVisibleText('alertsFlyoutTitle')).to.be( - 'APM Failed Transaction Rate (one)' - ); - expect(await testSubjects.getVisibleText('alertsFlyoutReason')).to.be( - 'Failed transactions rate is greater than 5.0% (current value is 31%) for elastic-co-frontend' - ); - - await testSubjects.click('alertsFlyoutPaginateNext'); - - expect(await testSubjects.getVisibleText('alertsFlyoutTitle')).to.be( - 'APM Failed Transaction Rate (one)' - ); - expect(await testSubjects.getVisibleText('alertsFlyoutReason')).to.be( - 'Failed transactions rate is greater than 5.0% (current value is 35%) for opbeans-python' - ); - - await testSubjects.click('alertsFlyoutPaginatePrevious'); - await testSubjects.click('alertsFlyoutPaginatePrevious'); - - await waitTableIsLoaded(); - - const rows = await getRows(); - expect(rows[0].status).to.be('close'); - expect(rows[0].lastUpdated).to.be('2021-10-19T14:55:14.503Z'); - expect(rows[0].duration).to.be('252002000'); - expect(rows[0].reason).to.be( - 'CPU usage is greater than a threshold of 40 (current value is 56.7%) for gke-edge-oblt-default-pool-350b44de-c3dd' - ); - }); + // This keeps failing in CI because the next button is not clickable + // Revisit this once we change the UI around based on feedback + /* + fail: Actions and Triggers app Alerts table should open a flyout and paginate through the flyout + │ Error: retry.try timeout: ElementClickInterceptedError: element click intercepted: Element ... is not clickable at point (1564, 795). Other element would receive the click:
...
+ */ + // it('should open a flyout and paginate through the flyout', async () => { + // await PageObjects.common.navigateToUrlWithBrowserHistory('triggersActions', '/alerts'); + // await waitTableIsLoaded(); + // await testSubjects.click('expandColumnCellOpenFlyoutButton-0'); + // await waitFlyoutOpen(); + // await waitFlyoutIsLoaded(); + + // expect(await testSubjects.getVisibleText('alertsFlyoutName')).to.be( + // 'APM Failed Transaction Rate (one)' + // ); + // expect(await testSubjects.getVisibleText('alertsFlyoutReason')).to.be( + // 'Failed transactions rate is greater than 5.0% (current value is 31%) for elastic-co-frontend' + // ); + + // await testSubjects.click('pagination-button-next'); + + // expect(await testSubjects.getVisibleText('alertsFlyoutName')).to.be( + // 'APM Failed Transaction Rate (one)' + // ); + // expect(await testSubjects.getVisibleText('alertsFlyoutReason')).to.be( + // 'Failed transactions rate is greater than 5.0% (current value is 35%) for opbeans-python' + // ); + + // await testSubjects.click('pagination-button-previous'); + // await testSubjects.click('pagination-button-previous'); + + // await waitTableIsLoaded(); + + // const rows = await getRows(); + // expect(rows[0].status).to.be('close'); + // expect(rows[0].lastUpdated).to.be('2021-10-19T14:55:14.503Z'); + // expect(rows[0].duration).to.be('252002000'); + // expect(rows[0].reason).to.be( + // 'CPU usage is greater than a threshold of 40 (current value is 56.7%) for gke-edge-oblt-default-pool-350b44de-c3dd' + // ); + // }); async function waitTableIsLoaded() { return await retry.try(async () => { @@ -130,12 +137,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); } - async function waitFlyoutOpen() { - return await retry.try(async () => { - const exists = await testSubjects.exists('alertsFlyout'); - if (!exists) throw new Error('Still loading...'); - }); - } + // async function waitFlyoutOpen() { + // return await retry.try(async () => { + // const exists = await testSubjects.exists('alertsFlyout'); + // if (!exists) throw new Error('Still loading...'); + // }); + // } + + // async function waitFlyoutIsLoaded() { + // return await retry.try(async () => { + // const exists = await testSubjects.exists('alertsFlyoutLoading'); + // if (exists) throw new Error('Still loading...'); + // }); + // } async function getRows() { const euiDataGridRows = await find.allByCssSelector('.euiDataGridRow'); From 0b2d02d35dfa1fc3dc0078f9977b5ced6449b5e9 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 5 May 2022 16:40:44 +0200 Subject: [PATCH 29/83] improve function performance (#131530) --- .../t_grid/toolbar/fields_browser/helpers.tsx | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx index d6c2c7de5aa18e..fd590c468b7e7a 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx @@ -6,7 +6,6 @@ */ import { EuiBadge, EuiLoadingSpinner } from '@elastic/eui'; -import { pickBy } from 'lodash/fp'; import styled from 'styled-components'; import { TimelineId } from '../../../../types'; @@ -108,27 +107,46 @@ export const filterSelectedBrowserFields = ({ }): BrowserFields => { const selectedFieldIds = new Set(columnHeaders.map(({ id }) => id)); - const filteredBrowserFields: BrowserFields = Object.keys(browserFields).reduce( - (filteredCategories, categoryId) => ({ - ...filteredCategories, - [categoryId]: { - ...browserFields[categoryId], - fields: pickBy( - ({ name }) => name != null && selectedFieldIds.has(name), - browserFields[categoryId].fields - ), - }, - }), - {} - ); - - // only pick non-empty categories from the filtered browser fields - const nonEmptyCategories: BrowserFields = pickBy( - (category) => categoryHasFields(category), - filteredBrowserFields - ); - - return nonEmptyCategories; + const result: Record> = {}; + + for (const [categoryName, categoryDescriptor] of Object.entries(browserFields)) { + if (!categoryDescriptor.fields) { + // ignore any category that is missing fields. This is not expected to happen. + // eslint-disable-next-line no-continue + continue; + } + + // keep track of whether this category had a selected field, if so, we should emit it into the result + let hadSelected = false; + + // The selected fields for this `categoryName` + const selectedFields: Record> = {}; + + for (const [fieldName, fieldDescriptor] of Object.entries(categoryDescriptor.fields)) { + // For historical reasons, we consider the name as it appears on the field descriptor, not the `fieldName` (attribute name) itself. + // It is unclear if there is any point in continuing to do this. + const fieldNameFromDescriptor = fieldDescriptor.name; + + if (!fieldNameFromDescriptor) { + // Ignore any field that is missing a name in its descriptor. This is not expected to happen. + // eslint-disable-next-line no-continue + continue; + } + + if (selectedFieldIds.has(fieldNameFromDescriptor)) { + hadSelected = true; + selectedFields[fieldName] = fieldDescriptor; + } + } + + if (hadSelected) { + result[categoryName] = { + ...browserFields[categoryName], + fields: selectedFields, + }; + } + } + return result; }; export const getAlertColumnHeader = (timelineId: string, fieldId: string) => From 5684ce27efd4c33f98a64507c4b1dc0271b56783 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 5 May 2022 17:16:18 +0200 Subject: [PATCH 30/83] add routes to observability nav (#131595) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../monitor_management/use_breadcrumbs.ts | 2 +- x-pack/plugins/synthetics/public/plugin.ts | 111 ++++++++++++------ 2 files changed, 77 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts index ab44c1b7c37a2f..be94df18ef7d2f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts @@ -17,7 +17,7 @@ export const useMonitorManagementBreadcrumbs = () => { useBreadcrumbs([ { text: MONITOR_MANAGEMENT_CRUMB, - href: `${appPath}/${MONITOR_MANAGEMENT_ROUTE}/all`, + href: `${appPath}/${MONITOR_MANAGEMENT_ROUTE}`, }, ]); }; diff --git a/x-pack/plugins/synthetics/public/plugin.ts b/x-pack/plugins/synthetics/public/plugin.ts index 88238b1bfbf371..8427f8c3060def 100644 --- a/x-pack/plugins/synthetics/public/plugin.ts +++ b/x-pack/plugins/synthetics/public/plugin.ts @@ -123,41 +123,8 @@ export class UptimePlugin }, }); - plugins.observability.navigation.registerSections( - from(core.getStartServices()).pipe( - map(([coreStart]) => { - if (coreStart.application.capabilities.uptime.show) { - return [ - { - label: 'Uptime', - sortKey: 500, - entries: [ - { - label: i18n.translate('xpack.synthetics.overview.heading', { - defaultMessage: 'Monitors', - }), - app: 'uptime', - path: '/', - matchFullPath: true, - ignoreTrailingSlash: true, - }, - { - label: i18n.translate('xpack.synthetics.certificatesPage.heading', { - defaultMessage: 'TLS Certificates', - }), - app: 'uptime', - path: '/certificates', - matchFullPath: true, - }, - ], - }, - ]; - } - - return []; - }) - ) - ); + registerUptimeRoutesWithNavigation(core, plugins); + registerSyntheticsRoutesWithNavigation(core, plugins); const { observabilityRuleTypeRegistry } = plugins.observability; @@ -270,3 +237,77 @@ export class UptimePlugin public stop(): void {} } + +function registerSyntheticsRoutesWithNavigation( + core: CoreSetup, + plugins: ClientPluginsSetup +) { + plugins.observability.navigation.registerSections( + from(core.getStartServices()).pipe( + map(([coreStart]) => { + if (coreStart.application.capabilities.uptime.show) { + return [ + { + label: 'Synthetics', + sortKey: 499, + entries: [ + { + label: i18n.translate('xpack.synthetics.overview.heading', { + defaultMessage: 'Monitors', + }), + app: 'synthetics', + path: '/manage-monitors', + matchFullPath: true, + ignoreTrailingSlash: true, + }, + ], + }, + ]; + } + + return []; + }) + ) + ); +} + +function registerUptimeRoutesWithNavigation( + core: CoreSetup, + plugins: ClientPluginsSetup +) { + plugins.observability.navigation.registerSections( + from(core.getStartServices()).pipe( + map(([coreStart]) => { + if (coreStart.application.capabilities.uptime.show) { + return [ + { + label: 'Uptime', + sortKey: 500, + entries: [ + { + label: i18n.translate('xpack.synthetics.overview.heading', { + defaultMessage: 'Monitors', + }), + app: 'uptime', + path: '/', + matchFullPath: true, + ignoreTrailingSlash: true, + }, + { + label: i18n.translate('xpack.synthetics.certificatesPage.heading', { + defaultMessage: 'TLS Certificates', + }), + app: 'uptime', + path: '/certificates', + matchFullPath: true, + }, + ], + }, + ]; + } + + return []; + }) + ) + ); +} From 4dd33a9009cf4dcc5eb9b426d07b0dd1e348dea3 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 5 May 2022 11:41:07 -0400 Subject: [PATCH 31/83] [es-snapshots] Fix verify job dependencies after FTR configs change (#131640) --- .buildkite/pipelines/es_snapshots/verify.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index 0d8e11e1c8e8bd..2ebb1d9a3b56bd 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -44,10 +44,8 @@ steps: agents: queue: kibana-default depends_on: - - default-cigroup - - oss-cigroup + - ftr-configs - jest-integration - - api-integration - wait: ~ continue_on_failure: true From 896b0e28ddac628d6e682523e5d1c4dfeade649c Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 5 May 2022 08:59:09 -0700 Subject: [PATCH 32/83] [ci] include test_group_env in more ci scripts (#131633) --- .buildkite/scripts/common/env.sh | 5 +++++ .buildkite/scripts/steps/on_merge_build_and_metrics.sh | 2 -- .buildkite/scripts/steps/test/ftr_configs.sh | 1 - .buildkite/scripts/steps/test/jest_parallel.sh | 2 -- .../scripts/steps/test/pick_test_group_run_order.sh | 1 - .buildkite/scripts/steps/test/test_group_env.sh | 8 -------- 6 files changed, 5 insertions(+), 14 deletions(-) delete mode 100644 .buildkite/scripts/steps/test/test_group_env.sh diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh index 0efcbb7dbcda36..82c42af67f226b 100755 --- a/.buildkite/scripts/common/env.sh +++ b/.buildkite/scripts/common/env.sh @@ -101,3 +101,8 @@ export DISABLE_BOOTSTRAP_VALIDATION=true # Prevent Browserlist from logging on CI about outdated database versions export BROWSERSLIST_IGNORE_OLD_DATA=true + +# keys used to associate test group data in ci-stats with Jest execution order +export TEST_GROUP_TYPE_UNIT="Jest Unit Tests" +export TEST_GROUP_TYPE_INTEGRATION="Jest Integration Tests" +export TEST_GROUP_TYPE_FUNCTIONAL="Functional Tests" diff --git a/.buildkite/scripts/steps/on_merge_build_and_metrics.sh b/.buildkite/scripts/steps/on_merge_build_and_metrics.sh index de46ba58e9d527..fb05bb99b0c54b 100755 --- a/.buildkite/scripts/steps/on_merge_build_and_metrics.sh +++ b/.buildkite/scripts/steps/on_merge_build_and_metrics.sh @@ -7,6 +7,4 @@ set -euo pipefail .buildkite/scripts/build_kibana_plugins.sh .buildkite/scripts/post_build_kibana_plugins.sh .buildkite/scripts/post_build_kibana.sh - -source ".buildkite/scripts/steps/test/test_group_env.sh" .buildkite/scripts/saved_object_field_metrics.sh diff --git a/.buildkite/scripts/steps/test/ftr_configs.sh b/.buildkite/scripts/steps/test/ftr_configs.sh index 52a4b9572f5b64..244b108a269f8b 100755 --- a/.buildkite/scripts/steps/test/ftr_configs.sh +++ b/.buildkite/scripts/steps/test/ftr_configs.sh @@ -3,7 +3,6 @@ set -euo pipefail source .buildkite/scripts/steps/functional/common.sh -source .buildkite/scripts/steps/test/test_group_env.sh export JOB_NUM=$BUILDKITE_PARALLEL_JOB export JOB=ftr-configs-${JOB_NUM} diff --git a/.buildkite/scripts/steps/test/jest_parallel.sh b/.buildkite/scripts/steps/test/jest_parallel.sh index f7efc13b501bdf..71ecf7a853d4a0 100755 --- a/.buildkite/scripts/steps/test/jest_parallel.sh +++ b/.buildkite/scripts/steps/test/jest_parallel.sh @@ -2,8 +2,6 @@ set -euo pipefail -source .buildkite/scripts/steps/test/test_group_env.sh - export JOB=$BUILDKITE_PARALLEL_JOB # a jest failure will result in the script returning an exit code of 10 diff --git a/.buildkite/scripts/steps/test/pick_test_group_run_order.sh b/.buildkite/scripts/steps/test/pick_test_group_run_order.sh index 3bb09282efb41f..56308b73c6fd7e 100644 --- a/.buildkite/scripts/steps/test/pick_test_group_run_order.sh +++ b/.buildkite/scripts/steps/test/pick_test_group_run_order.sh @@ -3,7 +3,6 @@ set -euo pipefail source .buildkite/scripts/common/util.sh -source .buildkite/scripts/steps/test/test_group_env.sh echo '--- Pick Test Group Run Order' node "$(dirname "${0}")/pick_test_group_run_order.js" diff --git a/.buildkite/scripts/steps/test/test_group_env.sh b/.buildkite/scripts/steps/test/test_group_env.sh deleted file mode 100644 index 3a8c12fdb4a522..00000000000000 --- a/.buildkite/scripts/steps/test/test_group_env.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# keys used to associate test group data in ci-stats with Jest execution order -export TEST_GROUP_TYPE_UNIT="Jest Unit Tests" -export TEST_GROUP_TYPE_INTEGRATION="Jest Integration Tests" -export TEST_GROUP_TYPE_FUNCTIONAL="Functional Tests" From 25e3526358e0916bda6a3e1d47a411355d1ea5a4 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 5 May 2022 13:22:33 -0400 Subject: [PATCH 33/83] [CI] Use node_modules cache baked into agent image (#131555) --- .buildkite/scripts/bootstrap.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.buildkite/scripts/bootstrap.sh b/.buildkite/scripts/bootstrap.sh index 34461ca6db1941..b4ec748b863e2e 100755 --- a/.buildkite/scripts/bootstrap.sh +++ b/.buildkite/scripts/bootstrap.sh @@ -6,6 +6,15 @@ source .buildkite/scripts/common/util.sh source .buildkite/scripts/common/setup_bazel.sh echo "--- yarn install and bootstrap" + +# Use the node_modules that is baked into the agent image, if it exists, as a cache +# But only for agents not mounting the workspace on a local ssd or in memory +# It actually ends up being slower to move all of the tiny files between the disks vs extracting archives from the yarn cache +if [[ -d ~/.kibana/node_modules && "$(pwd)" != *"/local-ssd/"* && "$(pwd)" != "/dev/shm"* ]]; then + echo "Using ~/.kibana/node_modules as a starting point" + mv ~/.kibana/node_modules ./ +fi + if ! yarn kbn bootstrap; then echo "bootstrap failed, trying again in 15 seconds" sleep 15 From 267c674d42906f4c09cdfe9bafcfc8cb8ed45dd1 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 5 May 2022 13:26:34 -0400 Subject: [PATCH 34/83] [CI] Split check types into its own step, use spot for checks (#131096) --- .buildkite/pipelines/on_merge.yml | 13 +++++++++++-- .buildkite/pipelines/pull_request/base.yml | 13 +++++++++++-- .../scripts/steps/{checks => }/check_types.sh | 2 ++ .buildkite/scripts/steps/checks.sh | 1 - 4 files changed, 24 insertions(+), 5 deletions(-) rename .buildkite/scripts/steps/{checks => }/check_types.sh (84%) diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index 26b3afbf081a20..8bc4ff3dfb9053 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -78,10 +78,19 @@ steps: - command: .buildkite/scripts/steps/checks.sh label: 'Checks' + agents: + queue: n2-2-spot + timeout_in_minutes: 60 + retry: + automatic: + - exit_status: '-1' + limit: 3 + + - command: .buildkite/scripts/steps/check_types.sh + label: 'Check Types' agents: queue: c2-8 - key: checks - timeout_in_minutes: 120 + timeout_in_minutes: 60 - command: .buildkite/scripts/steps/storybooks/build_and_upload.sh label: 'Build Storybooks' diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index dc771e53d9d75f..8a43bb19fff578 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -40,10 +40,19 @@ steps: - command: .buildkite/scripts/steps/checks.sh label: 'Checks' + agents: + queue: n2-2-spot + timeout_in_minutes: 60 + retry: + automatic: + - exit_status: '-1' + limit: 3 + + - command: .buildkite/scripts/steps/check_types.sh + label: 'Check Types' agents: queue: c2-8 - key: checks - timeout_in_minutes: 120 + timeout_in_minutes: 60 - command: .buildkite/scripts/steps/storybooks/build_and_upload.sh label: 'Build Storybooks' diff --git a/.buildkite/scripts/steps/checks/check_types.sh b/.buildkite/scripts/steps/check_types.sh similarity index 84% rename from .buildkite/scripts/steps/checks/check_types.sh rename to .buildkite/scripts/steps/check_types.sh index 3b649a73e80608..eb7a41d74e8d85 100755 --- a/.buildkite/scripts/steps/checks/check_types.sh +++ b/.buildkite/scripts/steps/check_types.sh @@ -4,6 +4,8 @@ set -euo pipefail source .buildkite/scripts/common/util.sh +.buildkite/scripts/bootstrap.sh + echo --- Check Types checks-reporter-with-killswitch "Check Types" \ node scripts/type_check diff --git a/.buildkite/scripts/steps/checks.sh b/.buildkite/scripts/steps/checks.sh index cae019150b626e..024037a8a4bb96 100755 --- a/.buildkite/scripts/steps/checks.sh +++ b/.buildkite/scripts/steps/checks.sh @@ -13,7 +13,6 @@ export DISABLE_BOOTSTRAP_VALIDATION=false .buildkite/scripts/steps/checks/doc_api_changes.sh .buildkite/scripts/steps/checks/kbn_pm_dist.sh .buildkite/scripts/steps/checks/plugin_list_docs.sh -.buildkite/scripts/steps/checks/check_types.sh .buildkite/scripts/steps/checks/bundle_limits.sh .buildkite/scripts/steps/checks/i18n.sh .buildkite/scripts/steps/checks/file_casing.sh From af8810fedeeb681213ec32902d990407f3aeedd4 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 5 May 2022 11:46:50 -0700 Subject: [PATCH 35/83] [ci/test-groups] define scripts externally (#131645) --- .buildkite/package-lock.json | 12 ++++++------ .buildkite/package.json | 2 +- .buildkite/pipelines/es_snapshots/verify.yml | 3 +++ .buildkite/pipelines/on_merge.yml | 4 ++++ .buildkite/pipelines/pull_request/base.yml | 4 ++++ 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.buildkite/package-lock.json b/.buildkite/package-lock.json index 734db1a6ef234b..04e3c73fdd2f53 100644 --- a/.buildkite/package-lock.json +++ b/.buildkite/package-lock.json @@ -8,7 +8,7 @@ "name": "kibana-buildkite", "version": "1.0.0", "dependencies": { - "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#f5381bea52e0a71f50a6919cb6357ff3262cf2d6" + "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#ef419d4f761dd256cb59bfab9b59f8b91029eb40" } }, "node_modules/@nodelib/fs.scandir": { @@ -355,8 +355,8 @@ }, "node_modules/kibana-buildkite-library": { "version": "1.0.0", - "resolved": "git+https://git@github.com/elastic/kibana-buildkite-library.git#f5381bea52e0a71f50a6919cb6357ff3262cf2d6", - "integrity": "sha512-L1JP2NvXR7mhKn9JBwROPgTEV4vDr5HWwZtkdvxtHjZ/MeOnJYFSDqB4JUY/gXTz6v3CO3eUm3GQ0BP/kewoqQ==", + "resolved": "git+https://git@github.com/elastic/kibana-buildkite-library.git#ef419d4f761dd256cb59bfab9b59f8b91029eb40", + "integrity": "sha512-ou/Db/DAhyMeD0uQeLpJ/VfUCg0PGPssIYsr4gJZSTZoqxCDrMtE4nhtH8sXErHq0TaugdqergUhyhVHNBSJiA==", "license": "MIT", "dependencies": { "@octokit/rest": "^18.10.0", @@ -801,9 +801,9 @@ } }, "kibana-buildkite-library": { - "version": "git+https://git@github.com/elastic/kibana-buildkite-library.git#f5381bea52e0a71f50a6919cb6357ff3262cf2d6", - "integrity": "sha512-L1JP2NvXR7mhKn9JBwROPgTEV4vDr5HWwZtkdvxtHjZ/MeOnJYFSDqB4JUY/gXTz6v3CO3eUm3GQ0BP/kewoqQ==", - "from": "kibana-buildkite-library@git+https://git@github.com/elastic/kibana-buildkite-library#f5381bea52e0a71f50a6919cb6357ff3262cf2d6", + "version": "git+https://git@github.com/elastic/kibana-buildkite-library.git#ef419d4f761dd256cb59bfab9b59f8b91029eb40", + "integrity": "sha512-ou/Db/DAhyMeD0uQeLpJ/VfUCg0PGPssIYsr4gJZSTZoqxCDrMtE4nhtH8sXErHq0TaugdqergUhyhVHNBSJiA==", + "from": "kibana-buildkite-library@git+https://git@github.com/elastic/kibana-buildkite-library#ef419d4f761dd256cb59bfab9b59f8b91029eb40", "requires": { "@octokit/rest": "^18.10.0", "axios": "^0.21.4", diff --git a/.buildkite/package.json b/.buildkite/package.json index 079f200a4cbc92..daff8bd5db7814 100644 --- a/.buildkite/package.json +++ b/.buildkite/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "private": true, "dependencies": { - "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#f5381bea52e0a71f50a6919cb6357ff3262cf2d6" + "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#ef419d4f761dd256cb59bfab9b59f8b91029eb40" } } diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index 2ebb1d9a3b56bd..bdbce0745c1f88 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -32,6 +32,9 @@ steps: agents: queue: kibana-default env: + JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/test/jest.sh' + JEST_INTEGRATION_SCRIPT: '.buildkite/scripts/steps/test/jest_integration.sh' + FTR_CONFIGS_SCRIPT: '.buildkite/scripts/steps/test/ftr_configs.sh' LIMIT_CONFIG_TYPE: integration,functional retry: automatic: diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index 8bc4ff3dfb9053..586199f0829257 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -53,6 +53,10 @@ steps: label: 'Pick Test Group Run Order' agents: queue: kibana-default + env: + JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/test/jest.sh' + JEST_INTEGRATION_SCRIPT: '.buildkite/scripts/steps/test/jest_integration.sh' + FTR_CONFIGS_SCRIPT: '.buildkite/scripts/steps/test/ftr_configs.sh' retry: automatic: - exit_status: '*' diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 8a43bb19fff578..73c03e0382a0fd 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -19,6 +19,10 @@ steps: label: 'Pick Test Group Run Order' agents: queue: kibana-default + env: + JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/test/jest.sh' + JEST_INTEGRATION_SCRIPT: '.buildkite/scripts/steps/test/jest_integration.sh' + FTR_CONFIGS_SCRIPT: '.buildkite/scripts/steps/test/ftr_configs.sh' retry: automatic: - exit_status: '*' From f7a1739dc0c0a305be3f95b804b5d601fdf7bbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 5 May 2022 21:20:51 +0200 Subject: [PATCH 36/83] Use `target_web` to ensure browser compatibility (#130874) --- packages/analytics/client/BUILD.bazel | 9 ++- packages/analytics/client/package.json | 1 + .../analytics/shippers/fullstory/BUILD.bazel | 9 ++- .../analytics/shippers/fullstory/package.json | 1 + packages/kbn-ace/BUILD.bazel | 15 ++++- packages/kbn-ace/package.json | 1 + packages/kbn-doc-links/BUILD.bazel | 9 ++- packages/kbn-doc-links/package.json | 1 + packages/kbn-field-types/BUILD.bazel | 17 ++++-- packages/kbn-field-types/package.json | 1 + packages/kbn-interpreter/BUILD.bazel | 9 ++- packages/kbn-interpreter/package.json | 1 + packages/kbn-io-ts-utils/BUILD.bazel | 9 ++- packages/kbn-io-ts-utils/package.json | 1 + packages/kbn-mapbox-gl/BUILD.bazel | 9 ++- packages/kbn-mapbox-gl/package.json | 1 + ..._babel_runtime_helpers_in_entry_bundles.ts | 0 ...libs_browser_polyfills_in_entry_bundles.ts | 0 .../find_target_node_imports.ts | 56 +++++++++++++++++++ .../index.ts | 1 + .../parse_stats.ts | 0 packages/kbn-optimizer/src/index.ts | 2 +- .../src/optimizer/optimizer_built_paths.ts | 5 +- packages/kbn-rule-data-utils/BUILD.bazel | 9 ++- packages/kbn-rule-data-utils/package.json | 1 + .../kbn-securitysolution-rules/BUILD.bazel | 9 ++- .../kbn-securitysolution-rules/package.json | 1 + .../kbn-securitysolution-utils/BUILD.bazel | 9 ++- .../kbn-securitysolution-utils/package.json | 1 + .../kbn-server-route-repository/BUILD.bazel | 13 ++++- .../kbn-server-route-repository/README.md | 8 +++ .../kbn-server-route-repository/package.json | 1 + .../src/web_index.ts | 21 +++++++ .../find_target_node_imports_in_bundles.js | 10 ++++ .../services/rest/create_call_apm_api.ts | 12 +--- .../common/constants.ts | 2 + .../common/schemas/csp_rule.ts | 2 - .../use_csp_benchmark_integrations.ts | 2 +- .../public/pages/rules/rules_container.tsx | 2 +- .../public/pages/rules/use_csp_rules.ts | 7 ++- .../fleet_integration/fleet_integration.ts | 7 ++- .../server/routes/benchmarks/benchmarks.ts | 3 +- .../update_rules_configuration.test.ts | 4 +- .../update_rules_configuration.ts | 9 ++- .../server/saved_objects/csp_rule_type.ts | 7 +-- .../services/call_observability_api/index.ts | 8 +-- .../services/rest/create_call_apm_api.ts | 12 +--- 47 files changed, 256 insertions(+), 62 deletions(-) rename packages/kbn-optimizer/src/{babel_runtime_helpers => audit_bundle_dependencies}/find_babel_runtime_helpers_in_entry_bundles.ts (100%) rename packages/kbn-optimizer/src/{babel_runtime_helpers => audit_bundle_dependencies}/find_node_libs_browser_polyfills_in_entry_bundles.ts (100%) create mode 100644 packages/kbn-optimizer/src/audit_bundle_dependencies/find_target_node_imports.ts rename packages/kbn-optimizer/src/{babel_runtime_helpers => audit_bundle_dependencies}/index.ts (91%) rename packages/kbn-optimizer/src/{babel_runtime_helpers => audit_bundle_dependencies}/parse_stats.ts (100%) create mode 100644 packages/kbn-server-route-repository/src/web_index.ts create mode 100644 scripts/find_target_node_imports_in_bundles.js diff --git a/packages/analytics/client/BUILD.bazel b/packages/analytics/client/BUILD.bazel index af79d33ccc6923..0b6a811adc22c2 100644 --- a/packages/analytics/client/BUILD.bazel +++ b/packages/analytics/client/BUILD.bazel @@ -62,6 +62,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -86,7 +93,7 @@ ts_project( js_library( name = PKG_DIRNAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/analytics/client/package.json b/packages/analytics/client/package.json index 82ab42bc22a31d..9f2c648183a412 100644 --- a/packages/analytics/client/package.json +++ b/packages/analytics/client/package.json @@ -2,6 +2,7 @@ "name": "@kbn/analytics-client", "private": true, "version": "1.0.0", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "license": "SSPL-1.0 OR Elastic License 2.0" } diff --git a/packages/analytics/shippers/fullstory/BUILD.bazel b/packages/analytics/shippers/fullstory/BUILD.bazel index 2825e3fd733eaa..c7da842e3cd936 100644 --- a/packages/analytics/shippers/fullstory/BUILD.bazel +++ b/packages/analytics/shippers/fullstory/BUILD.bazel @@ -62,6 +62,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -86,7 +93,7 @@ ts_project( js_library( name = PKG_DIRNAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/analytics/shippers/fullstory/package.json b/packages/analytics/shippers/fullstory/package.json index 5d8fa7b7df976c..ab5ac56b356415 100644 --- a/packages/analytics/shippers/fullstory/package.json +++ b/packages/analytics/shippers/fullstory/package.json @@ -2,6 +2,7 @@ "name": "@kbn/analytics-shippers-fullstory", "private": true, "version": "1.0.0", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "license": "SSPL-1.0 OR Elastic License 2.0" } diff --git a/packages/kbn-ace/BUILD.bazel b/packages/kbn-ace/BUILD.bazel index 630a636e99b9b7..8db4f8c4a4a217 100644 --- a/packages/kbn-ace/BUILD.bazel +++ b/packages/kbn-ace/BUILD.bazel @@ -52,6 +52,19 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + additional_args = [ + "--copy-files", + "--ignore", + "**/*/src/ace/modes/x_json/worker/x_json.ace.worker.js", + "--quiet" + ], + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -77,7 +90,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-ace/package.json b/packages/kbn-ace/package.json index f5775c3b34596a..ce8e83e0686efb 100644 --- a/packages/kbn-ace/package.json +++ b/packages/kbn-ace/package.json @@ -2,6 +2,7 @@ "name": "@kbn/ace", "version": "1.0.0", "private": true, + "browser": "./target_web/index.js", "main": "./target_node/index.js", "license": "SSPL-1.0 OR Elastic License 2.0" } diff --git a/packages/kbn-doc-links/BUILD.bazel b/packages/kbn-doc-links/BUILD.bazel index 13b68935c43261..25f376afba96b7 100644 --- a/packages/kbn-doc-links/BUILD.bazel +++ b/packages/kbn-doc-links/BUILD.bazel @@ -45,6 +45,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -69,7 +76,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-doc-links/package.json b/packages/kbn-doc-links/package.json index 8ddbe564c356cf..e4212ed989d9a1 100644 --- a/packages/kbn-doc-links/package.json +++ b/packages/kbn-doc-links/package.json @@ -1,5 +1,6 @@ { "name": "@kbn/doc-links", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", diff --git a/packages/kbn-field-types/BUILD.bazel b/packages/kbn-field-types/BUILD.bazel index 77a4acaedb2359..4259b9c14a6ab0 100644 --- a/packages/kbn-field-types/BUILD.bazel +++ b/packages/kbn-field-types/BUILD.bazel @@ -43,10 +43,17 @@ TYPES_DEPS = [ ] jsts_transpiler( - name = "target_node", - srcs = SRCS, - build_pkg_name = package_name(), - ) + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) ts_config( name = "tsconfig", @@ -72,7 +79,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-field-types/package.json b/packages/kbn-field-types/package.json index 4e6276e508c36f..14b842526d9bc1 100644 --- a/packages/kbn-field-types/package.json +++ b/packages/kbn-field-types/package.json @@ -3,5 +3,6 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", + "browser": "./target_web/index.js", "main": "./target_node/index.js" } diff --git a/packages/kbn-interpreter/BUILD.bazel b/packages/kbn-interpreter/BUILD.bazel index 469f16296230f0..f1c066d4cbd847 100644 --- a/packages/kbn-interpreter/BUILD.bazel +++ b/packages/kbn-interpreter/BUILD.bazel @@ -42,6 +42,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + peggy( name = "grammar", data = [ @@ -82,7 +89,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES + [":grammar"], - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index 1480e7e808370d..ca1f35c02874b1 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -1,6 +1,7 @@ { "name": "@kbn/interpreter", "author": "App Services", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", diff --git a/packages/kbn-io-ts-utils/BUILD.bazel b/packages/kbn-io-ts-utils/BUILD.bazel index aa0116b81efe68..15917e75a52853 100644 --- a/packages/kbn-io-ts-utils/BUILD.bazel +++ b/packages/kbn-io-ts-utils/BUILD.bazel @@ -49,6 +49,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -73,7 +80,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-io-ts-utils/package.json b/packages/kbn-io-ts-utils/package.json index 2dc3532e05d966..806f3c46cf3374 100644 --- a/packages/kbn-io-ts-utils/package.json +++ b/packages/kbn-io-ts-utils/package.json @@ -1,5 +1,6 @@ { "name": "@kbn/io-ts-utils", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", diff --git a/packages/kbn-mapbox-gl/BUILD.bazel b/packages/kbn-mapbox-gl/BUILD.bazel index 89cbeb4c431ae0..d4cebd52152f06 100644 --- a/packages/kbn-mapbox-gl/BUILD.bazel +++ b/packages/kbn-mapbox-gl/BUILD.bazel @@ -45,6 +45,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -69,7 +76,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-mapbox-gl/package.json b/packages/kbn-mapbox-gl/package.json index 493fbb881c80cd..f0a5c7eabdfcb8 100644 --- a/packages/kbn-mapbox-gl/package.json +++ b/packages/kbn-mapbox-gl/package.json @@ -3,5 +3,6 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", + "browser": "./target_web/index.js", "main": "./target_node/index.js" } diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_babel_runtime_helpers_in_entry_bundles.ts similarity index 100% rename from packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts rename to packages/kbn-optimizer/src/audit_bundle_dependencies/find_babel_runtime_helpers_in_entry_bundles.ts diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/find_node_libs_browser_polyfills_in_entry_bundles.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_node_libs_browser_polyfills_in_entry_bundles.ts similarity index 100% rename from packages/kbn-optimizer/src/babel_runtime_helpers/find_node_libs_browser_polyfills_in_entry_bundles.ts rename to packages/kbn-optimizer/src/audit_bundle_dependencies/find_node_libs_browser_polyfills_in_entry_bundles.ts diff --git a/packages/kbn-optimizer/src/audit_bundle_dependencies/find_target_node_imports.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_target_node_imports.ts new file mode 100644 index 00000000000000..dd1309f838e41f --- /dev/null +++ b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_target_node_imports.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; + +import { run } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; + +import { OptimizerConfig } from '../optimizer'; +import { parseStats } from './parse_stats'; + +/** + * Analyzes the bundle dependencies to find any imports using the `@kbn//target_node` build target. + * + * We should aim for those packages to be imported using the `@kbn//target_web` build because it's optimized + * for browser compatibility. + * + * This utility also helps identify when code that should only run in the server is leaked into the browser. + */ +export async function runFindTargetNodeImportsCli() { + run(async ({ log }) => { + const config = OptimizerConfig.create({ + includeCoreBundle: true, + repoRoot: REPO_ROOT, + }); + + const paths = config.bundles.map((b) => Path.resolve(b.outputDir, 'stats.json')); + + log.info('analyzing', paths.length, 'stats files'); + log.verbose(paths); + + const imports = new Set(); + for (const path of paths) { + const stats = parseStats(path); + + for (const module of stats.modules) { + if (module.name.includes('/target_node/')) { + const [, cleanName] = /\/((?:kbn-|@kbn\/).+)\/target_node/.exec(module.name) ?? []; + imports.add(cleanName || module.name); + } + } + } + + log.success('found', imports.size, '@kbn/*/target_node imports in entry bundles and chunks'); + log.write( + Array.from(imports, (i) => `'${i}',`) + .sort() + .join('\n') + ); + }); +} diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/index.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/index.ts similarity index 91% rename from packages/kbn-optimizer/src/babel_runtime_helpers/index.ts rename to packages/kbn-optimizer/src/audit_bundle_dependencies/index.ts index 3a7987f867bc50..e6059c4c2c9b53 100644 --- a/packages/kbn-optimizer/src/babel_runtime_helpers/index.ts +++ b/packages/kbn-optimizer/src/audit_bundle_dependencies/index.ts @@ -8,3 +8,4 @@ export * from './find_babel_runtime_helpers_in_entry_bundles'; export * from './find_node_libs_browser_polyfills_in_entry_bundles'; +export * from './find_target_node_imports'; diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/parse_stats.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/parse_stats.ts similarity index 100% rename from packages/kbn-optimizer/src/babel_runtime_helpers/parse_stats.ts rename to packages/kbn-optimizer/src/audit_bundle_dependencies/parse_stats.ts diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index d759a4aa02455d..48e77c86289050 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -14,4 +14,4 @@ export * from './node'; export * from './limits'; export * from './cli'; export * from './report_optimizer_timings'; -export * from './babel_runtime_helpers'; +export * from './audit_bundle_dependencies'; diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_built_paths.ts b/packages/kbn-optimizer/src/optimizer/optimizer_built_paths.ts index 294c3e835a3bd0..8421c0846d52a4 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_built_paths.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_built_paths.ts @@ -14,7 +14,10 @@ import { ascending } from '../common'; export async function getOptimizerBuiltPaths() { return ( await globby( - ['**/*', '!**/{__fixtures__,__snapshots__,integration_tests,babel_runtime_helpers,node}/**'], + [ + '**/*', + '!**/{__fixtures__,__snapshots__,integration_tests,audit_bundle_dependencies,node}/**', + ], { cwd: Path.resolve(__dirname, '../'), absolute: true, diff --git a/packages/kbn-rule-data-utils/BUILD.bazel b/packages/kbn-rule-data-utils/BUILD.bazel index 6477b558db9cb3..d4dc6577c02b46 100644 --- a/packages/kbn-rule-data-utils/BUILD.bazel +++ b/packages/kbn-rule-data-utils/BUILD.bazel @@ -44,6 +44,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -69,7 +76,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-rule-data-utils/package.json b/packages/kbn-rule-data-utils/package.json index 9372d7e70a8d19..d11f65e294a48a 100644 --- a/packages/kbn-rule-data-utils/package.json +++ b/packages/kbn-rule-data-utils/package.json @@ -1,5 +1,6 @@ { "name": "@kbn/rule-data-utils", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", diff --git a/packages/kbn-securitysolution-rules/BUILD.bazel b/packages/kbn-securitysolution-rules/BUILD.bazel index 80a27a426fbb26..31b8fa8679312b 100644 --- a/packages/kbn-securitysolution-rules/BUILD.bazel +++ b/packages/kbn-securitysolution-rules/BUILD.bazel @@ -47,6 +47,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -71,7 +78,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-rules/package.json b/packages/kbn-securitysolution-rules/package.json index 4962576450f592..da061b244e7a08 100644 --- a/packages/kbn-securitysolution-rules/package.json +++ b/packages/kbn-securitysolution-rules/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "security solution rule utilities to use across plugins", "license": "SSPL-1.0 OR Elastic License 2.0", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "private": true } diff --git a/packages/kbn-securitysolution-utils/BUILD.bazel b/packages/kbn-securitysolution-utils/BUILD.bazel index 70ecc2712d4af7..1842e5d1a523f2 100644 --- a/packages/kbn-securitysolution-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-utils/BUILD.bazel @@ -47,6 +47,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -71,7 +78,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-utils/package.json b/packages/kbn-securitysolution-utils/package.json index 8f347972f83169..e43d2570f730fb 100644 --- a/packages/kbn-securitysolution-utils/package.json +++ b/packages/kbn-securitysolution-utils/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "security solution utilities to use across plugins such lists, security_solution, cases, etc...", "license": "SSPL-1.0 OR Elastic License 2.0", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "private": true } diff --git a/packages/kbn-server-route-repository/BUILD.bazel b/packages/kbn-server-route-repository/BUILD.bazel index 06c09260e2fa60..b635362ae15219 100644 --- a/packages/kbn-server-route-repository/BUILD.bazel +++ b/packages/kbn-server-route-repository/BUILD.bazel @@ -52,6 +52,17 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = [ + "src/web_index.ts", + "src/format_request.ts", + "src/parse_endpoint.ts", + ], + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -76,7 +87,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-server-route-repository/README.md b/packages/kbn-server-route-repository/README.md index e22205540ef317..13d7972028cb77 100644 --- a/packages/kbn-server-route-repository/README.md +++ b/packages/kbn-server-route-repository/README.md @@ -5,3 +5,11 @@ Utility functions for creating a typed server route repository, and a typed clie ## Usage TBD + +## Server vs. Browser entry points + +This package exposes utils that can be used on both: the server and the browser. +However, importing the package might bring in server-only code, affecting the bundle size. +To avoid this, the package exposes 2 entry points: [`index.js`](./src/index.ts) and [`web_index.js`](./src/web_index.ts). + +When adding utilities to this package, please make sure to update the entry points accordingly and the [BUILD.bazel](./BUILD.bazel)'s `target_web` target build to include all the necessary files. diff --git a/packages/kbn-server-route-repository/package.json b/packages/kbn-server-route-repository/package.json index 32e59c896db009..1491f24c54dc18 100644 --- a/packages/kbn-server-route-repository/package.json +++ b/packages/kbn-server-route-repository/package.json @@ -1,5 +1,6 @@ { "name": "@kbn/server-route-repository", + "browser": "./target_web/web_index.js", "main": "./target_node/index.js", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", diff --git a/packages/kbn-server-route-repository/src/web_index.ts b/packages/kbn-server-route-repository/src/web_index.ts new file mode 100644 index 00000000000000..3ceeed55236bdc --- /dev/null +++ b/packages/kbn-server-route-repository/src/web_index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { formatRequest } from './format_request'; +export { parseEndpoint } from './parse_endpoint'; +export type { + RouteRepositoryClient, + ReturnOf, + EndpointOf, + ClientRequestParamsOf, + DecodedRequestParamsOf, + ServerRouteRepository, + ServerRoute, + RouteParamsRT, + RouteState, +} from './typings'; diff --git a/scripts/find_target_node_imports_in_bundles.js b/scripts/find_target_node_imports_in_bundles.js new file mode 100644 index 00000000000000..eae3b94efeabaf --- /dev/null +++ b/scripts/find_target_node_imports_in_bundles.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env/no_transpilation'); +require('@kbn/optimizer').runFindTargetNodeImportsCli(); diff --git a/x-pack/plugins/apm/public/services/rest/create_call_apm_api.ts b/x-pack/plugins/apm/public/services/rest/create_call_apm_api.ts index e9450ec67cc889..4d6ca03ba6a29c 100644 --- a/x-pack/plugins/apm/public/services/rest/create_call_apm_api.ts +++ b/x-pack/plugins/apm/public/services/rest/create_call_apm_api.ts @@ -8,16 +8,11 @@ import { CoreSetup, CoreStart } from '@kbn/core/public'; import type { ClientRequestParamsOf, - formatRequest as formatRequestType, ReturnOf, RouteRepositoryClient, ServerRouteRepository, } from '@kbn/server-route-repository'; -// @ts-expect-error cannot find module or correspondent type declarations -// The code and types are at separated folders on @kbn/server-route-repository -// so in order to do targeted imports they must me imported separately, and -// an error is expected here -import { formatRequest } from '@kbn/server-route-repository/target_node/format_request'; +import { formatRequest } from '@kbn/server-route-repository'; import { InspectResponse } from '@kbn/observability-plugin/typings/common'; import { FetchOptions } from '../../../common/fetch_options'; import { CallApi, callApi } from './call_api'; @@ -73,10 +68,7 @@ export function createCallApmApi(core: CoreStart | CoreSetup) { params?: Partial>; }; - const { method, pathname } = formatRequest( - endpoint, - params?.path - ) as ReturnType; + const { method, pathname } = formatRequest(endpoint, params?.path); return callApi(core, { ...options, diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 1d35d6439bead4..b2edd268c84850 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -33,3 +33,5 @@ export const INTERNAL_FEATURE_FLAGS = { showRisksMock: false, showFindingsGroupBy: false, } as const; + +export const cspRuleAssetSavedObjectType = 'csp_rule'; diff --git a/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule.ts b/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule.ts index a6aaa26e7a1a07..cdefc461cd9529 100644 --- a/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule.ts +++ b/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule.ts @@ -6,8 +6,6 @@ */ import { schema as rt, TypeOf } from '@kbn/config-schema'; -export const cspRuleAssetSavedObjectType = 'csp_rule'; - // TODO: needs to be shared with cloudbeat export const cspRuleSchema = rt.object({ id: rt.string(), diff --git a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/use_csp_benchmark_integrations.ts b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/use_csp_benchmark_integrations.ts index 53e65d05ce08d5..3bef982deb3a93 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/use_csp_benchmark_integrations.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/use_csp_benchmark_integrations.ts @@ -8,7 +8,7 @@ import { useQuery } from 'react-query'; import type { ListResult } from '@kbn/fleet-plugin/common'; import { BENCHMARKS_ROUTE_PATH } from '../../../common/constants'; -import { BenchmarksQuerySchema } from '../../../common/schemas/benchmark'; +import type { BenchmarksQuerySchema } from '../../../common/schemas/benchmark'; import { useKibana } from '../../common/hooks/use_kibana'; import type { Benchmark } from '../../../common/types'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx index e7bc2d5c1b3444..0db7392e0b9331 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx @@ -16,7 +16,7 @@ import { import { useParams } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; import { pagePathGetters } from '@kbn/fleet-plugin/public'; -import { cspRuleAssetSavedObjectType } from '../../../common/schemas/csp_rule'; +import { cspRuleAssetSavedObjectType } from '../../../common/constants'; import { extractErrorMessage, isNonNullable } from '../../../common/utils/helpers'; import { RulesTable } from './rules_table'; import { RulesBottomBar } from './rules_bottom_bar'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules.ts b/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules.ts index 2ce088cd5c29b9..8c4012f6c8b456 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules.ts @@ -7,8 +7,11 @@ import { useQuery, useMutation, useQueryClient } from 'react-query'; import { FunctionKeys } from 'utility-types'; import type { SavedObjectsFindOptions, SimpleSavedObject } from '@kbn/core/public'; -import { UPDATE_RULES_CONFIG_ROUTE_PATH } from '../../../common/constants'; -import { cspRuleAssetSavedObjectType, type CspRuleSchema } from '../../../common/schemas/csp_rule'; +import { + UPDATE_RULES_CONFIG_ROUTE_PATH, + cspRuleAssetSavedObjectType, +} from '../../../common/constants'; +import type { CspRuleSchema } from '../../../common/schemas/csp_rule'; import { useKibana } from '../../common/hooks/use_kibana'; import { UPDATE_FAILED } from './translations'; diff --git a/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts index 347730b5896727..9404985ce077f4 100644 --- a/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts @@ -17,8 +17,11 @@ import { cloudSecurityPostureRuleTemplateSavedObjectType, CloudSecurityPostureRuleTemplateSchema, } from '../../common/schemas/csp_rule_template'; -import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../../common/constants'; -import { CspRuleSchema, cspRuleAssetSavedObjectType } from '../../common/schemas/csp_rule'; +import { + CLOUD_SECURITY_POSTURE_PACKAGE_NAME, + cspRuleAssetSavedObjectType, +} from '../../common/constants'; +import { CspRuleSchema } from '../../common/schemas/csp_rule'; type ArrayElement = ArrayType extends ReadonlyArray< infer ElementType diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts index 6121bbe363e88f..d19a86d5b5c27f 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts @@ -19,10 +19,11 @@ import type { AgentPolicy, ListResult, } from '@kbn/fleet-plugin/common'; -import { cspRuleAssetSavedObjectType, CspRuleSchema } from '../../../common/schemas/csp_rule'; +import { CspRuleSchema } from '../../../common/schemas/csp_rule'; import { BENCHMARKS_ROUTE_PATH, CLOUD_SECURITY_POSTURE_PACKAGE_NAME, + cspRuleAssetSavedObjectType, } from '../../../common/constants'; import { BENCHMARK_PACKAGE_POLICY_PREFIX, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts index 270466d2e3adf1..27dcd3cee67035 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts @@ -26,7 +26,9 @@ import { CspAppContext } from '../../plugin'; import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; import { createPackagePolicyServiceMock } from '@kbn/fleet-plugin/server/mocks'; -import { cspRuleAssetSavedObjectType, CspRuleSchema } from '../../../common/schemas/csp_rule'; +import { cspRuleAssetSavedObjectType } from '../../../common/constants'; +import { CspRuleSchema } from '../../../common/schemas/csp_rule'; + import { ElasticsearchClient, KibanaRequest, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts index da84747f313547..21587394d51e87 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts @@ -20,9 +20,12 @@ import { PackagePolicy, PackagePolicyConfigRecord } from '@kbn/fleet-plugin/comm import { PackagePolicyServiceInterface } from '@kbn/fleet-plugin/server'; import { CspAppContext } from '../../plugin'; import { CspRulesConfigSchema } from '../../../common/schemas/csp_configuration'; -import { CspRuleSchema, cspRuleAssetSavedObjectType } from '../../../common/schemas/csp_rule'; -import { UPDATE_RULES_CONFIG_ROUTE_PATH } from '../../../common/constants'; -import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../../../common/constants'; +import { CspRuleSchema } from '../../../common/schemas/csp_rule'; +import { + CLOUD_SECURITY_POSTURE_PACKAGE_NAME, + UPDATE_RULES_CONFIG_ROUTE_PATH, + cspRuleAssetSavedObjectType, +} from '../../../common/constants'; import { CspRouter } from '../../types'; export const getPackagePolicy = async ( diff --git a/x-pack/plugins/cloud_security_posture/server/saved_objects/csp_rule_type.ts b/x-pack/plugins/cloud_security_posture/server/saved_objects/csp_rule_type.ts index f28bf100e21687..3afa68fdea2285 100644 --- a/x-pack/plugins/cloud_security_posture/server/saved_objects/csp_rule_type.ts +++ b/x-pack/plugins/cloud_security_posture/server/saved_objects/csp_rule_type.ts @@ -7,11 +7,8 @@ import { i18n } from '@kbn/i18n'; import type { SavedObjectsType, SavedObjectsValidationMap } from '@kbn/core/server'; -import { - type CspRuleSchema, - cspRuleSchema, - cspRuleAssetSavedObjectType, -} from '../../common/schemas/csp_rule'; +import { cspRuleAssetSavedObjectType } from '../../common/constants'; +import { type CspRuleSchema, cspRuleSchema } from '../../common/schemas/csp_rule'; const validationMap: SavedObjectsValidationMap = { '1.0.0': cspRuleSchema, diff --git a/x-pack/plugins/observability/public/services/call_observability_api/index.ts b/x-pack/plugins/observability/public/services/call_observability_api/index.ts index 881ea80a4de47a..f76f9f5cd7e070 100644 --- a/x-pack/plugins/observability/public/services/call_observability_api/index.ts +++ b/x-pack/plugins/observability/public/services/call_observability_api/index.ts @@ -5,9 +5,7 @@ * 2.0. */ -// @ts-expect-error -import { formatRequest } from '@kbn/server-route-repository/target_node/format_request'; -import type { formatRequest as formatRequestType } from '@kbn/server-route-repository/target_types/format_request'; +import { formatRequest } from '@kbn/server-route-repository'; import type { HttpSetup } from '@kbn/core/public'; import type { AbstractObservabilityClient, ObservabilityClient } from './types'; @@ -19,9 +17,7 @@ export function createCallObservabilityApi(http: HttpSetup) { const client: AbstractObservabilityClient = (endpoint, options) => { const { params: { path, body, query } = {}, ...rest } = options; - const { method, pathname } = formatRequest(endpoint, path) as ReturnType< - typeof formatRequestType - >; + const { method, pathname } = formatRequest(endpoint, path); return http[method](pathname, { ...rest, diff --git a/x-pack/plugins/ux/public/services/rest/create_call_apm_api.ts b/x-pack/plugins/ux/public/services/rest/create_call_apm_api.ts index eddd5fe07694e5..f7e9b6799e17d8 100644 --- a/x-pack/plugins/ux/public/services/rest/create_call_apm_api.ts +++ b/x-pack/plugins/ux/public/services/rest/create_call_apm_api.ts @@ -8,16 +8,11 @@ import { CoreSetup, CoreStart } from '@kbn/core/public'; import type { ClientRequestParamsOf, - formatRequest as formatRequestType, ReturnOf, RouteRepositoryClient, ServerRouteRepository, } from '@kbn/server-route-repository'; -// @ts-expect-error cannot find module or correspondent type declarations -// The code and types are at separated folders on @kbn/server-route-repository -// so in order to do targeted imports they must me imported separately, and -// an error is expected here -import { formatRequest } from '@kbn/server-route-repository/target_node/format_request'; +import { formatRequest } from '@kbn/server-route-repository'; import type { APMServerRouteRepository, APIEndpoint, @@ -73,10 +68,7 @@ export function createCallApmApi(core: CoreStart | CoreSetup) { params?: Partial>; }; - const { method, pathname } = formatRequest( - endpoint, - params?.path - ) as ReturnType; + const { method, pathname } = formatRequest(endpoint, params?.path); return callApi(core, { ...options, From 78b22417704b8bb3c18d0b1d4bb66ccbfa9571dc Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Thu, 5 May 2022 16:17:09 -0400 Subject: [PATCH 37/83] synthetics - fix test directory (#131655) --- .buildkite/ftr_configs.yml | 3 +++ x-pack/plugins/synthetics/scripts/e2e.js | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 26dd1b4bcfc3ca..40be42461c4930 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -34,7 +34,10 @@ disabled: - x-pack/test/functional_enterprise_search/with_host_configured.config.ts - x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts - x-pack/plugins/apm/ftr_e2e/ftr_config.ts + + # Elastic Synthetics configs - x-pack/plugins/synthetics/e2e/config.ts + - x-pack/plugins/synthetics/e2e/playwright_run.ts # Configs that exist but weren't running in CI when this file was introduced - test/visual_regression/config.ts diff --git a/x-pack/plugins/synthetics/scripts/e2e.js b/x-pack/plugins/synthetics/scripts/e2e.js index a329a6bf03c4b9..cfdd7b09f806e7 100644 --- a/x-pack/plugins/synthetics/scripts/e2e.js +++ b/x-pack/plugins/synthetics/scripts/e2e.js @@ -61,7 +61,7 @@ const config = './playwright_run.ts'; function executeRunner() { if (server) { childProcess.execSync( - `node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}'`, + `node ../../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}'`, { cwd: e2eDir, stdio: 'inherit', @@ -69,7 +69,7 @@ function executeRunner() { ); } else if (runner) { childProcess.execSync( - `node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}' --headless ${headless} --grep '${grep}'`, + `node ../../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}' --headless ${headless} --grep '${grep}'`, { cwd: e2eDir, stdio: 'inherit', @@ -77,7 +77,7 @@ function executeRunner() { ); } else { childProcess.execSync( - `node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}' --grep '${grep}'`, + `node ../../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}' --grep '${grep}'`, { cwd: e2eDir, stdio: 'inherit', From 635aee0c080b2273994958067dac48adb9db969b Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Thu, 5 May 2022 13:48:37 -0700 Subject: [PATCH 38/83] [Security Solution] Re-enable endpoint jest tests, delete flakey test (#124136) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pages/endpoint_hosts/view/index.test.tsx | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 39b1c9aecaf651..c773e0486620a7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -133,8 +133,7 @@ const timepickerRanges = [ jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/hooks/use_license'); -// FLAKY: https://github.com/elastic/kibana/issues/115489 -describe.skip('when on the endpoint list page', () => { +describe('when on the endpoint list page', () => { const docGenerator = new EndpointDocGenerator(); const { act, screen, fireEvent, waitFor } = reactTestingLibrary; @@ -296,17 +295,6 @@ describe.skip('when on the endpoint list page', () => { }); describe('when there is no selected host in the url', () => { - it('should not show the flyout', () => { - setEndpointListApiMockImplementation(coreStart.http, { - endpointsResults: [], - }); - - const renderResult = render(); - expect.assertions(1); - return renderResult.findByTestId('endpointDetailsFlyout').catch((e) => { - expect(e).not.toBeNull(); - }); - }); describe('when list data loads', () => { const generatedPolicyStatuses: Array< HostInfo['metadata']['Endpoint']['policy']['applied']['status'] From b7aa4b8e0d99c425c02ba2e431e21bf799d7b6a7 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Thu, 5 May 2022 13:51:35 -0700 Subject: [PATCH 39/83] unskipping the grok debugger a11y test - works as expected (#131564) * unskipping the test- works as expected * clean * added a comment * restored --- x-pack/test/accessibility/apps/grok_debugger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/accessibility/apps/grok_debugger.ts b/x-pack/test/accessibility/apps/grok_debugger.ts index 4f40696bb0eb6a..8ee9114c7da0a7 100644 --- a/x-pack/test/accessibility/apps/grok_debugger.ts +++ b/x-pack/test/accessibility/apps/grok_debugger.ts @@ -12,8 +12,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const grokDebugger = getService('grokDebugger'); - // this test is failing as there is a violation https://github.com/elastic/kibana/issues/62102 - describe.skip('Dev tools grok debugger Accessibility', () => { + // Fixes:https://github.com/elastic/kibana/issues/62102 + describe('Dev tools grok debugger', () => { before(async () => { await PageObjects.common.navigateToApp('grokDebugger'); await grokDebugger.assertExists(); From 3099433056440f64d440080148de699ddaa9f23d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 6 May 2022 00:38:42 +0100 Subject: [PATCH 40/83] chore(NA): adds support for bazel packages to live anywhere (#130833) * chore(NA): creates a simple location free package * chore(NA): creates two more simple location free packages * chore(NA): add support on build tasks to build packages anywhere * chore(NA): add support for xpack * chore(NA): logic for discover bazel packages only with BUILD.bazel and package.json * chore(NA): do not allow child projects to have dependencies declared * chore(NA): create package on xpack folder * chore(NA): exclude bazel packages inside xpack plugins from xpack build * fix(NA): build copy and failing jest tests for @kbn/pm * chore(NA): exclude x-pack/package.json from being a bazel package * refact(NA): include normalized method on bazel-packages package * chore(NA): fix check ts projects task * chore(NA): impossible if so cli integartion test passes * chore(NA): fix jest tests for @kbn/pm * chore(NA): use created packages * chore(NA): discard dependencies on child projects * chore(NA): remove changes from cli * chore(NA): remove wrongly commented line on @kbn/pm * fix(NA): build tasks to exclude correct bazel package locations * chore(NA): include free packages on cli * chore(NA): update import resolver * chore(NA): removing location free plugins created for testing purposes * refact(NA): imports order on @kbn/bazel-packages * docs(NA): clarify notes around the changes to discoverBazelPackageLocations * refact(NA): remove redundant code from packages/kbn-import-resolver/src/import_resolver.ts * chore(NA): remove typo from previous commit * refact(NA): simplify clean task removing filter for dev packages * chore(NA): apply eslint lint fix * refact(NA): simplify discoverBazelPackageLocations logic * chore(NA): redo changes on import resolver checks Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../src/bazel_package_dirs.ts | 2 +- .../src/discover_packages.ts | 21 ++++++- .../src/import_resolver.ts | 15 ++++- packages/kbn-pm/dist/index.js | 46 ++++----------- .../kbn-pm/src/__snapshots__/run.test.ts.snap | 30 +++------- .../kibana/packages/bar/package.json | 5 +- .../__fixtures__/plugins/quux/package.json | 6 +- .../__fixtures__/plugins/zorge/package.json | 5 +- .../utils/__snapshots__/project.test.ts.snap | 4 -- .../utils/__snapshots__/projects.test.ts.snap | 25 +++----- packages/kbn-pm/src/utils/project.test.ts | 59 ------------------- packages/kbn-pm/src/utils/project.ts | 47 --------------- packages/kbn-pm/src/utils/projects.test.ts | 2 +- packages/kbn-pm/src/utils/projects.ts | 16 ++++- src/dev/build/build_distributables.ts | 3 +- src/dev/build/tasks/build_packages_task.ts | 7 ++- src/dev/build/tasks/clean_tasks.ts | 24 +++++--- src/dev/build/tasks/copy_source_task.ts | 7 +-- src/dev/build/tasks/transpile_babel_task.ts | 3 +- src/dev/typescript/projects.ts | 35 ++++++----- x-pack/tasks/build.ts | 7 +++ 21 files changed, 129 insertions(+), 240 deletions(-) diff --git a/packages/kbn-bazel-packages/src/bazel_package_dirs.ts b/packages/kbn-bazel-packages/src/bazel_package_dirs.ts index 1027883df10ddc..80248646f1e6f7 100644 --- a/packages/kbn-bazel-packages/src/bazel_package_dirs.ts +++ b/packages/kbn-bazel-packages/src/bazel_package_dirs.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ +import globby from 'globby'; import Path from 'path'; -import globby from 'globby'; import { REPO_ROOT } from '@kbn/utils'; /** diff --git a/packages/kbn-bazel-packages/src/discover_packages.ts b/packages/kbn-bazel-packages/src/discover_packages.ts index db31b54beec75f..8b78e4e2931188 100644 --- a/packages/kbn-bazel-packages/src/discover_packages.ts +++ b/packages/kbn-bazel-packages/src/discover_packages.ts @@ -9,6 +9,7 @@ import Path from 'path'; import globby from 'globby'; +import normalizePath from 'normalize-path'; import { REPO_ROOT } from '@kbn/utils'; import { asyncMapWithLimit } from '@kbn/std'; @@ -16,7 +17,7 @@ import { BazelPackage } from './bazel_package'; import { BAZEL_PACKAGE_DIRS } from './bazel_package_dirs'; export function discoverBazelPackageLocations(repoRoot: string) { - return globby + const packagesWithPackageJson = globby .sync( BAZEL_PACKAGE_DIRS.map((dir) => `${dir}/*/package.json`), { @@ -24,8 +25,26 @@ export function discoverBazelPackageLocations(repoRoot: string) { absolute: true, } ) + // NOTE: removing x-pack by default for now to prevent a situation where a BUILD.bazel file + // needs to be added at the root of the folder which will make x-pack to be wrongly recognized + // as a Bazel package in that case + .filter((path) => !normalizePath(path).includes('x-pack/package.json')) .sort((a, b) => a.localeCompare(b)) .map((path) => Path.dirname(path)); + + const packagesWithBuildBazel = globby + .sync( + BAZEL_PACKAGE_DIRS.map((dir) => `${dir}/*/BUILD.bazel`), + { + cwd: repoRoot, + absolute: true, + } + ) + .map((path) => Path.dirname(path)); + + // NOTE: only return as discovered packages the ones with a package.json + BUILD.bazel file. + // In the future we should change this to only discover the ones declaring kibana.json. + return packagesWithPackageJson.filter((pkg) => packagesWithBuildBazel.includes(pkg)); } export async function discoverBazelPackages(repoRoot: string = REPO_ROOT) { diff --git a/packages/kbn-import-resolver/src/import_resolver.ts b/packages/kbn-import-resolver/src/import_resolver.ts index 44114ce731e28f..05b69f299a798b 100644 --- a/packages/kbn-import-resolver/src/import_resolver.ts +++ b/packages/kbn-import-resolver/src/import_resolver.ts @@ -25,9 +25,18 @@ const NODE_MODULE_SEG = Path.sep + 'node_modules' + Path.sep; export class ImportResolver { static create(repoRoot: string) { const pkgMap = new Map(); - for (const dir of discoverBazelPackageLocations(repoRoot)) { - const pkg = JSON.parse(Fs.readFileSync(Path.resolve(dir, 'package.json'), 'utf8')); - pkgMap.set(pkg.name, normalizePath(Path.relative(repoRoot, dir))); + for (const dir of discoverBazelPackageLocations(REPO_ROOT)) { + const relativeBazelPackageDir = Path.relative(REPO_ROOT, dir); + const repoRootBazelPackageDir = Path.resolve(repoRoot, relativeBazelPackageDir); + + if (!Fs.existsSync(Path.resolve(repoRootBazelPackageDir, 'package.json'))) { + continue; + } + + const pkg = JSON.parse( + Fs.readFileSync(Path.resolve(repoRootBazelPackageDir, 'package.json'), 'utf8') + ); + pkgMap.set(pkg.name, normalizePath(relativeBazelPackageDir)); } return new ImportResolver(repoRoot, pkgMap, readPackageMap()); diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 97ff65d4f71bc0..184c16f96167f7 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -61563,32 +61563,6 @@ class Project { return this.json.name; } - ensureValidProjectDependency(project) { - const relativePathToProject = normalizePath(path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(this.path, project.path)); - const relativePathToProjectIfBazelPkg = normalizePath(path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(this.path, `${__dirname}/../../../bazel-bin/packages/${path__WEBPACK_IMPORTED_MODULE_1___default.a.basename(project.path)}`)); - const versionInPackageJson = this.allDependencies[project.name]; - const expectedVersionInPackageJson = `link:${relativePathToProject}`; - const expectedVersionInPackageJsonIfBazelPkg = `link:${relativePathToProjectIfBazelPkg}`; // TODO: after introduce bazel to build all the packages and completely remove the support for kbn packages - // do not allow child projects to hold dependencies, unless they are meant to be published externally - - if (versionInPackageJson === expectedVersionInPackageJson || versionInPackageJson === expectedVersionInPackageJsonIfBazelPkg) { - return; - } - - const updateMsg = 'Update its package.json to the expected value below.'; - const meta = { - actual: `"${project.name}": "${versionInPackageJson}"`, - expected: `"${project.name}": "${expectedVersionInPackageJson}" or "${project.name}": "${expectedVersionInPackageJsonIfBazelPkg}"`, - package: `${this.name} (${this.packageJsonLocation})` - }; - - if (Object(_package_json__WEBPACK_IMPORTED_MODULE_5__[/* isLinkDependency */ "a"])(versionInPackageJson)) { - throw new _errors__WEBPACK_IMPORTED_MODULE_3__[/* CliError */ "a"](`[${this.name}] depends on [${project.name}] using 'link:', but the path is wrong. ${updateMsg}`, meta); - } - - throw new _errors__WEBPACK_IMPORTED_MODULE_3__[/* CliError */ "a"](`[${this.name}] depends on [${project.name}] but it's not using the local package. ${updateMsg}`, meta); - } - getBuildConfig() { return this.json.kibana && this.json.kibana.build || {}; } @@ -61660,10 +61634,6 @@ class Project { return Object.values(this.allDependencies).every(dep => Object(_package_json__WEBPACK_IMPORTED_MODULE_5__[/* isLinkDependency */ "a"])(dep)); } -} // We normalize all path separators to `/` in generated files - -function normalizePath(path) { - return path.replace(/[\\\/]+/g, '/'); } /***/ }), @@ -61685,7 +61655,8 @@ function normalizePath(path) { /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__("util"); /* 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__("./src/utils/errors.ts"); -/* harmony import */ var _project__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__("./src/utils/project.ts"); +/* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__("./src/utils/log.ts"); +/* harmony import */ var _project__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__("./src/utils/project.ts"); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -61698,6 +61669,7 @@ function normalizePath(path) { + const glob = Object(util__WEBPACK_IMPORTED_MODULE_2__["promisify"])(glob__WEBPACK_IMPORTED_MODULE_0___default.a); /** a Map of project names to Project instances */ @@ -61716,7 +61688,7 @@ async function getProjects(rootPath, projectsPathsPatterns, { for (const filePath of pathsToProcess) { const projectConfigPath = normalize(filePath); const projectDir = path__WEBPACK_IMPORTED_MODULE_1___default.a.dirname(projectConfigPath); - const project = await _project__WEBPACK_IMPORTED_MODULE_4__[/* Project */ "a"].fromPath(projectDir); + const project = await _project__WEBPACK_IMPORTED_MODULE_5__[/* Project */ "a"].fromPath(projectDir); const excludeProject = exclude.includes(project.name) || include.length > 0 && !include.includes(project.name) || bazelOnly && !project.isBazelPackage(); if (excludeProject) { @@ -61790,10 +61762,18 @@ function buildProjectGraph(projects) { const projectDeps = []; const dependencies = project.allDependencies; + if (!project.isSinglePackageJsonProject && Object.keys(dependencies).length > 0) { + _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].warning(`${project.name} is not allowed to hold local dependencies and they will be discarded. Please declare them at the root package.json`); + } + + if (!project.isSinglePackageJsonProject) { + projectGraph.set(project.name, projectDeps); + continue; + } + for (const depName of Object.keys(dependencies)) { if (projects.has(depName)) { const dep = projects.get(depName); - project.ensureValidProjectDependency(dep); projectDeps.push(dep); } } diff --git a/packages/kbn-pm/src/__snapshots__/run.test.ts.snap b/packages/kbn-pm/src/__snapshots__/run.test.ts.snap index e5efc9a9152249..28e1b98e0fcd96 100644 --- a/packages/kbn-pm/src/__snapshots__/run.test.ts.snap +++ b/packages/kbn-pm/src/__snapshots__/run.test.ts.snap @@ -4,14 +4,9 @@ exports[`excludes project if single \`exclude\` filter is specified 1`] = ` Object { "graph": Object { "bar": Array [], - "baz": Array [ - "bar", - ], + "baz": Array [], "kibana": Array [], - "quux": Array [ - "bar", - "baz", - ], + "quux": Array [], "with-additional-projects": Array [], }, "projects": Array [ @@ -42,12 +37,8 @@ Object { exports[`includes only projects specified in multiple \`include\` filters 1`] = ` Object { "graph": Object { - "bar": Array [ - "foo", - ], - "baz": Array [ - "bar", - ], + "bar": Array [], + "baz": Array [], "foo": Array [], }, "projects": Array [ @@ -72,20 +63,13 @@ Object { exports[`passes all found projects to the command if no filter is specified 1`] = ` Object { "graph": Object { - "bar": Array [ - "foo", - ], - "baz": Array [ - "bar", - ], + "bar": Array [], + "baz": Array [], "foo": Array [], "kibana": Array [ "foo", ], - "quux": Array [ - "bar", - "baz", - ], + "quux": Array [], "with-additional-projects": Array [], }, "projects": Array [ diff --git a/packages/kbn-pm/src/utils/__fixtures__/kibana/packages/bar/package.json b/packages/kbn-pm/src/utils/__fixtures__/kibana/packages/bar/package.json index b5eae58393860f..06a8b8dcc6aa8a 100644 --- a/packages/kbn-pm/src/utils/__fixtures__/kibana/packages/bar/package.json +++ b/packages/kbn-pm/src/utils/__fixtures__/kibana/packages/bar/package.json @@ -1,7 +1,4 @@ { "name": "bar", - "version": "1.0.0", - "dependencies": { - "foo": "link:../foo" - } + "version": "1.0.0" } diff --git a/packages/kbn-pm/src/utils/__fixtures__/plugins/quux/package.json b/packages/kbn-pm/src/utils/__fixtures__/plugins/quux/package.json index b2794986c5b0bc..f2a30624545092 100644 --- a/packages/kbn-pm/src/utils/__fixtures__/plugins/quux/package.json +++ b/packages/kbn-pm/src/utils/__fixtures__/plugins/quux/package.json @@ -1,8 +1,4 @@ { "name": "quux", - "version": "1.0.0", - "dependencies": { - "bar": "link:../../kibana/packages/bar", - "baz": "link:../baz" - } + "version": "1.0.0" } diff --git a/packages/kbn-pm/src/utils/__fixtures__/plugins/zorge/package.json b/packages/kbn-pm/src/utils/__fixtures__/plugins/zorge/package.json index 80a27b17661dd1..3f22a1845b66a1 100644 --- a/packages/kbn-pm/src/utils/__fixtures__/plugins/zorge/package.json +++ b/packages/kbn-pm/src/utils/__fixtures__/plugins/zorge/package.json @@ -1,7 +1,4 @@ { "name": "zorge", - "version": "1.0.0", - "dependencies": { - "foo": "link:../../kibana/packages/foo" - } + "version": "1.0.0" } diff --git a/packages/kbn-pm/src/utils/__snapshots__/project.test.ts.snap b/packages/kbn-pm/src/utils/__snapshots__/project.test.ts.snap index b3bcc402db2a30..d4a4e5ca234528 100644 --- a/packages/kbn-pm/src/utils/__snapshots__/project.test.ts.snap +++ b/packages/kbn-pm/src/utils/__snapshots__/project.test.ts.snap @@ -1,7 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#ensureValidProjectDependency using link:, but with wrong path 1`] = `"[kibana] depends on [foo] using 'link:', but the path is wrong. Update its package.json to the expected value below."`; - -exports[`#ensureValidProjectDependency using version instead of link: 1`] = `"[kibana] depends on [foo] but it's not using the local package. Update its package.json to the expected value below."`; - exports[`#getExecutables() throws CliError when bin is something strange 1`] = `"[kibana] has an invalid \\"bin\\" field in its package.json, expected an object or a string"`; diff --git a/packages/kbn-pm/src/utils/__snapshots__/projects.test.ts.snap b/packages/kbn-pm/src/utils/__snapshots__/projects.test.ts.snap index 86ba136c50aa17..a716b9fab4e5b7 100644 --- a/packages/kbn-pm/src/utils/__snapshots__/projects.test.ts.snap +++ b/packages/kbn-pm/src/utils/__snapshots__/projects.test.ts.snap @@ -2,37 +2,28 @@ exports[`#buildProjectGraph builds full project graph 1`] = ` Object { - "bar": Array [ - "foo", - ], + "bar": Array [], "baz": Array [], "foo": Array [], "kibana": Array [ "foo", ], - "quux": Array [ - "bar", - "baz", - ], - "zorge": Array [ - "foo", - ], + "quux": Array [], + "zorge": Array [], } `; exports[`#topologicallyBatchProjects batches projects topologically based on their project dependencies 1`] = ` Array [ Array [ + "bar", "foo", "baz", - ], - Array [ - "kibana", - "bar", + "quux", "zorge", ], Array [ - "quux", + "kibana", ], ] `; @@ -43,10 +34,8 @@ Array [ "kibana", "bar", "baz", - "zorge", - ], - Array [ "quux", + "zorge", ], ] `; diff --git a/packages/kbn-pm/src/utils/project.test.ts b/packages/kbn-pm/src/utils/project.test.ts index 9be59538802838..389dbf123cd52c 100644 --- a/packages/kbn-pm/src/utils/project.test.ts +++ b/packages/kbn-pm/src/utils/project.test.ts @@ -50,65 +50,6 @@ test('fields', async () => { expect(kibana.hasScript('build')).toBe(false); }); -describe('#ensureValidProjectDependency', () => { - test('valid link: version', async () => { - const root = createProjectWith({ - dependencies: { - foo: 'link:packages/foo', - }, - }); - - const foo = createProjectWith( - { - name: 'foo', - }, - 'packages/foo' - ); - - expect(() => root.ensureValidProjectDependency(foo)).not.toThrow(); - }); - - test('using link:, but with wrong path', () => { - const root = createProjectWith( - { - dependencies: { - foo: 'link:wrong/path', - }, - }, - rootPath - ); - - const foo = createProjectWith( - { - name: 'foo', - }, - 'packages/foo' - ); - - expect(() => root.ensureValidProjectDependency(foo)).toThrowErrorMatchingSnapshot(); - }); - - test('using version instead of link:', () => { - const root = createProjectWith( - { - dependencies: { - foo: '1.0.0', - }, - }, - rootPath - ); - - const foo = createProjectWith( - { - name: 'foo', - }, - 'packages/foo' - ); - - expect(() => root.ensureValidProjectDependency(foo)).toThrowErrorMatchingSnapshot(); - }); -}); - describe('#getExecutables()', () => { test('converts bin:string to an object with absolute paths', () => { const project = createProjectWith({ diff --git a/packages/kbn-pm/src/utils/project.ts b/packages/kbn-pm/src/utils/project.ts index 48c606c10da42c..842f8285431166 100644 --- a/packages/kbn-pm/src/utils/project.ts +++ b/packages/kbn-pm/src/utils/project.ts @@ -88,48 +88,6 @@ export class Project { return this.json.name; } - public ensureValidProjectDependency(project: Project) { - const relativePathToProject = normalizePath(Path.relative(this.path, project.path)); - const relativePathToProjectIfBazelPkg = normalizePath( - Path.relative( - this.path, - `${__dirname}/../../../bazel-bin/packages/${Path.basename(project.path)}` - ) - ); - - const versionInPackageJson = this.allDependencies[project.name]; - const expectedVersionInPackageJson = `link:${relativePathToProject}`; - const expectedVersionInPackageJsonIfBazelPkg = `link:${relativePathToProjectIfBazelPkg}`; - - // TODO: after introduce bazel to build all the packages and completely remove the support for kbn packages - // do not allow child projects to hold dependencies, unless they are meant to be published externally - if ( - versionInPackageJson === expectedVersionInPackageJson || - versionInPackageJson === expectedVersionInPackageJsonIfBazelPkg - ) { - return; - } - - const updateMsg = 'Update its package.json to the expected value below.'; - const meta = { - actual: `"${project.name}": "${versionInPackageJson}"`, - expected: `"${project.name}": "${expectedVersionInPackageJson}" or "${project.name}": "${expectedVersionInPackageJsonIfBazelPkg}"`, - package: `${this.name} (${this.packageJsonLocation})`, - }; - - if (isLinkDependency(versionInPackageJson)) { - throw new CliError( - `[${this.name}] depends on [${project.name}] using 'link:', but the path is wrong. ${updateMsg}`, - meta - ); - } - - throw new CliError( - `[${this.name}] depends on [${project.name}] but it's not using the local package. ${updateMsg}`, - meta - ); - } - public getBuildConfig(): BuildConfig { return (this.json.kibana && this.json.kibana.build) || {}; } @@ -206,8 +164,3 @@ export class Project { return Object.values(this.allDependencies).every((dep) => isLinkDependency(dep)); } } - -// We normalize all path separators to `/` in generated files -function normalizePath(path: string) { - return path.replace(/[\\\/]+/g, '/'); -} diff --git a/packages/kbn-pm/src/utils/projects.test.ts b/packages/kbn-pm/src/utils/projects.test.ts index bf7bb052b254ae..c87876642cf0bd 100644 --- a/packages/kbn-pm/src/utils/projects.test.ts +++ b/packages/kbn-pm/src/utils/projects.test.ts @@ -249,6 +249,6 @@ describe('#includeTransitiveProjects', () => { const quux = projects.get('quux')!; const withTransitive = includeTransitiveProjects([quux], projects); - expect([...withTransitive.keys()]).toEqual(['quux', 'bar', 'baz', 'foo']); + expect([...withTransitive.keys()]).toEqual(['quux']); }); }); diff --git a/packages/kbn-pm/src/utils/projects.ts b/packages/kbn-pm/src/utils/projects.ts index 28a1fcfec8c367..e30dfc9f4c8769 100644 --- a/packages/kbn-pm/src/utils/projects.ts +++ b/packages/kbn-pm/src/utils/projects.ts @@ -11,6 +11,7 @@ import path from 'path'; import { promisify } from 'util'; import { CliError } from './errors'; +import { log } from './log'; import { Project } from './project'; const glob = promisify(globSync); @@ -115,14 +116,23 @@ export function buildProjectGraph(projects: ProjectMap) { const projectGraph: ProjectGraph = new Map(); for (const project of projects.values()) { - const projectDeps = []; + const projectDeps: Project[] = []; const dependencies = project.allDependencies; + if (!project.isSinglePackageJsonProject && Object.keys(dependencies).length > 0) { + log.warning( + `${project.name} is not allowed to hold local dependencies and they will be discarded. Please declare them at the root package.json` + ); + } + + if (!project.isSinglePackageJsonProject) { + projectGraph.set(project.name, projectDeps); + continue; + } + for (const depName of Object.keys(dependencies)) { if (projects.has(depName)) { const dep = projects.get(depName)!; - project.ensureValidProjectDependency(dep); - projectDeps.push(dep); } } diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index ad9c8323b769d8..0a3db5dc36d070 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -80,10 +80,11 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions await run(Tasks.CreatePackageJson); await run(Tasks.InstallDependencies); await run(Tasks.GeneratePackagesOptimizedAssets); - await run(Tasks.CleanPackages); + await run(Tasks.DeleteBazelPackagesFromBuildRoot); await run(Tasks.CreateNoticeFile); await run(Tasks.UpdateLicenseFile); await run(Tasks.RemovePackageJsonDeps); + await run(Tasks.CleanPackageManagerRelatedFiles); await run(Tasks.CleanTypescript); await run(Tasks.CleanExtraFilesFromModules); await run(Tasks.CleanEmptyFolders); diff --git a/src/dev/build/tasks/build_packages_task.ts b/src/dev/build/tasks/build_packages_task.ts index e30ffd082e250a..62baf74559a2a7 100644 --- a/src/dev/build/tasks/build_packages_task.ts +++ b/src/dev/build/tasks/build_packages_task.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import cpy from 'cpy'; import Path from 'path'; import { discoverBazelPackages } from '@kbn/bazel-packages'; @@ -53,9 +54,9 @@ export const BuildXpack: Task = { }); log.info('copying built x-pack into build dir'); - await scanCopy({ - source: config.resolveFromRepo('x-pack/build/plugin/kibana/x-pack'), - destination: build.resolvePath('x-pack'), + await cpy('**/{.,}*', build.resolvePath('x-pack'), { + cwd: config.resolveFromRepo('x-pack/build/plugin/kibana/x-pack'), + parents: true, }); }, }; diff --git a/src/dev/build/tasks/clean_tasks.ts b/src/dev/build/tasks/clean_tasks.ts index 19747ce72b5a65..c794ca277f77f9 100644 --- a/src/dev/build/tasks/clean_tasks.ts +++ b/src/dev/build/tasks/clean_tasks.ts @@ -7,7 +7,7 @@ */ import minimatch from 'minimatch'; - +import { discoverBazelPackages } from '@kbn/bazel-packages'; import { deleteAll, deleteEmptyFolders, scanDelete, Task, GlobalTask } from '../lib'; export const Clean: GlobalTask = { @@ -26,14 +26,11 @@ export const Clean: GlobalTask = { }, }; -export const CleanPackages: Task = { - description: 'Cleaning source for packages that are now installed in node_modules', +export const CleanPackageManagerRelatedFiles: Task = { + description: 'Cleaning package manager related files from the build folder', async run(config, log, build) { - await deleteAll( - [build.resolvePath('packages'), build.resolvePath('yarn.lock'), build.resolvePath('.npmrc')], - log - ); + await deleteAll([build.resolvePath('yarn.lock'), build.resolvePath('.npmrc')], log); }, }; @@ -200,3 +197,16 @@ export const CleanEmptyFolders: Task = { ]); }, }; + +export const DeleteBazelPackagesFromBuildRoot: Task = { + description: + 'Deleting bazel packages outputs from build folder root as they are now installed as node_modules', + + async run(config, log, build) { + const bazelPackagesOnBuildRoot = (await discoverBazelPackages()).map((pkg) => + build.resolvePath(pkg.normalizedRepoRelativeDir) + ); + + await deleteAll(bazelPackagesOnBuildRoot, log); + }, +}; diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index 141eafc57ae1fa..9fc0827e8c2c62 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { getAllRepoRelativeBazelPackageDirs } from '@kbn/bazel-packages'; -import normalizePath from 'normalize-path'; +import { discoverBazelPackages } from '@kbn/bazel-packages'; import { copyAll, Task } from '../lib'; @@ -48,8 +47,8 @@ export const CopySource: Task = { 'tsconfig*.json', '.i18nrc.json', 'kibana.d.ts', - // explicitly ignore all package roots, even if they're not selected by previous patterns - ...getAllRepoRelativeBazelPackageDirs().map((dir) => `!${normalizePath(dir)}/**`), + // explicitly ignore all bazel package locations, even if they're not selected by previous patterns + ...(await discoverBazelPackages()).map((pkg) => `!${pkg.normalizedRepoRelativeDir}/**`), ], }); }, diff --git a/src/dev/build/tasks/transpile_babel_task.ts b/src/dev/build/tasks/transpile_babel_task.ts index 37f63d31415e99..ee7d1e19de43a4 100644 --- a/src/dev/build/tasks/transpile_babel_task.ts +++ b/src/dev/build/tasks/transpile_babel_task.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { discoverBazelPackages } from '@kbn/bazel-packages'; import { pipeline } from 'stream'; import { promisify } from 'util'; @@ -24,10 +25,10 @@ const transpileWithBabel = async (srcGlobs: string[], build: Build, preset: stri vfs.src( srcGlobs.concat([ '!**/*.d.ts', - '!packages/**', '!**/node_modules/**', '!**/bower_components/**', '!**/__tests__/**', + ...(await discoverBazelPackages()).map((pkg) => `!${pkg.normalizedRepoRelativeDir}/**`), ]), { cwd: buildRoot, diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 6d9b372069b225..848ca09a86671f 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import glob from 'glob'; +import globby from 'globby'; import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { BAZEL_PACKAGE_DIRS } from '@kbn/bazel-packages'; @@ -23,11 +23,8 @@ const createProject = (rootRelativePath: string, options: ProjectOptions = {}) = cache: PROJECT_CACHE, }); -const findProjects = (pattern: string) => - // NOTE: using glob.sync rather than glob-all or globby - // because it takes less than 10 ms, while the other modules - // both took closer to 1000ms. - glob.sync(pattern, { cwd: REPO_ROOT }).map((path) => createProject(path)); +const findProjects = (patterns: string[]) => + globby.sync(patterns, { cwd: REPO_ROOT }).map((path) => createProject(path)); export const PROJECTS = [ createProject('tsconfig.json'), @@ -73,16 +70,18 @@ export const PROJECTS = [ disableTypeCheck: true, }), - ...findProjects('src/plugins/*/tsconfig.json'), - ...findProjects('src/plugins/chart_expressions/*/tsconfig.json'), - ...findProjects('src/plugins/vis_types/*/tsconfig.json'), - ...findProjects('x-pack/plugins/*/tsconfig.json'), - ...findProjects('examples/*/tsconfig.json'), - ...findProjects('x-pack/examples/*/tsconfig.json'), - ...findProjects('test/plugin_functional/plugins/*/tsconfig.json'), - ...findProjects('test/interpreter_functional/plugins/*/tsconfig.json'), - ...findProjects('test/server_integration/__fixtures__/plugins/*/tsconfig.json'), - ...findProjects('packages/kbn-type-summarizer/tests/tsconfig.json'), - - ...BAZEL_PACKAGE_DIRS.flatMap((dir) => findProjects(`${dir}/*/tsconfig.json`)), + // Glob patterns to be all search at once + ...findProjects([ + 'src/plugins/*/tsconfig.json', + 'src/plugins/chart_expressions/*/tsconfig.json', + 'src/plugins/vis_types/*/tsconfig.json', + 'x-pack/plugins/*/tsconfig.json', + 'examples/*/tsconfig.json', + 'x-pack/examples/*/tsconfig.json', + 'test/plugin_functional/plugins/*/tsconfig.json', + 'test/interpreter_functional/plugins/*/tsconfig.json', + 'test/server_integration/__fixtures__/plugins/*/tsconfig.json', + 'packages/kbn-type-summarizer/tests/tsconfig.json', + ...BAZEL_PACKAGE_DIRS.map((dir) => `${dir}/*/tsconfig.json`), + ]), ]; diff --git a/x-pack/tasks/build.ts b/x-pack/tasks/build.ts index 4680464976d79f..dacb6b6447affc 100644 --- a/x-pack/tasks/build.ts +++ b/x-pack/tasks/build.ts @@ -11,6 +11,7 @@ import { writeFileSync } from 'fs'; import { promisify } from 'util'; import { pipeline } from 'stream'; +import { discoverBazelPackages } from '@kbn/bazel-packages'; import { REPO_ROOT } from '@kbn/utils'; import { transformFileStream, transformFileWithBabel } from '@kbn/dev-utils'; import { ToolingLog } from '@kbn/tooling-log'; @@ -49,6 +50,11 @@ async function reportTask() { } async function copySourceAndBabelify() { + // get bazel packages inside x-pack + const xpackBazelPackages = (await discoverBazelPackages()) + .filter((pkg) => pkg.normalizedRepoRelativeDir.startsWith('x-pack/')) + .map((pkg) => `${pkg.normalizedRepoRelativeDir.replace('x-pack/', '')}/**`); + // copy source files and apply some babel transformations in the process await asyncPipeline( vfs.src( @@ -87,6 +93,7 @@ async function copySourceAndBabelify() { 'plugins/apm/ftr_e2e/**', 'plugins/apm/scripts/**', 'plugins/lists/server/scripts/**', + ...xpackBazelPackages, ], allowEmpty: true, } From 692c47f616c72e7d3196ae79d67c3c91d417a5b2 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 5 May 2022 19:26:49 -0500 Subject: [PATCH 41/83] skip flaky suite (#131611) --- .../host_alerts_table/host_alerts_table.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx index 4294f23f796bd6..a89055a72df6f1 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx @@ -47,7 +47,8 @@ const renderComponent = () => ); -describe('HostAlertsTable', () => { +// FLAKY: https://github.com/elastic/kibana/issues/131611 +describe.skip('HostAlertsTable', () => { beforeEach(() => { jest.clearAllMocks(); }); From 7f62a784df49f43a50b80dc26612250bf77554bd Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Fri, 6 May 2022 04:11:51 +0200 Subject: [PATCH 42/83] [packages] add kbn-performance-testing-dataset-extractor (#131631) --- .../scalability_dataset_extraction.sh | 12 +- package.json | 3 +- packages/BUILD.bazel | 2 + .../BUILD.bazel | 122 +++++++++++++++ .../README.md | 14 ++ .../jest.config.js | 13 ++ .../package.json | 11 ++ .../src/cli.ts | 81 ++++++++++ .../src/es_client.ts | 148 ++++++++++++++++++ .../src/extractor.ts | 88 +++++++++++ .../src/index.ts | 10 ++ .../tsconfig.json | 17 ++ .../extract_performance_testing_dataset.js | 10 ++ yarn.lock | 79 ++-------- 14 files changed, 543 insertions(+), 67 deletions(-) create mode 100644 packages/kbn-performance-testing-dataset-extractor/BUILD.bazel create mode 100644 packages/kbn-performance-testing-dataset-extractor/README.md create mode 100644 packages/kbn-performance-testing-dataset-extractor/jest.config.js create mode 100644 packages/kbn-performance-testing-dataset-extractor/package.json create mode 100644 packages/kbn-performance-testing-dataset-extractor/src/cli.ts create mode 100644 packages/kbn-performance-testing-dataset-extractor/src/es_client.ts create mode 100644 packages/kbn-performance-testing-dataset-extractor/src/extractor.ts create mode 100644 packages/kbn-performance-testing-dataset-extractor/src/index.ts create mode 100644 packages/kbn-performance-testing-dataset-extractor/tsconfig.json create mode 100644 scripts/extract_performance_testing_dataset.js diff --git a/.buildkite/scripts/steps/functional/scalability_dataset_extraction.sh b/.buildkite/scripts/steps/functional/scalability_dataset_extraction.sh index b2ce23db38fdb7..1a8bd77bd2893d 100755 --- a/.buildkite/scripts/steps/functional/scalability_dataset_extraction.sh +++ b/.buildkite/scripts/steps/functional/scalability_dataset_extraction.sh @@ -19,14 +19,20 @@ for i in "${journeys[@]}"; do JOURNEY_NAME="${i}" echo "Looking for JOURNEY=${JOURNEY_NAME} and BUILD_ID=${BUILD_ID} in APM traces" - ./node_modules/.bin/performance-testing-dataset-extractor -u "${USER_FROM_VAULT}" -p "${PASS_FROM_VAULT}" -c "${ES_SERVER_URL}" -b "${BUILD_ID}" -n "${JOURNEY_NAME}" + node scripts/extract_performance_testing_dataset \ + --journeyName "${JOURNEY_NAME}" \ + --buildId "${BUILD_ID}" \ + --es-url "${ES_SERVER_URL}" \ + --es-username "${USER_FROM_VAULT}" \ + --es-password "${PASS_FROM_VAULT}" done -# archive json files with traces and upload as build artifacts echo "--- Upload Kibana build, plugins and scalability traces to the public bucket" mkdir "${BUILD_ID}" -tar -czf "${BUILD_ID}/scalability_traces.tar.gz" output +# Archive json files with traces and upload as build artifacts +tar -czf "${BUILD_ID}/scalability_traces.tar.gz" -C target scalability_traces buildkite-agent artifact upload "${BUILD_ID}/scalability_traces.tar.gz" +# Upload Kibana build, plugins, commit sha and traces to the bucket buildkite-agent artifact download kibana-default.tar.gz ./"${BUILD_ID}" buildkite-agent artifact download kibana-default-plugins.tar.gz ./"${BUILD_ID}" echo "${BUILDKITE_COMMIT}" > "${BUILD_ID}/KIBANA_COMMIT_HASH" diff --git a/package.json b/package.json index a896137ee34b2c..69db4553679523 100644 --- a/package.json +++ b/package.json @@ -474,7 +474,6 @@ "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^6.0.0", - "@elastic/performance-testing-dataset-extractor": "^0.0.3", "@elastic/synthetics": "^1.0.0-beta.22", "@emotion/babel-preset-css-prop": "^11.2.0", "@emotion/jest": "^11.9.0", @@ -501,6 +500,7 @@ "@kbn/import-resolver": "link:bazel-bin/packages/kbn-import-resolver", "@kbn/jest-serializers": "link:bazel-bin/packages/kbn-jest-serializers", "@kbn/optimizer": "link:bazel-bin/packages/kbn-optimizer", + "@kbn/performance-testing-dataset-extractor": "link:bazel-bin/packages/kbn-performance-testing-dataset-extractor", "@kbn/plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator", "@kbn/plugin-helpers": "link:bazel-bin/packages/kbn-plugin-helpers", "@kbn/pm": "link:packages/kbn-pm", @@ -647,6 +647,7 @@ "@types/kbn__mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module_types", "@types/kbn__monaco": "link:bazel-bin/packages/kbn-monaco/npm_module_types", "@types/kbn__optimizer": "link:bazel-bin/packages/kbn-optimizer/npm_module_types", + "@types/kbn__performance-testing-dataset-extractor": "link:bazel-bin/packages/kbn-performance-testing-dataset-extractor/npm_module_types", "@types/kbn__plugin-discovery": "link:bazel-bin/packages/kbn-plugin-discovery/npm_module_types", "@types/kbn__plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator/npm_module_types", "@types/kbn__plugin-helpers": "link:bazel-bin/packages/kbn-plugin-helpers/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 52d4cbbdfb8c09..45f57b4c4bed0e 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -62,6 +62,7 @@ filegroup( "//packages/kbn-mapbox-gl:build", "//packages/kbn-monaco:build", "//packages/kbn-optimizer:build", + "//packages/kbn-performance-testing-dataset-extractor:build", "//packages/kbn-plugin-discovery:build", "//packages/kbn-plugin-generator:build", "//packages/kbn-plugin-helpers:build", @@ -160,6 +161,7 @@ filegroup( "//packages/kbn-mapbox-gl:build_types", "//packages/kbn-monaco:build_types", "//packages/kbn-optimizer:build_types", + "//packages/kbn-performance-testing-dataset-extractor:build_types", "//packages/kbn-plugin-discovery:build_types", "//packages/kbn-plugin-generator:build_types", "//packages/kbn-plugin-helpers:build_types", diff --git a/packages/kbn-performance-testing-dataset-extractor/BUILD.bazel b/packages/kbn-performance-testing-dataset-extractor/BUILD.bazel new file mode 100644 index 00000000000000..b58375165352cb --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/BUILD.bazel @@ -0,0 +1,122 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "kbn-performance-testing-dataset-extractor" +PKG_REQUIRE_NAME = "@kbn/performance-testing-dataset-extractor" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-utils", + "//packages/kbn-tooling-log", + "@npm//@elastic/elasticsearch", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "//packages/kbn-dev-utils:npm_module_types", + "//packages/kbn-utils:npm_module_types", + "//packages/kbn-tooling-log:npm_module_types", + "@npm//@elastic/elasticsearch", + "@npm//@types/node", + "@npm//@types/jest", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-performance-testing-dataset-extractor/README.md b/packages/kbn-performance-testing-dataset-extractor/README.md new file mode 100644 index 00000000000000..ef5488a82ff211 --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/README.md @@ -0,0 +1,14 @@ +# @kbn/performance-testing-dataset-extractor + +A library to convert APM traces into JSON format for performance testing. + +## Usage + +``` + node scripts/extract_performance_testing_dataset \ + --journeyName "<_source.labels.journeyName>" \ + --buildId "<_source.labels.testBuildId>" \ + --es-url "" \ + --es-username "" \ + --es-password "" +``` \ No newline at end of file diff --git a/packages/kbn-performance-testing-dataset-extractor/jest.config.js b/packages/kbn-performance-testing-dataset-extractor/jest.config.js new file mode 100644 index 00000000000000..e31a2d79968931 --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/jest.config.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-performance-testing-dataset-extractor'], +}; diff --git a/packages/kbn-performance-testing-dataset-extractor/package.json b/packages/kbn-performance-testing-dataset-extractor/package.json new file mode 100644 index 00000000000000..4d637728b28de7 --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/package.json @@ -0,0 +1,11 @@ +{ + "name": "@kbn/performance-testing-dataset-extractor", + "description": "A library to convert APM traces into JSON format for performance testing.", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "kibana": { + "devOnly": true + } +} diff --git a/packages/kbn-performance-testing-dataset-extractor/src/cli.ts b/packages/kbn-performance-testing-dataset-extractor/src/cli.ts new file mode 100644 index 00000000000000..7d16f625e4874a --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/src/cli.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** *********************************************************** + * + * Run `node scripts/extract_performance_testing_dataset --help` for usage information + * + *************************************************************/ + +import { run, createFlagError } from '@kbn/dev-utils'; +import { extractor } from './extractor'; + +export async function runExtractor() { + run( + async ({ log, flags }) => { + const baseURL = flags['es-url']; + if (baseURL && typeof baseURL !== 'string') { + throw createFlagError('--es-url must be a string'); + } + if (!baseURL) { + throw createFlagError('--es-url must be defined'); + } + + const username = flags['es-username']; + if (username && typeof username !== 'string') { + throw createFlagError('--es-username must be a string'); + } + if (!username) { + throw createFlagError('--es-username must be defined'); + } + + const password = flags['es-password']; + if (password && typeof password !== 'string') { + throw createFlagError('--es-password must be a string'); + } + if (!password) { + throw createFlagError('--es-password must be defined'); + } + + const journeyName = flags.journeyName; + if (journeyName && typeof journeyName !== 'string') { + throw createFlagError('--journeyName must be a string'); + } + if (!journeyName) { + throw createFlagError('--journeyName must be defined'); + } + + const buildId = flags.buildId; + if (buildId && typeof buildId !== 'string') { + throw createFlagError('--buildId must be a string'); + } + if (!buildId) { + throw createFlagError('--buildId must be defined'); + } + + return extractor({ + param: { journeyName, buildId }, + client: { baseURL, username, password }, + log, + }); + }, + { + description: `CLI to fetch and normalize APM traces for journey scalability testing`, + flags: { + string: ['journeyName', 'buildId', 'es-url', 'es-username', 'es-password'], + help: ` + --journeyName Single user performance journey name, stored in APM-based document as label: 'labels.journeyName' + --buildId BUILDKITE_JOB_ID or uuid generated locally, stored in APM-based document as label: 'labels.testBuildId' + --es-url url for Elasticsearch (APM cluster) + --es-username username for Elasticsearch (APM cluster) + --es-password password for Elasticsearch (APM cluster) + `, + }, + } + ); +} diff --git a/packages/kbn-performance-testing-dataset-extractor/src/es_client.ts b/packages/kbn-performance-testing-dataset-extractor/src/es_client.ts new file mode 100644 index 00000000000000..53c2e8ba9e8c38 --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/src/es_client.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Client } from '@elastic/elasticsearch'; + +interface ClientOptions { + node: string; + username: string; + password: string; +} + +interface Labels { + journeyName: string; + maxUsersCount: string; +} + +interface Request { + method: string; + headers: string; + body?: { original: string }; +} + +interface Response { + status_code: number; +} + +interface Transaction { + id: string; + name: string; + type: string; +} + +export interface Document { + labels: Labels; + character: string; + quote: string; + service: { version: string }; + processor: string; + trace: { id: string }; + '@timestamp': string; + environment: string; + url: { path: string }; + http: { + request: Request; + response: Response; + }; + transaction: Transaction; +} + +export function initClient(options: ClientOptions) { + const client = new Client({ + node: options.node, + auth: { + username: options.username, + password: options.password, + }, + }); + + return { + async getTransactions(buildId: string, journeyName: string) { + const result = await client.search({ + body: { + track_total_hits: true, + sort: [ + { + '@timestamp': { + order: 'desc', + unmapped_type: 'boolean', + }, + }, + ], + size: 10000, + stored_fields: ['*'], + _source: true, + query: { + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'transaction.type': 'request', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'processor.event': 'transaction', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'labels.testBuildId': buildId, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'labels.journeyName': journeyName, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + should: [], + must_not: [], + }, + }, + }, + }); + return result?.hits?.hits; + }, + }; +} diff --git a/packages/kbn-performance-testing-dataset-extractor/src/extractor.ts b/packages/kbn-performance-testing-dataset-extractor/src/extractor.ts new file mode 100644 index 00000000000000..cd0dbed9d8e51a --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/src/extractor.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import fs from 'fs/promises'; +import { existsSync } from 'fs'; +import path from 'path'; +import { ToolingLog } from '@kbn/tooling-log'; +import { initClient, Document } from './es_client'; + +interface CLIParams { + param: { + journeyName: string; + buildId: string; + }; + client: { + baseURL: string; + username: string; + password: string; + }; + log: ToolingLog; +} + +export const extractor = async ({ param, client, log }: CLIParams) => { + const authOptions = { + node: client.baseURL, + username: client.username, + password: client.password, + }; + const esClient = initClient(authOptions); + const hits = await esClient.getTransactions(param.buildId, param.journeyName); + if (!hits || hits.length === 0) { + log.warning(` + No transactions found with 'labels.testBuildId=${param.buildId}' and 'labels.journeyName=${param.journeyName}' + \nOutput file won't be generated + `); + return; + } + + const source = hits[0]!._source as Document; + const journeyName = source.labels.journeyName || 'Unknown Journey'; + const kibanaVersion = source.service.version; + const maxUsersCount = source.labels.maxUsersCount || '0'; + + const data = hits + .map((hit) => hit!._source as Document) + .map((hit) => { + return { + processor: hit.processor, + traceId: hit.trace.id, + timestamp: hit['@timestamp'], + environment: hit.environment, + request: { + url: { path: hit.url.path }, + headers: hit.http.request.headers, + method: hit.http.request.method, + body: hit.http.request.body ? JSON.parse(hit.http.request.body.original) : '', + }, + response: { statusCode: hit.http.response.status_code }, + transaction: { + id: hit.transaction.id, + name: hit.transaction.name, + type: hit.transaction.type, + }, + }; + }); + + const output = { + journeyName, + kibanaVersion, + maxUsersCount, + traceItems: data, + }; + + const outputDir = path.resolve('target/scalability_traces'); + const fileName = `${output.journeyName.replace(/ /g, '')}-${param.buildId}.json`; + const filePath = path.resolve(outputDir, fileName); + + log.info(`Found ${hits.length} transactions, output file: ${filePath}`); + if (!existsSync(outputDir)) { + await fs.mkdir(outputDir, { recursive: true }); + } + await fs.writeFile(filePath, JSON.stringify(output, null, 2), 'utf8'); +}; diff --git a/packages/kbn-performance-testing-dataset-extractor/src/index.ts b/packages/kbn-performance-testing-dataset-extractor/src/index.ts new file mode 100644 index 00000000000000..4e739789d65af8 --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/src/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { extractor } from './extractor'; +export * from './cli'; diff --git a/packages/kbn-performance-testing-dataset-extractor/tsconfig.json b/packages/kbn-performance-testing-dataset-extractor/tsconfig.json new file mode 100644 index 00000000000000..a8cfc2cceb08b8 --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/scripts/extract_performance_testing_dataset.js b/scripts/extract_performance_testing_dataset.js new file mode 100644 index 00000000000000..deb3da481f1e12 --- /dev/null +++ b/scripts/extract_performance_testing_dataset.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env'); +require('@kbn/performance-testing-dataset-extractor').runExtractor(); diff --git a/yarn.lock b/yarn.lock index 5b1d85c282b2bf..6eddaa5c415094 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1496,16 +1496,6 @@ dependencies: "@elastic/ecs-helpers" "^1.1.0" -"@elastic/elasticsearch@7.17.0": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.17.0.tgz#589fb219234cf1b0da23744e82b1d25e2fe9a797" - integrity sha512-5QLPCjd0uLmLj1lSuKSThjNpq39f6NmlTy9ROLFwG5gjyTgpwSqufDeYG/Fm43Xs05uF7WcscoO7eguI3HuuYA== - dependencies: - debug "^4.3.1" - hpagent "^0.1.1" - ms "^2.1.3" - secure-json-parse "^2.4.0" - "@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@8.2.0-canary.2": version "8.2.0-canary.2" resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.2.0-canary.2.tgz#2513926cdbfe7c070e1fa6926f7829171b27cdba" @@ -1628,19 +1618,6 @@ resolved "https://registry.yarnpkg.com/@elastic/numeral/-/numeral-2.5.1.tgz#96acf39c3d599950646ef8ccfd24a3f057cf4932" integrity sha512-Tby6TKjixRFY+atVNeYUdGr9m0iaOq8230KTwn8BbUhkh7LwozfgKq0U98HRX7n63ZL62szl+cDKTYzh5WPCFQ== -"@elastic/performance-testing-dataset-extractor@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@elastic/performance-testing-dataset-extractor/-/performance-testing-dataset-extractor-0.0.3.tgz#c9823154c1d23c0dfec86f7183a5e2327999d0ca" - integrity sha512-ND33m4P1yOLPqnKnwWTcwDNB5dCw5NK9503e2WaZzljoy75RN9Lg5+YsQM7RFZKDs/+yNp7XRCJszeiUOcMFvg== - dependencies: - "@elastic/elasticsearch" "7.17.0" - axios "^0.26.1" - axios-curlirize "1.3.7" - lodash "^4.17.21" - qs "^6.10.3" - tslib "^2.3.1" - yargs "^17.4.0" - "@elastic/react-search-ui-views@1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@elastic/react-search-ui-views/-/react-search-ui-views-1.6.0.tgz#7211d47c29ef0636c853721491b9905ac7ae58da" @@ -2955,15 +2932,15 @@ version "0.0.0" uid "" -"@kbn/analytics-shippers-elastic-v3-common@link:bazel-bin/packages/analytics/shippers/elastic_v3/common": +"@kbn/analytics-shippers-elastic-v3-browser@link:bazel-bin/packages/analytics/shippers/elastic_v3/browser": version "0.0.0" uid "" -"@kbn/analytics-shippers-elastic-v3-server@link:bazel-bin/packages/analytics/shippers/elastic_v3/server": +"@kbn/analytics-shippers-elastic-v3-common@link:bazel-bin/packages/analytics/shippers/elastic_v3/common": version "0.0.0" uid "" -"@kbn/analytics-shippers-elastic-v3-browser@link:bazel-bin/packages/analytics/shippers/elastic_v3/browser": +"@kbn/analytics-shippers-elastic-v3-server@link:bazel-bin/packages/analytics/shippers/elastic_v3/server": version "0.0.0" uid "" @@ -3135,6 +3112,10 @@ version "0.0.0" uid "" +"@kbn/performance-testing-dataset-extractor@link:bazel-bin/packages/kbn-performance-testing-dataset-extractor": + version "0.0.0" + uid "" + "@kbn/plugin-discovery@link:bazel-bin/packages/kbn-plugin-discovery": version "0.0.0" uid "" @@ -6067,15 +6048,15 @@ version "0.0.0" uid "" -"@types/kbn__analytics-shippers-elastic-v3-common@link:bazel-bin/packages/analytics/shippers/elastic_v3/common/npm_module_types": +"@types/kbn__analytics-shippers-elastic-v3-browser@link:bazel-bin/packages/analytics/shippers/elastic_v3/browser/npm_module_types": version "0.0.0" uid "" -"@types/kbn__analytics-shippers-elastic-v3-server@link:bazel-bin/packages/analytics/shippers/elastic_v3/server/npm_module_types": +"@types/kbn__analytics-shippers-elastic-v3-common@link:bazel-bin/packages/analytics/shippers/elastic_v3/common/npm_module_types": version "0.0.0" uid "" -"@types/kbn__analytics-shippers-elastic-v3-browser@link:bazel-bin/packages/analytics/shippers/elastic_v3/browser/npm_module_types": +"@types/kbn__analytics-shippers-elastic-v3-server@link:bazel-bin/packages/analytics/shippers/elastic_v3/server/npm_module_types": version "0.0.0" uid "" @@ -6223,6 +6204,10 @@ version "0.0.0" uid "" +"@types/kbn__performance-testing-dataset-extractor@link:bazel-bin/packages/kbn-performance-testing-dataset-extractor/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__plugin-discovery@link:bazel-bin/packages/kbn-plugin-discovery/npm_module_types": version "0.0.0" uid "" @@ -8714,11 +8699,6 @@ axe-core@^4.2.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5" integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== -axios-curlirize@1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/axios-curlirize/-/axios-curlirize-1.3.7.tgz#0153c51a5af0e92370169daea33f234d588baad1" - integrity sha512-csSsuMyZj1dv1fL0zRPnDAHWrmlISMvK+wx9WJI/igRVDT4VMgbf2AVenaHghFLfI1nQijXUevYEguYV6u5hjA== - axios@^0.21.1: version "0.21.1" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" @@ -8740,13 +8720,6 @@ axios@^0.25.0: dependencies: follow-redirects "^1.14.7" -axios@^0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" - integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== - dependencies: - follow-redirects "^1.14.8" - axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -14793,7 +14766,7 @@ follow-redirects@1.12.1: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6" integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg== -follow-redirects@^1.0.0, follow-redirects@^1.10.0, follow-redirects@^1.14.4, follow-redirects@^1.14.7, follow-redirects@^1.14.8: +follow-redirects@^1.0.0, follow-redirects@^1.10.0, follow-redirects@^1.14.4, follow-redirects@^1.14.7: version "1.14.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== @@ -16254,7 +16227,7 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" -hpagent@^0.1.1, hpagent@^0.1.2: +hpagent@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9" integrity sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ== @@ -23516,13 +23489,6 @@ qs@^6.10.0: dependencies: side-channel "^1.0.4" -qs@^6.10.3: - version "6.10.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" - integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== - dependencies: - side-channel "^1.0.4" - qs@^6.7.0: version "6.9.4" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" @@ -30698,19 +30664,6 @@ yargs@^17.0.1, yargs@^17.2.1, yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.0.0" -yargs@^17.4.0: - version "17.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.1.tgz#ebe23284207bb75cee7c408c33e722bfb27b5284" - integrity sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.0.0" - yargs@^3.15.0: version "3.32.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" From bb4aa706b801f3028d812b90d04ef7c0f656c5d3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 May 2022 23:48:39 -0400 Subject: [PATCH 43/83] Update dependency elastic-apm-node to ^3.33.0 (#131680) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 21 ++++----------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 69db4553679523..2f0a37ff477355 100644 --- a/package.json +++ b/package.json @@ -252,7 +252,7 @@ "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "del": "^5.1.0", - "elastic-apm-node": "^3.32.0", + "elastic-apm-node": "^3.33.0", "email-addresses": "^5.0.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", diff --git a/yarn.lock b/yarn.lock index 6eddaa5c415094..439a288a9db598 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13041,10 +13041,10 @@ elastic-apm-http-client@11.0.1: semver "^6.3.0" stream-chopper "^3.0.1" -elastic-apm-node@^3.32.0: - version "3.32.0" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.32.0.tgz#fdad3c03aabc4e7994b4155b031f68d9774af49a" - integrity sha512-6vOe1FZv5toCouuyfiXZuWNE1+1fim9zvsv7H56BKRYa7xQ3X1fxq7QAP2gLd/Z9zvSDLGNXS4DPE1eqX1A1Jw== +elastic-apm-node@^3.33.0: + version "3.33.0" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.33.0.tgz#ad2850b005355299c3a9fdc631875162480ceb15" + integrity sha512-ZJKcRbYdEU87MWyB9CczGTLEERA595OWd0nqpxrDBkogogpxwpzCOialKZA+bekpNA0Oa4Sv18zRCdyqpV25Pw== dependencies: "@elastic/ecs-pino-format" "^1.2.0" after-all-results "^2.0.0" @@ -13076,7 +13076,6 @@ elastic-apm-node@^3.32.0: shallow-clone-shim "^2.0.0" source-map "^0.8.0-beta.0" sql-summary "^1.0.1" - traceparent "^1.0.0" traverse "^0.6.6" unicode-byte-truncate "^1.0.0" @@ -23609,11 +23608,6 @@ random-bytes@~1.0.0: resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= -random-poly-fill@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/random-poly-fill/-/random-poly-fill-1.0.1.tgz#13634dc0255a31ecf85d4a182d92c40f9bbcf5ed" - integrity sha512-bMOL0hLfrNs52+EHtIPIXxn2PxYwXb0qjnKruTjXiM/sKfYqj506aB2plFwWW1HN+ri724bAVVGparh4AtlJKw== - random-word-slugs@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/random-word-slugs/-/random-word-slugs-0.0.5.tgz#6ccd6c7ea320be9fbc19507f8c3a7d4a970ff61f" @@ -28119,13 +28113,6 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= -traceparent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/traceparent/-/traceparent-1.0.0.tgz#9b14445cdfe5c19f023f1c04d249c3d8e003a5ce" - integrity sha512-b/hAbgx57pANQ6cg2eBguY3oxD6FGVLI1CC2qoi01RmHR7AYpQHPXTig9FkzbWohEsVuHENZHP09aXuw3/LM+w== - dependencies: - random-poly-fill "^1.0.1" - traverse@^0.6.6, traverse@~0.6.6: version "0.6.6" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" From 0d4cc4dc326f1475070862c0fb11414883768ef5 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 6 May 2022 10:10:01 +0300 Subject: [PATCH 44/83] [Unified search] Improves the current filter/search experience (#128401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Unified search] Moves dataview picker to the search bar (#126560) * [Unified search] Moves dataview picker to the search bar * alter texts * Remove unused file * [ChangeDataView] Design cleanup * Fix services mock failure * Show newly created datavuew on the list * Keep dataview picker in discover in mobile view * Cleanup * Cleanup translations * Fix some discover FTs * Fix management FTs * More test fixes * Added a dismissible tour * Pulled the selectabl list into a new component … for reuse Called `DataViewsList`. I then changed Lens’ config panel’s own EuiSelectableList to use this new component instead. *Didn’t do any test updates* * Fix broken jest test * Use the same picker component on Discover mobile view * Apply some CI fixes * Fix more functional tests * More FTs fixes * Close the tour popover for lens tests * More FTs fixes * Fix lens FTs * Using new `styles.ts` pattern for custom styles and allowing for `fullWidth` buttons * Better tour text and i18n * Update copy * No exclamation point * Cleanup * Fixes on discover tests * Fixes on Lens Fts - create runtime fields * Fixes on edit permission of add field in discover and some FTs fixes * Further Fts fixes * More FTs fixes * Made tour opt-in with `showNewMenuTour` * Refactor the OSS FTs to change less files * Further cleanup on the FTs * Remove unecessary action * Fix dataview creation bug * Add a unit test to the new component * More fixes * Fix OSS a11y tests * Adds another unit test for Lens permissions * Make a change to stabilize the tests * Clear flyout prop as it is not used anymore * Deisgn fixes for mobile view * Address PR comments WIP * Update the layrpanl dataview list when a new dataview is created Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: cchaos Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> * fix test * Fix CI * Fix i18n checks * [Unified search] Redesign search bar (#126566) * [Unified search] Redesign search bar * Changes to the saved queries design * Remove `globalQueryBar` from nested under `.kbnTopNavMenu__wrapper` * [Security Solution] Removing overrides of `.globaQueryBar` * Cleanup of `.globalQueryBar` styles and variants * General layout fixes/adjustments * More general layout fixes and better defaults * Enable Clear all button if saved query is applied * Clear should be enabled if searched query is applied but not the save as new * fix some bugs on enabling and disabling the menu options * [design] Working on Load Filter set menu * Indicate selection even before the user clicks apply * prettify the time text * Add delete saved qquery confirmation modal * Some cleaning up * Fix the manage saved queries link * Enable load filter set if saved query is created for the first time * Some fixes on linting and on translations * Clanup scss * Make some fixes * Fix navigatation unit test * Fix translations check * Fix rest jest tests * Fix some functional tests * Fix checks * Fixes the appearance of the searchbar components depending on the flags * Fix OSS tests * Fixes on tests and query bar logic * Fix the OSS ally tests * Fix graph and lens KQL bar display problems, apply saveQuery flag correctly * Fix more tests * Fix security solution test * Undo tweaks for graph * Better fix for Graph - Emulate new look, not old - Uses same context menu panel as the new one - Provides the same button type (filter button) * Fix checks and jest test * Further fixes * Fix language switcher test * Hopefully fixes the rest of the solution tests * Cleanup * Fixes on maps filtering * Fix Discover padding * [Discover] Added New Field button to bottom of fields list * Simple responsive behavior * Quick filter updates * CI fixes * area-label fix * 18n translate the add filed btn in discover * Fix lens functional test * fix functional tests * Refactor and add unit test for the add filter button * Add unit test to the saved management list component * Stabilize lens test * Second try to solve the flakiness * Fix panels depending on the flags * Fix jest tests * Fix double menu when only the queryInput is on * Add storybook for the different layouts of the unified search bar * Fix ci checks * Removee hooks comment * Fix flakiness * Fix problem with lens dataview change * Fix checks * Ensure that the query is submitted * Fixes oon CI * Fixes flakiness * Fix checks CI * Update texts * Cleanup * Fix unskipped funtional test * Fix FT * Update src/plugins/unified_search/public/saved_query_form/save_query_form.tsx Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Fix eslint * Fix checks Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: cchaos Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Fix jest * Fix jest tests * Stabilize discover security tests * [Unified search] Further enhancements (#129877) * [Unified search] Redesign search bar * Changes to the saved queries design * Remove `globalQueryBar` from nested under `.kbnTopNavMenu__wrapper` * [Security Solution] Removing overrides of `.globaQueryBar` * Cleanup of `.globalQueryBar` styles and variants * General layout fixes/adjustments * More general layout fixes and better defaults * Enable Clear all button if saved query is applied * Clear should be enabled if searched query is applied but not the save as new * fix some bugs on enabling and disabling the menu options * [design] Working on Load Filter set menu * Indicate selection even before the user clicks apply * prettify the time text * Add delete saved qquery confirmation modal * Some cleaning up * Fix the manage saved queries link * Enable load filter set if saved query is created for the first time * Some fixes on linting and on translations * Clanup scss * Make some fixes * Fix navigatation unit test * Fix translations check * Fix rest jest tests * Fix some functional tests * Fix checks * Fixes the appearance of the searchbar components depending on the flags * Fix OSS tests * Fixes on tests and query bar logic * Fix the OSS ally tests * Fix graph and lens KQL bar display problems, apply saveQuery flag correctly * Fix more tests * Fix security solution test * Undo tweaks for graph * Better fix for Graph - Emulate new look, not old - Uses same context menu panel as the new one - Provides the same button type (filter button) * Fix checks and jest test * Further fixes * Fix language switcher test * Hopefully fixes the rest of the solution tests * Cleanup * Fixes on maps filtering * Fix Discover padding * [Discover] Added New Field button to bottom of fields list * Simple responsive behavior * Quick filter updates * CI fixes * area-label fix * 18n translate the add filed btn in discover * Fix lens functional test * fix functional tests * Refactor and add unit test for the add filter button * Add unit test to the saved management list component * Stabilize lens test * Second try to solve the flakiness * Fix panels depending on the flags * Fix jest tests * Fix double menu when only the queryInput is on * Add storybook for the different layouts of the unified search bar * Fix ci checks * Removee hooks comment * Fix flakiness * Fix problem with lens dataview change * Fix checks * Ensure that the query is submitted * Fixes oon CI * Fixes flakiness * Fix checks CI * Update texts * Cleanup * Fix unskipped funtional test * Fix FT * Fixes filter bar responsiveness * Add filter button option to the main menu * Filter form design pass - Added tooltip around “Add filter” icon button - Fixed padding - Stubbed in “Add” vs “Edit” labels * Query menu icon button tooltip * ‘Update’ not ‘Edit’ button label * Apply different strings for adding filter mode vs edit * Hide Manage SO link if the user donesnt have the permissions * Fix the wrapping of the badges and datepicker badge * Minor props naming fix * Fix checks * Fix mobile * Cleanup * Fix checks * Faux button group * Changes on the search input placeholder * Fix CI Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: cchaos Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> * Fix translations * Fix security solution add filter bug * Filter bar / items: Reduce to only necessary wrappers and styles - Switch to Emotion for styling - Rename `FilterBadgesWrapper` to `FilterItems` - Move `FilterItems` and `FilterItem` so own folder `filter_item/` # Conflicts: # src/plugins/unified_search/public/filter_bar/filter_bar.tsx # src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx # src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx # src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx # src/plugins/unified_search/public/filter_bar/filter_view/index.tsx # src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx # src/plugins/unified_search/public/search_bar/search_bar.tsx * Fix styles by removing styled-components Also added prop `afterQueryBar` to signify that it needs the top margin # Conflicts: # packages/kbn-babel-preset/styled_components_files.js * Refactor `SearchBar` and `QueryBarTopRow` a bit - Added `showSubmitButton` to explicitely hide the submit button (defaults to true) - SearchBar always renders the `QueryBarTopRow` which now fully controls visibility of the pieces - Renamed class with new `uni` prefix - Tried and failed to use Emotion * Update usages # Conflicts: # x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx * Fix a test * Fix jest test * Attaches filter menu when no add button and better dropdown styles * Fix lint errors * Clearing the query and filters of a saved query should unload the saved query * Do not allow to update the loaded saved query if no filters or no query * Fix Graph by creating reusable `KibanaFilterButtonGroup` * Use EuiTheme for SearchBar styles Updated `displayStyle` for Lens & Discover * Fix displayStyle prop problem * Correct alignment of data visualizer * Super nit comment * Data visualizer improvements * Apply useMemo in editPermission cont in Discover * Fix translations checks * Fix translations checks * Fix filter badge popover bug * [Submit button] Changed to EuiSuperUpdateButton - Quickly adds tooltip content and filled state for more prominence when dirty - Fixed 18n * Fix tests * Align the position with the other tolltips and check that functional tests succeed * Give some time to the tooltip to disappear * Fix tests * Fix CI checks * Address PR comments * Move constants outside render * Apply PR comments * Fix dataview picker glich * lovercase emotion function * Fix a11y problem on dataview picker * Lowercase rest of emotion style functions * Clean up Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: cchaos Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- .../steps/storybooks/build_and_upload.js | 1 + .../styled_components_files.js | 2 +- src/dev/storybook/aliases.ts | 1 + .../application/top_nav/dashboard_top_nav.tsx | 2 +- src/plugins/data/public/types.ts | 1 + .../scripting_help/test_script.scss | 5 - .../components/scripting_help/test_script.tsx | 5 +- src/plugins/discover/kibana.json | 2 +- .../application/context/context_app.test.tsx | 3 +- .../application/context/context_app.tsx | 3 +- .../components/layout/discover_layout.scss | 5 +- .../components/layout/discover_layout.tsx | 5 +- .../discover_index_pattern.test.tsx.snap | 3 - ...ver_index_pattern_management.test.tsx.snap | 735 ------------------ .../sidebar/change_indexpattern.test.tsx | 71 -- .../sidebar/change_indexpattern.tsx | 109 --- .../sidebar/discover_index_pattern.test.tsx | 95 --- .../sidebar/discover_index_pattern.tsx | 74 -- ...discover_index_pattern_management.test.tsx | 118 --- .../discover_index_pattern_management.tsx | 130 ---- .../components/sidebar/discover_sidebar.scss | 4 +- .../components/sidebar/discover_sidebar.tsx | 83 +- .../sidebar/discover_sidebar_responsive.tsx | 47 +- .../top_nav/discover_topnav.test.tsx | 2 + .../components/top_nav/discover_topnav.tsx | 91 ++- .../main/discover_main_app.test.tsx | 7 +- src/plugins/discover/tsconfig.json | 1 + .../public/top_nav_menu/top_nav_menu.test.tsx | 1 - .../public/top_nav_menu/top_nav_menu.tsx | 8 +- src/plugins/unified_search/.storybook/main.js | 9 + .../public/__stories__/search_bar.stories.tsx | 431 ++++++++++ .../dataview_picker/change_dataview.styles.ts | 20 + .../dataview_picker/change_dataview.test.tsx | 138 ++++ .../dataview_picker/change_dataview.tsx | 244 ++++++ .../dataview_picker/dataview_list.test.tsx | 71 ++ .../public/dataview_picker/dataview_list.tsx | 76 ++ .../public/dataview_picker/index.tsx | 52 ++ .../filter_bar/_global_filter_group.scss | 54 -- .../public/filter_bar/_index.scss | 3 - .../public/filter_bar/filter_bar.styles.ts | 22 + .../public/filter_bar/filter_bar.tsx | 227 +----- .../filter_button_group.scss | 24 + .../filter_button_group.tsx | 47 ++ .../public/filter_bar/filter_editor/index.tsx | 89 ++- .../filter_editor/lib/filter_label.tsx | 2 +- .../{ => filter_item}/_variables.scss | 0 .../filter_item.scss} | 20 +- .../{ => filter_item}/filter_item.tsx | 15 +- .../filter_bar/filter_item/filter_items.tsx | 86 ++ .../public/filter_bar/filter_options.tsx | 176 ----- .../public/filter_bar/filter_view/index.tsx | 2 +- .../public/filter_bar/index.tsx | 9 +- src/plugins/unified_search/public/index.scss | 2 - src/plugins/unified_search/public/index.ts | 2 + src/plugins/unified_search/public/plugin.ts | 3 - .../public/query_string_input/_query_bar.scss | 97 +-- .../query_string_input/add_filter_popover.tsx | 80 ++ .../filter_editor_wrapper.tsx | 88 +++ .../language_switcher.test.tsx | 53 +- .../query_string_input/language_switcher.tsx | 158 ++-- .../query_bar_menu.test.tsx | 278 +++++++ .../query_string_input/query_bar_menu.tsx | 186 +++++ .../query_bar_menu_panels.tsx | 494 ++++++++++++ .../query_string_input/query_bar_top_row.tsx | 226 ++++-- .../query_string_input.test.tsx | 1 - .../query_string_input/query_string_input.tsx | 81 +- .../saved_query_form/save_query_form.tsx | 94 +-- .../public/saved_query_management/_index.scss | 3 +- .../_saved_query_list_item.scss | 21 - ...scss => _saved_query_management_list.scss} | 9 - .../public/saved_query_management/index.ts | 2 +- .../saved_query_list_item.tsx | 154 ---- .../saved_query_management_component.tsx | 340 -------- .../saved_query_management_list.test.tsx | 165 ++++ .../saved_query_management_list.tsx | 385 +++++++++ .../public/search_bar/create_search_bar.tsx | 3 +- .../public/search_bar/search_bar.styles.ts | 24 + .../public/search_bar/search_bar.test.tsx | 116 +-- .../public/search_bar/search_bar.tsx | 241 +++--- .../public/typeahead/_suggestion.scss | 5 +- .../typeahead/suggestions_component.tsx | 28 +- .../vis_type_markdown/public/markdown_vis.ts | 2 + .../timelion/public/timelion_vis_type.tsx | 3 +- .../public/vis_types/base_vis_type.ts | 1 + .../visualizations/public/vis_types/types.ts | 1 + .../components/visualize_top_nav.tsx | 3 +- test/accessibility/apps/discover.ts | 8 +- test/accessibility/apps/filter_panel.ts | 35 +- .../apps/dashboard/group1/embed_mode.ts | 3 +- .../dashboard/group2/dashboard_saved_query.ts | 29 +- .../apps/dashboard/group3/dashboard_state.ts | 3 +- .../controls/options_list.ts | 5 +- .../_indexpattern_without_timefield.ts | 3 +- .../apps/discover/_saved_queries.ts | 36 +- test/functional/apps/home/_navigation.ts | 3 +- .../apps/visualize/group2/_gauge_chart.ts | 3 +- .../apps/visualize/group6/_vega_chart.ts | 5 +- test/functional/page_objects/common_page.ts | 1 + test/functional/page_objects/discover_page.ts | 16 +- test/functional/page_objects/index.ts | 2 + .../page_objects/unified_search_page.ts | 26 + .../functional/page_objects/visualize_page.ts | 5 + .../services/dashboard/visualizations.ts | 2 + test/functional/services/filter_bar.ts | 4 +- test/functional/services/query_bar.ts | 16 +- .../saved_query_management_component.ts | 60 +- .../components/search_panel/search_panel.scss | 8 +- .../components/search_panel/search_panel.tsx | 1 - .../graph/public/components/_search_bar.scss | 9 +- .../public/components/search_bar.test.tsx | 6 +- .../graph/public/components/search_bar.tsx | 88 +-- x-pack/plugins/lens/kibana.json | 1 + .../plugins/lens/public/app_plugin/app.scss | 4 - .../lens/public/app_plugin/app.test.tsx | 69 ++ .../lens/public/app_plugin/lens_top_nav.tsx | 151 +++- .../lens/public/app_plugin/mounter.tsx | 2 + .../plugins/lens/public/app_plugin/types.ts | 4 + .../change_indexpattern.tsx | 47 +- .../datapanel.test.tsx | 95 --- .../indexpattern_datasource/datapanel.tsx | 98 +-- .../indexpattern_datasource/indexpattern.tsx | 29 + .../layerpanel.test.tsx | 10 +- .../indexpattern_datasource/layerpanel.tsx | 1 - .../lens/public/mocks/datasource_mock.ts | 1 + .../lens/public/mocks/services_mock.tsx | 4 + x-pack/plugins/lens/public/plugin.ts | 2 + x-pack/plugins/lens/public/types.ts | 9 + x-pack/plugins/lens/public/utils.ts | 54 +- x-pack/plugins/lens/tsconfig.json | 15 +- .../security_solution/cypress/tasks/alerts.ts | 16 +- .../cypress/tasks/hosts/authentications.ts | 2 +- .../cypress/tasks/hosts/uncommon_processes.ts | 2 +- .../cypress/tasks/security_header.ts | 4 +- .../filters_global.test.tsx.snap | 2 +- .../filters_global/filters_global.tsx | 2 +- .../components/query_bar/index.test.tsx | 12 +- .../common/components/query_bar/index.tsx | 8 +- .../common/components/search_bar/index.tsx | 11 +- .../components/rules/query_bar/index.tsx | 19 +- .../view/components/search_bar.tsx | 11 +- .../components/timeline/query_bar/index.tsx | 1 + .../search_or_filter/search_or_filter.tsx | 15 +- .../expression/read_only_filter_items.tsx | 8 +- .../translations/translations/fr-FR.json | 247 +++--- .../translations/translations/ja-JP.json | 35 +- .../translations/translations/zh-CN.json | 35 +- .../apps/canvas/embeddables/lens.ts | 3 +- .../drilldowns/explore_data_panel_action.ts | 2 +- .../feature_controls/dashboard_security.ts | 7 + .../feature_controls/discover_security.ts | 5 + .../functional/apps/lens/group2/dashboard.ts | 2 + .../apps/lens/group2/show_underlying_data.ts | 2 +- .../group2/show_underlying_data_dashboard.ts | 7 +- .../apps/lens/group3/disable_auto_apply.ts | 1 + .../apps/lens/group3/lens_tagging.ts | 2 + .../group1/feature_controls/maps_security.ts | 12 - .../feature_controls/visualize_security.ts | 9 +- .../test/functional/page_objects/lens_page.ts | 20 +- .../services/ml/dashboard_embeddables.ts | 4 +- .../functional/services/transform/discover.ts | 2 +- .../functional/services/transform/wizard.ts | 3 +- 161 files changed, 4559 insertions(+), 3705 deletions(-) delete mode 100644 src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.scss delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.test.tsx delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.tsx delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.test.tsx delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.tsx delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx delete mode 100644 src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx create mode 100644 src/plugins/unified_search/.storybook/main.js create mode 100644 src/plugins/unified_search/public/__stories__/search_bar.stories.tsx create mode 100644 src/plugins/unified_search/public/dataview_picker/change_dataview.styles.ts create mode 100644 src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx create mode 100644 src/plugins/unified_search/public/dataview_picker/change_dataview.tsx create mode 100644 src/plugins/unified_search/public/dataview_picker/dataview_list.test.tsx create mode 100644 src/plugins/unified_search/public/dataview_picker/dataview_list.tsx create mode 100644 src/plugins/unified_search/public/dataview_picker/index.tsx delete mode 100644 src/plugins/unified_search/public/filter_bar/_global_filter_group.scss delete mode 100644 src/plugins/unified_search/public/filter_bar/_index.scss create mode 100644 src/plugins/unified_search/public/filter_bar/filter_bar.styles.ts create mode 100644 src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.scss create mode 100644 src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.tsx rename src/plugins/unified_search/public/filter_bar/{ => filter_item}/_variables.scss (100%) rename src/plugins/unified_search/public/filter_bar/{_global_filter_item.scss => filter_item/filter_item.scss} (82%) rename src/plugins/unified_search/public/filter_bar/{ => filter_item}/filter_item.tsx (97%) create mode 100644 src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx delete mode 100644 src/plugins/unified_search/public/filter_bar/filter_options.tsx create mode 100644 src/plugins/unified_search/public/query_string_input/add_filter_popover.tsx create mode 100644 src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx create mode 100644 src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx create mode 100644 src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx create mode 100644 src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx delete mode 100644 src/plugins/unified_search/public/saved_query_management/_saved_query_list_item.scss rename src/plugins/unified_search/public/saved_query_management/{_saved_query_management_component.scss => _saved_query_management_list.scss} (76%) delete mode 100644 src/plugins/unified_search/public/saved_query_management/saved_query_list_item.tsx delete mode 100644 src/plugins/unified_search/public/saved_query_management/saved_query_management_component.tsx create mode 100644 src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx create mode 100644 src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx create mode 100644 src/plugins/unified_search/public/search_bar/search_bar.styles.ts create mode 100644 test/functional/page_objects/unified_search_page.ts diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.js b/.buildkite/scripts/steps/storybooks/build_and_upload.js index becb8f1bd871f1..c541f595487536 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.js +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.js @@ -38,6 +38,7 @@ const STORYBOOKS = [ 'security_solution', 'shared_ux', 'ui_actions_enhanced', + 'unified_search', ]; const GITHUB_CONTEXT = 'Build and Publish Storybooks'; diff --git a/packages/kbn-babel-preset/styled_components_files.js b/packages/kbn-babel-preset/styled_components_files.js index a8b1234a406fd9..53052809b6b2f6 100644 --- a/packages/kbn-babel-preset/styled_components_files.js +++ b/packages/kbn-babel-preset/styled_components_files.js @@ -13,7 +13,7 @@ module.exports = { */ USES_STYLED_COMPONENTS: [ /packages[\/\\]kbn-ui-shared-deps-(npm|src)[\/\\]/, - /src[\/\\]plugins[\/\\](unified_search|kibana_react)[\/\\]/, + /src[\/\\]plugins[\/\\](kibana_react)[\/\\]/, /x-pack[\/\\]plugins[\/\\](apm|beats_management|cases|fleet|infra|lists|observability|osquery|security_solution|timelines|synthetics|ux)[\/\\]/, /x-pack[\/\\]test[\/\\]plugin_functional[\/\\]plugins[\/\\]resolver_test[\/\\]/, ], diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 4167719d3bb314..45b8aad7df8cfd 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -8,6 +8,7 @@ // Please also add new aliases to test/scripts/jenkins_storybook.sh export const storybookAliases = { + unified_search: 'src/plugins/unified_search/.storybook', coloring: 'packages/kbn-coloring/.storybook', apm: 'x-pack/plugins/apm/.storybook', canvas: 'x-pack/plugins/canvas/storybook', diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 5907c2caff1055..7095ad34cd1897 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -491,7 +491,7 @@ export function DashboardTopNav({ const showQueryInput = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowQueryInput)); const showDatePicker = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowDatePicker)); const showFilterBar = shouldShowFilterBar(Boolean(embedSettings?.forceHideFilterBar)); - const showQueryBar = showQueryInput || showDatePicker; + const showQueryBar = showQueryInput || showDatePicker || showFilterBar; const showSearchBar = showQueryBar || showFilterBar; const screenTitle = dashboardState.title; diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 69c2035b28e5ca..58b66bb74b5e3c 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -108,6 +108,7 @@ export interface IDataPluginServices extends Partial { uiSettings: CoreStart['uiSettings']; savedObjects: CoreStart['savedObjects']; notifications: CoreStart['notifications']; + application: CoreStart['application']; http: CoreStart['http']; storage: IStorageWrapper; data: DataPublicPluginStart; diff --git a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.scss b/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.scss deleted file mode 100644 index ca230711827dc7..00000000000000 --- a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.scss +++ /dev/null @@ -1,5 +0,0 @@ -.testScript__searchBar { - .globalQueryBar { - padding: $euiSize 0 0; - } -} diff --git a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.tsx b/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.tsx index 52bf8823316989..0eb0898f41b60a 100644 --- a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.tsx +++ b/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.tsx @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import './test_script.scss'; - import React, { Component, Fragment } from 'react'; import { @@ -223,8 +221,11 @@ export class TestScript extends Component { /> + +
{ const topNavProps = { appName: 'context', showSearchBar: true, - showQueryBar: false, + showQueryBar: true, + showQueryInput: false, showFilterBar: true, showSaveQuery: false, showDatePicker: false, diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index e84bbf644a8951..1f886fdacac6b9 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -133,7 +133,8 @@ export const ContextApp = ({ indexPattern, anchorId }: ContextAppProps) => { return { appName: 'context', showSearchBar: true, - showQueryBar: false, + showQueryBar: true, + showQueryInput: false, showFilterBar: true, showSaveQuery: false, showDatePicker: false, diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss index 1d074c002e340c..9ea41f343b885a 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss @@ -29,15 +29,14 @@ discover-app { .dscPageBody__contents { overflow: hidden; - padding-top: $euiSizeXS / 2; // A little breathing room for the index pattern button } .dscPageContent__wrapper { - padding: 0 $euiSize $euiSize 0; + padding: $euiSizeS $euiSizeS $euiSizeS 0; overflow: hidden; // Ensures horizontal scroll of table @include euiBreakpoint('xs', 's') { - padding: 0 $euiSize $euiSize; + padding: 0 $euiSizeS $euiSizeS; } } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index ad1c96e308d12d..6cbc8add99c39e 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -226,6 +226,9 @@ export function DiscoverLayout({ stateContainer={stateContainer} updateQuery={onUpdateQuery} resetSavedSearch={resetSavedSearch} + onChangeIndexPattern={onChangeIndexPattern} + onEditRuntimeField={onEditRuntimeField} + useNewFieldsApi={useNewFieldsApi} /> - + - - - } - closePopover={[Function]} - data-test-subj="discover-addRuntimeField-popover" - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
- -`; diff --git a/src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.test.tsx deleted file mode 100644 index a5e93c1d895bce..00000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import React from 'react'; -import { EuiSelectable } from '@elastic/eui'; -import { ShallowWrapper } from 'enzyme'; -import { act } from 'react-dom/test-utils'; -import { shallowWithIntl } from '@kbn/test-jest-helpers'; -import { ChangeIndexPattern } from './change_indexpattern'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; -import { IndexPatternRef } from './types'; - -function getProps() { - return { - indexPatternId: indexPatternMock.id, - indexPatternRefs: [ - indexPatternMock as IndexPatternRef, - indexPatternWithTimefieldMock as IndexPatternRef, - ], - onChangeIndexPattern: jest.fn(), - trigger: { - label: indexPatternMock.title, - title: indexPatternMock.title, - 'data-test-subj': 'indexPattern-switch-link', - }, - }; -} - -function getIndexPatternPickerList(instance: ShallowWrapper) { - return instance.find(EuiSelectable).first(); -} - -function getIndexPatternPickerOptions(instance: ShallowWrapper) { - return getIndexPatternPickerList(instance).prop('options'); -} - -export function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { - const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions( - instance - ).map((option: { label: string }) => - option.label === selectedLabel - ? { ...option, checked: 'on' } - : { ...option, checked: undefined } - ); - return getIndexPatternPickerList(instance).prop('onChange')!(options); -} - -describe('ChangeIndexPattern', () => { - test('switching index pattern to the same index pattern does not trigger onChangeIndexPattern', async () => { - const props = getProps(); - const comp = shallowWithIntl(); - await act(async () => { - selectIndexPatternPickerOption(comp, indexPatternMock.title); - }); - expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(0); - }); - test('switching index pattern to a different index pattern triggers onChangeIndexPattern', async () => { - const props = getProps(); - const comp = shallowWithIntl(); - await act(async () => { - selectIndexPatternPickerOption(comp, indexPatternWithTimefieldMock.title); - }); - expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(1); - expect(props.onChangeIndexPattern).toHaveBeenCalledWith(indexPatternWithTimefieldMock.id); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.tsx b/src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.tsx deleted file mode 100644 index ceee905cff6fa0..00000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.tsx +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { - EuiButton, - EuiPopover, - EuiPopoverTitle, - EuiSelectable, - EuiButtonProps, -} from '@elastic/eui'; -import { EuiSelectableProps } from '@elastic/eui/src/components/selectable/selectable'; -import { IndexPatternRef } from './types'; - -export type ChangeIndexPatternTriggerProps = EuiButtonProps & { - label: string; - title?: string; -}; - -// TODO: refactor to shared component with ../../../../../../../../x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern - -export function ChangeIndexPattern({ - indexPatternId, - indexPatternRefs, - onChangeIndexPattern, - selectableProps, - trigger, -}: { - indexPatternId?: string; - indexPatternRefs: IndexPatternRef[]; - onChangeIndexPattern: (newId: string) => void; - selectableProps?: EuiSelectableProps<{ value: string }>; - trigger: ChangeIndexPatternTriggerProps; -}) { - const [isPopoverOpen, setPopoverIsOpen] = useState(false); - - const createTrigger = function () { - const { label, title, ...rest } = trigger; - return ( - setPopoverIsOpen(!isPopoverOpen)} - {...rest} - > - {label} - - ); - }; - - return ( - setPopoverIsOpen(false)} - display="block" - panelPaddingSize="s" - > -
- - {i18n.translate('discover.fieldChooser.indexPattern.changeDataViewTitle', { - defaultMessage: 'Change data view', - })} - - - data-test-subj="indexPattern-switcher" - {...selectableProps} - searchable - singleSelection="always" - options={indexPatternRefs.map(({ title, id }) => ({ - label: title, - key: id, - value: id, - checked: id === indexPatternId ? 'on' : undefined, - }))} - onChange={(choices) => { - const choice = choices.find(({ checked }) => checked) as unknown as { - value: string; - }; - if (choice.value !== indexPatternId) { - onChangeIndexPattern(choice.value); - } - setPopoverIsOpen(false); - }} - searchProps={{ - compressed: true, - ...(selectableProps ? selectableProps.searchProps : undefined), - }} - > - {(list, search) => ( - <> - {search} - {list} - - )} - -
-
- ); -} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.test.tsx deleted file mode 100644 index d640e2fa115947..00000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.test.tsx +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { shallowWithIntl as shallow } from '@kbn/test-jest-helpers'; -import { ShallowWrapper } from 'enzyme'; -import { ChangeIndexPattern } from './change_indexpattern'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObject } from '@kbn/core/server'; -import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern'; -import { EuiSelectable } from '@elastic/eui'; -import type { DataView, DataViewAttributes } from '@kbn/data-views-plugin/public'; -import { indexPatternsMock } from '../../../../__mocks__/index_patterns'; - -const indexPattern = { - id: 'the-index-pattern-id-first', - title: 'test1 title', -} as DataView; - -const indexPattern1 = { - id: 'the-index-pattern-id-first', - attributes: { - title: 'test1 title', - }, -} as SavedObject; - -const indexPattern2 = { - id: 'the-index-pattern-id', - attributes: { - title: 'test2 title', - }, -} as SavedObject; - -const defaultProps = { - indexPatternList: [indexPattern1, indexPattern2], - selectedIndexPattern: indexPattern, - useNewFieldsApi: true, - indexPatterns: indexPatternsMock, - onChangeIndexPattern: jest.fn(), -}; - -function getIndexPatternPickerList(instance: ShallowWrapper) { - return instance.find(ChangeIndexPattern).first().dive().find(EuiSelectable); -} - -function getIndexPatternPickerOptions(instance: ShallowWrapper) { - return getIndexPatternPickerList(instance).prop('options'); -} - -function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { - const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions( - instance - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ).map((option: any) => - option.label === selectedLabel - ? { ...option, checked: 'on' } - : { ...option, checked: undefined } - ); - return getIndexPatternPickerList(instance).prop('onChange')!(options); -} - -describe('DiscoverIndexPattern', () => { - test('Invalid props dont cause an exception', () => { - const props = { - indexPatternList: null, - selectedIndexPattern: null, - onChangeIndexPattern: jest.fn(), - } as unknown as DiscoverIndexPatternProps; - - expect(shallow()).toMatchSnapshot(`""`); - }); - test('should list all index patterns', () => { - const instance = shallow(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(getIndexPatternPickerOptions(instance)!.map((option: any) => option.label)).toEqual([ - 'test1 title', - 'test2 title', - ]); - }); - - test('should switch data panel to target index pattern', async () => { - const instance = shallow(); - await act(async () => { - selectIndexPatternPickerOption(instance, 'test2 title'); - }); - expect(defaultProps.onChangeIndexPattern).toHaveBeenCalledWith('the-index-pattern-id'); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.tsx deleted file mode 100644 index 83aa3ce478215c..00000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useState, useEffect } from 'react'; -import { SavedObject } from '@kbn/core/public'; -import type { DataView, DataViewAttributes } from '@kbn/data-views-plugin/public'; -import { IndexPatternRef } from './types'; -import { ChangeIndexPattern } from './change_indexpattern'; - -export interface DiscoverIndexPatternProps { - /** - * list of available index patterns, if length > 1, component offers a "change" link - */ - indexPatternList: Array>; - /** - * Callback function when changing an index pattern - */ - onChangeIndexPattern: (id: string) => void; - /** - * currently selected index pattern - */ - selectedIndexPattern: DataView; -} - -/** - * Component allows you to select an index pattern in discovers side bar - */ -export function DiscoverIndexPattern({ - indexPatternList, - onChangeIndexPattern, - selectedIndexPattern, -}: DiscoverIndexPatternProps) { - const options: IndexPatternRef[] = (indexPatternList || []).map((entity) => ({ - id: entity.id, - title: entity.attributes!.title, - })); - const { id: selectedId, title: selectedTitle } = selectedIndexPattern || {}; - - const [selected, setSelected] = useState({ - id: selectedId, - title: selectedTitle || '', - }); - useEffect(() => { - const { id, title } = selectedIndexPattern; - setSelected({ id, title }); - }, [selectedIndexPattern]); - if (!selectedId) { - return null; - } - - return ( - { - const indexPattern = options.find((pattern) => pattern.id === id); - if (indexPattern) { - onChangeIndexPattern(id); - setSelected(indexPattern); - } - }} - /> - ); -} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx deleted file mode 100644 index cddbe087030e7c..00000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers'; -import { EuiContextMenuPanel, EuiPopover, EuiContextMenuItem } from '@elastic/eui'; -import { DiscoverServices } from '../../../../build_services'; -import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; -import { stubLogstashIndexPattern } from '@kbn/data-plugin/common/stubs'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; - -const mockServices = { - history: () => ({ - location: { - search: '', - }, - }), - capabilities: { - visualize: { - show: true, - }, - discover: { - save: false, - }, - }, - core: { - application: { - navigateToApp: jest.fn(), - }, - }, - uiSettings: { - get: (key: string) => { - if (key === 'fields:popularLimit') { - return 5; - } - }, - }, - dataViewFieldEditor: { - openEditor: jest.fn(), - userPermissions: { - editIndexPattern: () => { - return true; - }, - }, - }, -} as unknown as DiscoverServices; - -describe('Discover DataView Management', () => { - const indexPattern = stubLogstashIndexPattern; - - const editField = jest.fn(); - const createNewDataView = jest.fn(); - - const mountComponent = () => { - return mountWithIntl( - - - - ); - }; - - test('renders correctly', () => { - const component = mountComponent(); - expect(component).toMatchSnapshot(); - expect(component.find(EuiPopover).length).toBe(1); - }); - - test('click on a button opens popover', () => { - const component = mountComponent(); - expect(component.find(EuiContextMenuPanel).length).toBe(0); - - const button = findTestSubject(component, 'discoverIndexPatternActions'); - button.simulate('click'); - - expect(component.find(EuiContextMenuPanel).length).toBe(1); - expect(component.find(EuiContextMenuItem).length).toBe(3); - }); - - test('click on an add button executes editField callback', () => { - const component = mountComponent(); - const button = findTestSubject(component, 'discoverIndexPatternActions'); - button.simulate('click'); - - const addButton = findTestSubject(component, 'indexPattern-add-field'); - addButton.simulate('click'); - expect(editField).toHaveBeenCalledWith(undefined); - }); - - test('click on a manage button navigates away from discover', () => { - const component = mountComponent(); - const button = findTestSubject(component, 'discoverIndexPatternActions'); - button.simulate('click'); - - const manageButton = findTestSubject(component, 'indexPattern-manage-field'); - manageButton.simulate('click'); - expect(mockServices.core.application.navigateToApp).toHaveBeenCalled(); - }); - - test('click on add dataView button executes createNewDataView callback', () => { - const component = mountComponent(); - const button = findTestSubject(component, 'discoverIndexPatternActions'); - button.simulate('click'); - - const manageButton = findTestSubject(component, 'dataview-create-new'); - manageButton.simulate('click'); - expect(createNewDataView).toHaveBeenCalled(); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx deleted file mode 100644 index 823aa9c0050c05..00000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useState } from 'react'; -import { - EuiButtonIcon, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiHorizontalRule, - EuiPopover, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; - -export interface DiscoverIndexPatternManagementProps { - /** - * Currently selected index pattern - */ - selectedIndexPattern?: DataView; - /** - * Read from the Fields API - */ - useNewFieldsApi?: boolean; - /** - * Callback to execute on edit field action - * @param fieldName - */ - editField: (fieldName?: string) => void; - - /** - * Callback to execute on create new data action - */ - createNewDataView: () => void; -} - -export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManagementProps) { - const { dataViewFieldEditor, core } = useDiscoverServices(); - const { useNewFieldsApi, selectedIndexPattern, editField, createNewDataView } = props; - const dataViewEditPermission = dataViewFieldEditor?.userPermissions.editIndexPattern(); - const canEditDataViewField = !!dataViewEditPermission && useNewFieldsApi; - const [isAddIndexPatternFieldPopoverOpen, setIsAddIndexPatternFieldPopoverOpen] = useState(false); - - if (!useNewFieldsApi || !selectedIndexPattern || !canEditDataViewField) { - return null; - } - - const addField = () => { - editField(undefined); - }; - - return ( - { - setIsAddIndexPatternFieldPopoverOpen(false); - }} - ownFocus - data-test-subj="discover-addRuntimeField-popover" - button={ - { - setIsAddIndexPatternFieldPopoverOpen(!isAddIndexPatternFieldPopoverOpen); - }} - /> - } - > - { - setIsAddIndexPatternFieldPopoverOpen(false); - addField(); - }} - > - {i18n.translate('discover.fieldChooser.indexPatterns.addFieldButton', { - defaultMessage: 'Add field', - })} - , - { - setIsAddIndexPatternFieldPopoverOpen(false); - core.application.navigateToApp('management', { - path: `/kibana/indexPatterns/patterns/${props.selectedIndexPattern?.id}`, - }); - }} - > - {i18n.translate('discover.fieldChooser.indexPatterns.manageFieldButton', { - defaultMessage: 'Manage settings', - })} - , - , - { - setIsAddIndexPatternFieldPopoverOpen(false); - createNewDataView(); - }} - > - {i18n.translate('discover.fieldChooser.dataViews.createNewDataView', { - defaultMessage: 'Create new data view', - })} - , - ]} - /> - - ); -} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss index 9ef123fa1a60f3..6845b1c89901d9 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss @@ -2,7 +2,7 @@ overflow: hidden; margin: 0 !important; flex-grow: 1; - padding-left: $euiSize; + padding: $euiSizeS 0 $euiSizeS $euiSizeS; width: $euiSize * 19; height: 100%; @@ -19,7 +19,7 @@ .dscSidebar__mobile { width: 100%; - padding: $euiSize $euiSize 0; + padding: $euiSizeS $euiSizeS 0; .dscSidebar__mobileBadge { margin-left: $euiSizeS; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index fb6af1bc1b7756..22f954e714987e 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx @@ -20,16 +20,17 @@ import { EuiNotificationBadge, EuiPageSideBar, useResizeObserver, + EuiButton, } from '@elastic/eui'; import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect'; -import { isEqual, sortBy } from 'lodash'; +import { isEqual } from 'lodash'; import { FormattedMessage } from '@kbn/i18n-react'; import { indexPatterns as indexPatternUtils } from '@kbn/data-plugin/public'; +import { DataViewPicker } from '@kbn/unified-search-plugin/public'; import { DataViewField } from '@kbn/data-views-plugin/public'; import { useDiscoverServices } from '../../../../utils/use_discover_services'; import { DiscoverField } from './discover_field'; -import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; import { FIELDS_LIMIT_SETTING } from '../../../../../common'; import { groupFields } from './lib/group_fields'; @@ -37,7 +38,6 @@ import { getDetails } from './lib/get_details'; import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive'; -import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { ElasticSearchHit } from '../../../../types'; @@ -83,6 +83,8 @@ export interface DiscoverSidebarProps extends Omit(null); @@ -297,34 +299,6 @@ export function DiscoverSidebarComponent({ return null; } - if (useFlyout) { - return ( -
- - - o.attributes.title)} - onChangeIndexPattern={onChangeIndexPattern} - /> - - - - - -
- ); - } - return ( - - - - o.attributes.title)} - onChangeIndexPattern={onChangeIndexPattern} - /> - - - - - - + {Boolean(showDataViewPicker) && ( + + )}
+ + editField()} + size="s" + > + {i18n.translate('discover.fieldChooser.addField.label', { + defaultMessage: 'Add a field', + })} + + ); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index f2f58c43d5e7fa..f7664197ca98cd 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -7,7 +7,6 @@ */ import React, { useEffect, useRef, useState, useCallback } from 'react'; -import { sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { UiCounterMetricType } from '@kbn/analytics'; @@ -19,21 +18,16 @@ import { EuiBadge, EuiFlyoutHeader, EuiFlyout, - EuiSpacer, EuiIcon, EuiLink, EuiPortal, - EuiFlexGroup, - EuiFlexItem, } from '@elastic/eui'; import type { DataViewField, DataView, DataViewAttributes } from '@kbn/data-views-plugin/public'; import { SavedObject } from '@kbn/core/types'; import { useDiscoverServices } from '../../../../utils/use_discover_services'; -import { DiscoverIndexPattern } from './discover_index_pattern'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { AppState } from '../../services/discover_state'; -import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; import { AvailableFields$, DataDocuments$ } from '../../utils/use_saved_search'; import { calcFieldCounts } from '../../utils/calc_field_counts'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; @@ -91,10 +85,6 @@ export interface DiscoverSidebarResponsiveProps { * @param eventName */ trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; - /** - * Shows index pattern and a button that displays the sidebar in a flyout - */ - useFlyout?: boolean; /** * Read from the Fields API */ @@ -124,13 +114,7 @@ export interface DiscoverSidebarResponsiveProps { */ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) { const services = useDiscoverServices(); - const { - selectedIndexPattern, - onEditRuntimeField, - useNewFieldsApi, - onChangeIndexPattern, - onDataViewCreated, - } = props; + const { selectedIndexPattern, onEditRuntimeField, useNewFieldsApi, onDataViewCreated } = props; const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter()); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); /** @@ -291,34 +275,6 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) )}
-
- - - o.attributes.title)} - /> - - - - - -
- -
diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx index 938d2d55df0047..7b8831f734279e 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx @@ -40,6 +40,8 @@ function getProps(savePermissions = true): DiscoverTopNavProps { onOpenInspector: jest.fn(), searchSource: {} as ISearchSource, resetSavedSearch: () => {}, + onEditRuntimeField: jest.fn(), + onChangeIndexPattern: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx index 8656a2fdb70728..87d2f04bd604b1 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useRef, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { Query, TimeRange } from '@kbn/data-plugin/public'; import { DataViewType } from '@kbn/data-views-plugin/public'; @@ -25,6 +25,9 @@ export type DiscoverTopNavProps = Pick< updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; stateContainer: GetStateReturn; resetSavedSearch: () => void; + onChangeIndexPattern: (indexPattern: string) => void; + onEditRuntimeField: () => void; + useNewFieldsApi?: boolean; }; export const DiscoverTopNav = ({ @@ -38,6 +41,9 @@ export const DiscoverTopNav = ({ navigateTo, savedSearch, resetSavedSearch, + onChangeIndexPattern, + onEditRuntimeField, + useNewFieldsApi = false, }: DiscoverTopNavProps) => { const history = useHistory(); const showDatePicker = useMemo( @@ -45,7 +51,16 @@ export const DiscoverTopNav = ({ [indexPattern] ); const services = useDiscoverServices(); - const { TopNavMenu } = services.navigation.ui; + const { dataViewEditor, navigation, dataViewFieldEditor, data } = services; + const editPermission = useMemo( + () => dataViewFieldEditor.userPermissions.editIndexPattern(), + [dataViewFieldEditor] + ); + const canEditDataViewField = !!editPermission && useNewFieldsApi; + const closeFieldEditor = useRef<() => void | undefined>(); + const closeDataViewEditor = useRef<() => void | undefined>(); + + const { TopNavMenu } = navigation.ui; const onOpenSavedSearch = useCallback( (newSavedSearchId: string) => { @@ -58,6 +73,64 @@ export const DiscoverTopNav = ({ [history, resetSavedSearch, savedSearch.id] ); + useEffect(() => { + return () => { + // Make sure to close the editors when unmounting + if (closeFieldEditor.current) { + closeFieldEditor.current(); + } + if (closeDataViewEditor.current) { + closeDataViewEditor.current(); + } + }; + }, []); + + const editField = useMemo( + () => + canEditDataViewField + ? async (fieldName?: string, uiAction: 'edit' | 'add' = 'edit') => { + if (indexPattern?.id) { + const indexPatternInstance = await data.dataViews.get(indexPattern.id); + closeFieldEditor.current = dataViewFieldEditor.openEditor({ + ctx: { + dataView: indexPatternInstance, + }, + fieldName, + onSave: async () => { + onEditRuntimeField(); + }, + }); + } + } + : undefined, + [ + canEditDataViewField, + indexPattern?.id, + data.dataViews, + dataViewFieldEditor, + onEditRuntimeField, + ] + ); + + const addField = useMemo( + () => (canEditDataViewField && editField ? () => editField(undefined, 'add') : undefined), + [editField, canEditDataViewField] + ); + + const createNewDataView = useCallback(() => { + const indexPatternFieldEditPermission = dataViewEditor.userPermissions.editDataView; + if (!indexPatternFieldEditPermission) { + return; + } + closeDataViewEditor.current = dataViewEditor.openEditor({ + onSave: async (dataView) => { + if (dataView.id) { + onChangeIndexPattern(dataView.id); + } + }, + }); + }, [dataViewEditor, onChangeIndexPattern]); + const topNavMenu = useMemo( () => getTopNavLinks({ @@ -99,6 +172,18 @@ export const DiscoverTopNav = ({ return getHeaderActionMenuMounter(); }, []); + const dataViewPickerProps = { + trigger: { + label: indexPattern?.title || '', + 'data-test-subj': 'discover-dataView-switch-link', + title: indexPattern?.title || '', + }, + currentDataViewId: indexPattern?.id, + onAddField: addField, + onDataViewCreated: createNewDataView, + onChangeDataView: (newIndexPatternId: string) => onChangeIndexPattern(newIndexPatternId), + }; + return ( ); }; diff --git a/src/plugins/discover/public/application/main/discover_main_app.test.tsx b/src/plugins/discover/public/application/main/discover_main_app.test.tsx index ceb06df058faee..d2f0c7e2dd0058 100644 --- a/src/plugins/discover/public/application/main/discover_main_app.test.tsx +++ b/src/plugins/discover/public/application/main/discover_main_app.test.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { indexPatternMock } from '../../__mocks__/index_pattern'; import { DiscoverMainApp } from './discover_main_app'; +import { DiscoverTopNav } from './components/top_nav/discover_topnav'; import { savedSearchMock } from '../../__mocks__/saved_search'; import { SavedObject } from '@kbn/core/types'; import type { DataViewAttributes } from '@kbn/data-views-plugin/public'; import { setHeaderActionMenuMounter } from '../../kibana_services'; -import { findTestSubject } from '@elastic/eui/lib/test'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { discoverServiceMock } from '../../__mocks__/services'; import { Router } from 'react-router-dom'; @@ -42,8 +42,7 @@ describe('DiscoverMainApp', () => { ); - expect(findTestSubject(component, 'indexPattern-switch-link').text()).toBe( - indexPatternMock.title - ); + expect(component.find(DiscoverTopNav).exists()).toBe(true); + expect(component.find(DiscoverTopNav).prop('indexPattern')).toEqual(indexPatternMock); }); }); diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 817e73f16617e6..9915680ada26e8 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -25,6 +25,7 @@ { "path": "../data_view_field_editor/tsconfig.json"}, { "path": "../field_formats/tsconfig.json" }, { "path": "../data_views/tsconfig.json" }, + { "path": "../unified_search/tsconfig.json" }, { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, { "path": "../data_view_editor/tsconfig.json" }, { "path": "../../../x-pack/plugins/triggers_actions_ui/tsconfig.json" } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 85a9803ffced67..aee35c1f331c73 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -159,7 +159,6 @@ describe('TopNavMenu', () => { await refresh(); - expect(component.find(WRAPPER_SELECTOR).length).toBe(1); expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1); // menu is rendered outside of the component diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 7eb7365ed79f35..62dc67a3ee941c 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -117,15 +117,15 @@ export function TopNavMenu(props: TopNavMenuProps): ReactElement | null { {renderMenu(menuClassName)} - {renderSearchBar()} + {renderSearchBar()} ); } else { return ( - - {renderMenu(menuClassName)} + <> + {renderMenu(menuClassName)} {renderSearchBar()} - + ); } } diff --git a/src/plugins/unified_search/.storybook/main.js b/src/plugins/unified_search/.storybook/main.js new file mode 100644 index 00000000000000..8dc3c5d1518f4d --- /dev/null +++ b/src/plugins/unified_search/.storybook/main.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx b/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx new file mode 100644 index 00000000000000..49e25e04d01a83 --- /dev/null +++ b/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx @@ -0,0 +1,431 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public'; +import { SearchBar, SearchBarProps } from '../search_bar'; +import { setIndexPatterns } from '../services'; + +const mockIndexPatterns = [ + { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], + }, + { + id: '1235', + title: 'test-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], + }, +] as DataView[]; + +const mockTimeHistory = { + get: () => { + return []; + }, + add: action('set'), + get$: () => { + return { + pipe: () => {}, + }; + }, +}; + +const createMockWebStorage = () => ({ + clear: action('clear'), + getItem: action('getItem'), + key: action('key'), + removeItem: action('removeItem'), + setItem: action('setItem'), + length: 0, +}); + +const createMockStorage = () => ({ + storage: createMockWebStorage(), + set: action('set'), + remove: action('remove'), + clear: action('clear'), + get: () => true, +}); + +const services = { + uiSettings: { + get: () => {}, + }, + savedObjects: action('savedObjects'), + notifications: action('notifications'), + http: { + basePath: { + prepend: () => 'http://test', + }, + }, + docLinks: { + links: { + query: { + kueryQuerySyntax: '', + }, + }, + }, + storage: createMockStorage(), + data: { + query: { + savedQueries: { + findSavedQueries: () => + Promise.resolve({ + queries: [ + { + id: 'testwewe', + attributes: { + title: 'Saved query 1', + description: '', + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + filters: [], + }, + }, + { + id: '0173d0d0-b19a-11ec-8323-837d6b231b82', + attributes: { + title: 'test', + description: '', + query: { + query: '', + language: 'kuery', + }, + filters: [ + { + meta: { + index: '1234', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + }, + ], + }), + }, + }, + autocomplete: { + hasQuerySuggestions: () => Promise.resolve(false), + getQuerySuggestions: () => [], + }, + dataViews: { + getIdsWithTitle: () => [ + { id: '1234', title: 'logstash-*' }, + { id: '1235', title: 'test-*' }, + ], + }, + }, +}; + +setIndexPatterns({ + get: () => Promise.resolve(mockIndexPatterns[0]), +} as unknown as DataViewsContract); + +function wrapSearchBarInContext(testProps: SearchBarProps) { + const defaultOptions = { + appName: 'test', + timeHistory: mockTimeHistory, + intl: null as any, + showQueryBar: true, + showFilterBar: true, + showDatePicker: true, + showAutoRefreshOnly: false, + showSaveQuery: true, + showQueryInput: true, + indexPatterns: mockIndexPatterns, + dateRangeFrom: 'now-15m', + dateRangeTo: 'now', + query: { query: '', language: 'kuery' }, + filters: [], + onClearSavedQuery: action('onClearSavedQuery'), + onFiltersUpdated: action('onFiltersUpdated'), + } as unknown as SearchBarProps; + + return ( + + + + + + ); +} + +storiesOf('SearchBar', module) + .add('default', () => wrapSearchBarInContext({ showQueryInput: true } as SearchBarProps)) + .add('with dataviewPicker', () => + wrapSearchBarInContext({ + dataViewPickerComponentProps: { + currentDataViewId: '1234', + trigger: { + 'data-test-subj': 'dataView-switch-link', + label: 'logstash-*', + title: 'logstash-*', + }, + onChangeDataView: action('onChangeDataView'), + }, + } as SearchBarProps) + ) + .add('with dataviewPicker enhanced', () => + wrapSearchBarInContext({ + dataViewPickerComponentProps: { + currentDataViewId: '1234', + trigger: { + 'data-test-subj': 'dataView-switch-link', + label: 'logstash-*', + title: 'logstash-*', + }, + onChangeDataView: action('onChangeDataView'), + onAddField: action('onAddField'), + onDataViewCreated: action('onDataViewCreated'), + }, + } as SearchBarProps) + ) + .add('with filterBar off', () => + wrapSearchBarInContext({ + showFilterBar: false, + } as SearchBarProps) + ) + .add('with query input off', () => + wrapSearchBarInContext({ + showQueryInput: false, + } as SearchBarProps) + ) + .add('with date picker off', () => + wrapSearchBarInContext({ + showDatePicker: false, + } as SearchBarProps) + ) + .add('with date picker off', () => + wrapSearchBarInContext({ + showDatePicker: false, + } as SearchBarProps) + ) + .add('with only the date picker on', () => + wrapSearchBarInContext({ + showDatePicker: true, + showFilterBar: false, + showQueryInput: false, + } as SearchBarProps) + ) + .add('with only the filter bar on', () => + wrapSearchBarInContext({ + showDatePicker: false, + showFilterBar: true, + showQueryInput: false, + filters: [ + { + meta: { + index: '1234', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + } as unknown as SearchBarProps) + ) + .add('with only the query bar on', () => + wrapSearchBarInContext({ + showDatePicker: false, + showFilterBar: false, + showQueryInput: true, + query: { query: 'Test: miaou', language: 'kuery' }, + } as unknown as SearchBarProps) + ) + .add('with only the filter bar and the date picker on', () => + wrapSearchBarInContext({ + showDatePicker: true, + showFilterBar: true, + showQueryInput: false, + filters: [ + { + meta: { + index: '1234', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + } as unknown as SearchBarProps) + ) + .add('with loaded saved query without changes', () => + wrapSearchBarInContext({ + savedQuery: { + id: '0173d0d0-b19a-11ec-8323-837d6b231b82', + attributes: { + title: 'test', + description: '', + query: { + query: '', + language: 'kuery', + }, + filters: [ + { + meta: { + index: '1234', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + }, + filters: [ + { + meta: { + index: '1234', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + } as unknown as SearchBarProps) + ) + .add('with loaded saved query with changes', () => + wrapSearchBarInContext({ + savedQuery: { + id: '0173d0d0-b19a-11ec-8323-837d6b231b82', + attributes: { + title: 'test', + description: '', + query: { + query: '', + language: 'kuery', + }, + filters: [ + { + meta: { + index: '1234', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + }, + } as unknown as SearchBarProps) + ) + .add('show only query bar without submit', () => + wrapSearchBarInContext({ + showDatePicker: false, + showFilterBar: false, + showAutoRefreshOnly: false, + showQueryInput: true, + showSubmitButton: false, + } as SearchBarProps) + ); diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.styles.ts b/src/plugins/unified_search/public/dataview_picker/change_dataview.styles.ts new file mode 100644 index 00000000000000..1c505752d392c6 --- /dev/null +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.styles.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const DATA_VIEW_POPOVER_CONTENT_WIDTH = 280; + +export const changeDataViewStyles = ({ fullWidth }: { fullWidth?: boolean }) => { + return { + trigger: { + maxWidth: fullWidth ? undefined : DATA_VIEW_POPOVER_CONTENT_WIDTH, + }, + popoverContent: { + width: DATA_VIEW_POPOVER_CONTENT_WIDTH, + }, + }; +}; diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx new file mode 100644 index 00000000000000..d3081561a0c4e2 --- /dev/null +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { act } from 'react-dom/test-utils'; +import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { ChangeDataView } from './change_dataview'; +import { EuiTourStep } from '@elastic/eui'; +import type { DataViewPickerProps } from '.'; + +describe('DataView component', () => { + const createMockWebStorage = () => ({ + clear: jest.fn(), + getItem: jest.fn(), + key: jest.fn(), + removeItem: jest.fn(), + setItem: jest.fn(), + length: 0, + }); + + const createMockStorage = () => ({ + storage: createMockWebStorage(), + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }); + const getStorage = (v: boolean) => { + const storage = createMockStorage(); + storage.get.mockReturnValue(v); + return storage; + }; + + function wrapDataViewComponentInContext(testProps: DataViewPickerProps, storageValue: boolean) { + let dataMock = dataPluginMock.createStartContract(); + dataMock = { + ...dataMock, + dataViews: { + ...dataMock.dataViews, + getIdsWithTitle: jest.fn(), + }, + }; + const services = { + data: dataMock, + storage: getStorage(storageValue), + }; + + return ( + + + + + + ); + } + let props: DataViewPickerProps; + beforeEach(() => { + props = { + currentDataViewId: 'dataview-1', + trigger: { + label: 'Dataview 1', + title: 'Dataview 1', + fullWidth: true, + 'data-test-subj': 'dataview-trigger', + }, + onChangeDataView: jest.fn(), + }; + }); + it('should not render the tour component by default', async () => { + await act(async () => { + const component = mount(wrapDataViewComponentInContext(props, true)); + expect(component.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); + }); + it('should render the tour component if the showNewMenuTour is true', async () => { + const component = mount( + wrapDataViewComponentInContext({ ...props, showNewMenuTour: true }, false) + ); + expect(component.find(EuiTourStep).prop('isStepOpen')).toBe(true); + }); + + it('should not render the add runtime field menu if addField is not given', async () => { + await act(async () => { + const component = mount(wrapDataViewComponentInContext(props, true)); + findTestSubject(component, 'dataview-trigger').simulate('click'); + expect(component.find('[data-test-subj="indexPattern-add-field"]').length).toBe(0); + }); + }); + + it('should render the add runtime field menu if addField is given', async () => { + const addFieldSpy = jest.fn(); + const component = mount( + wrapDataViewComponentInContext( + { ...props, onAddField: addFieldSpy, showNewMenuTour: true }, + false + ) + ); + findTestSubject(component, 'dataview-trigger').simulate('click'); + expect(component.find('[data-test-subj="indexPattern-add-field"]').at(0).text()).toContain( + 'Add a field to this data view' + ); + component.find('[data-test-subj="indexPattern-add-field"]').first().simulate('click'); + expect(addFieldSpy).toHaveBeenCalled(); + }); + + it('should not render the add datavuew menu if onDataViewCreated is not given', async () => { + await act(async () => { + const component = mount(wrapDataViewComponentInContext(props, true)); + findTestSubject(component, 'dataview-trigger').simulate('click'); + expect(component.find('[data-test-subj="idataview-create-new"]').length).toBe(0); + }); + }); + + it('should render the add datavuew menu if onDataViewCreated is given', async () => { + const addDataViewSpy = jest.fn(); + const component = mount( + wrapDataViewComponentInContext( + { ...props, onDataViewCreated: addDataViewSpy, showNewMenuTour: true }, + false + ) + ); + findTestSubject(component, 'dataview-trigger').simulate('click'); + expect(component.find('[data-test-subj="dataview-create-new"]').at(0).text()).toContain( + 'Create a data view' + ); + component.find('[data-test-subj="dataview-create-new"]').first().simulate('click'); + expect(addDataViewSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx new file mode 100644 index 00000000000000..3e0ed7cc8a2664 --- /dev/null +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx @@ -0,0 +1,244 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useState, useEffect } from 'react'; +import { css } from '@emotion/react'; +import { + EuiPopover, + EuiHorizontalRule, + EuiButton, + EuiContextMenuPanel, + EuiContextMenuItem, + useEuiTheme, + useGeneratedHtmlId, + EuiIcon, + EuiLink, + EuiText, + EuiTourStep, + EuiContextMenuPanelProps, +} from '@elastic/eui'; +import type { DataViewListItem } from '@kbn/data-views-plugin/public'; +import { IDataPluginServices } from '@kbn/data-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { DataViewPickerProps } from '.'; +import { DataViewsList } from './dataview_list'; +import { changeDataViewStyles } from './change_dataview.styles'; + +const NEW_DATA_VIEW_MENU_STORAGE_KEY = 'data.newDataViewMenu'; + +const newMenuTourTitle = i18n.translate('unifiedSearch.query.dataViewMenu.newMenuTour.title', { + defaultMessage: 'A better data view menu', +}); + +const newMenuTourDescription = i18n.translate( + 'unifiedSearch.query.dataViewMenu.newMenuTour.description', + { + defaultMessage: + 'This menu now offers all the tools you need to create, find, and edit your data views.', + } +); + +const newMenuTourDismissLabel = i18n.translate( + 'unifiedSearch.query.dataViewMenu.newMenuTour.dismissLabel', + { + defaultMessage: 'Got it', + } +); + +export function ChangeDataView({ + isMissingCurrent, + currentDataViewId, + onChangeDataView, + onAddField, + onDataViewCreated, + trigger, + selectableProps, + showNewMenuTour = false, +}: DataViewPickerProps) { + const { euiTheme } = useEuiTheme(); + const [isPopoverOpen, setPopoverIsOpen] = useState(false); + const [dataViewsList, setDataViewsList] = useState([]); + const [triggerLabel, setTriggerLabel] = useState(''); + const kibana = useKibana(); + const { application, data, storage } = kibana.services; + const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth }); + + const [isTourDismissed, setIsTourDismissed] = useState(() => + Boolean(storage.get(NEW_DATA_VIEW_MENU_STORAGE_KEY)) + ); + const [isTourOpen, setIsTourOpen] = useState(false); + + useEffect(() => { + if (showNewMenuTour && !isTourDismissed) { + setIsTourOpen(true); + } + }, [isTourDismissed, setIsTourOpen, showNewMenuTour]); + + const onTourDismiss = () => { + storage.set(NEW_DATA_VIEW_MENU_STORAGE_KEY, true); + setIsTourDismissed(true); + setIsTourOpen(false); + }; + + // Create a reusable id to ensure search input is the first focused item in the popover even though it's not the first item + const searchListInputId = useGeneratedHtmlId({ prefix: 'dataviewPickerListSearchInput' }); + + useEffect(() => { + const fetchDataViews = async () => { + const dataViewsRefs = await data.dataViews.getIdsWithTitle(); + setDataViewsList(dataViewsRefs); + }; + fetchDataViews(); + }, [data, currentDataViewId]); + + useEffect(() => { + if (trigger.label) { + setTriggerLabel(trigger.label); + } + }, [trigger.label]); + + const createTrigger = function () { + const { label, title, 'data-test-subj': dataTestSubj, fullWidth, ...rest } = trigger; + return ( + { + setPopoverIsOpen(!isPopoverOpen); + setIsTourOpen(false); + // onTourDismiss(); TODO: Decide if opening the menu should also dismiss the tour + }} + color={isMissingCurrent ? 'danger' : 'primary'} + iconSide="right" + iconType="arrowDown" + title={title} + fullWidth={fullWidth} + {...rest} + > + {triggerLabel} + + ); + }; + + const getPanelItems = () => { + const panelItems: EuiContextMenuPanelProps['items'] = []; + if (onAddField) { + panelItems.push( + { + setPopoverIsOpen(false); + onAddField(); + }} + > + {i18n.translate('unifiedSearch.query.queryBar.indexPattern.addFieldButton', { + defaultMessage: 'Add a field to this data view', + })} + , + { + setPopoverIsOpen(false); + application.navigateToApp('management', { + path: `/kibana/indexPatterns/patterns/${currentDataViewId}`, + }); + }} + > + {i18n.translate('unifiedSearch.query.queryBar.indexPattern.manageFieldButton', { + defaultMessage: 'Manage this data view', + })} + , + + ); + } + panelItems.push( + { + onChangeDataView(newId); + setPopoverIsOpen(false); + }} + currentDataViewId={currentDataViewId} + selectableProps={selectableProps} + searchListInputId={searchListInputId} + /> + ); + + if (onDataViewCreated) { + panelItems.push( + , + { + setPopoverIsOpen(false); + onDataViewCreated(); + }} + > + {i18n.translate('unifiedSearch.query.queryBar.indexPattern.addNewDataView', { + defaultMessage: 'Create a data view', + })} + + ); + } + + return panelItems; + }; + + return ( + +   {newMenuTourTitle} + + } + content={ + +

{newMenuTourDescription}

+
+ } + isStepOpen={isTourOpen} + onFinish={onTourDismiss} + step={1} + stepsTotal={1} + footerAction={ + + {newMenuTourDismissLabel} + + } + repositionOnScroll + display="block" + > + setPopoverIsOpen(false)} + panelPaddingSize="none" + initialFocus={`#${searchListInputId}`} + display="block" + buffer={8} + > +
+ +
+
+
+ ); +} diff --git a/src/plugins/unified_search/public/dataview_picker/dataview_list.test.tsx b/src/plugins/unified_search/public/dataview_picker/dataview_list.test.tsx new file mode 100644 index 00000000000000..813beae20369c2 --- /dev/null +++ b/src/plugins/unified_search/public/dataview_picker/dataview_list.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiSelectable } from '@elastic/eui'; +import { act } from 'react-dom/test-utils'; +import { ShallowWrapper } from 'enzyme'; +import { shallowWithIntl as shallow } from '@kbn/test-jest-helpers'; +import { DataViewsList, DataViewsListProps } from './dataview_list'; + +function getDataViewPickerList(instance: ShallowWrapper) { + return instance.find(EuiSelectable).first(); +} + +function getDataViewPickerOptions(instance: ShallowWrapper) { + return getDataViewPickerList(instance).prop('options'); +} + +function selectDataViewPickerOption(instance: ShallowWrapper, selectedLabel: string) { + const options: Array<{ label: string; checked?: 'on' | 'off' }> = getDataViewPickerOptions( + instance + ).map((option: { label: string }) => + option.label === selectedLabel + ? { ...option, checked: 'on' } + : { ...option, checked: undefined } + ); + return getDataViewPickerList(instance).prop('onChange')!(options); +} + +describe('DataView list component', () => { + const list = [ + { + id: 'dataview-1', + title: 'dataview-1', + }, + { + id: 'dataview-2', + title: 'dataview-2', + }, + ]; + const changeDataViewSpy = jest.fn(); + let props: DataViewsListProps; + beforeEach(() => { + props = { + currentDataViewId: 'dataview-1', + onChangeDataView: changeDataViewSpy, + dataViewsList: list, + }; + }); + it('should trigger the onChangeDataView if a new dataview is selected', async () => { + const component = shallow(); + await act(async () => { + selectDataViewPickerOption(component, 'dataview-2'); + }); + expect(changeDataViewSpy).toHaveBeenCalled(); + }); + + it('should list all dataviiew', () => { + const component = shallow(); + + expect(getDataViewPickerOptions(component)!.map((option: any) => option.label)).toEqual([ + 'dataview-1', + 'dataview-2', + ]); + }); +}); diff --git a/src/plugins/unified_search/public/dataview_picker/dataview_list.tsx b/src/plugins/unified_search/public/dataview_picker/dataview_list.tsx new file mode 100644 index 00000000000000..153cbdd3cf3f24 --- /dev/null +++ b/src/plugins/unified_search/public/dataview_picker/dataview_list.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiSelectable, EuiSelectableProps, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { DataViewListItem } from '@kbn/data-views-plugin/public'; + +export interface DataViewsListProps { + dataViewsList: DataViewListItem[]; + onChangeDataView: (newId: string) => void; + currentDataViewId?: string; + selectableProps?: EuiSelectableProps; + searchListInputId?: string; +} + +export function DataViewsList({ + dataViewsList, + onChangeDataView, + currentDataViewId, + selectableProps, + searchListInputId, +}: DataViewsListProps) { + return ( + + {...selectableProps} + data-test-subj="indexPattern-switcher" + searchable + singleSelection="always" + options={dataViewsList?.map(({ title, id }) => ({ + key: id, + label: title, + value: id, + checked: id === currentDataViewId ? 'on' : undefined, + }))} + onChange={(choices) => { + const choice = choices.find(({ checked }) => checked) as unknown as { + value: string; + }; + onChangeDataView(choice.value); + }} + searchProps={{ + id: searchListInputId, + compressed: true, + placeholder: i18n.translate('unifiedSearch.query.queryBar.indexPattern.findDataView', { + defaultMessage: 'Find a data view', + }), + ...(selectableProps ? selectableProps.searchProps : undefined), + }} + > + {(list, search) => ( + + {search} + {list} + + )} + + ); +} diff --git a/src/plugins/unified_search/public/dataview_picker/index.tsx b/src/plugins/unified_search/public/dataview_picker/index.tsx new file mode 100644 index 00000000000000..bd24aef0498ef6 --- /dev/null +++ b/src/plugins/unified_search/public/dataview_picker/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { EuiButtonProps, EuiSelectableProps } from '@elastic/eui'; +import { ChangeDataView } from './change_dataview'; + +export type ChangeDataViewTriggerProps = EuiButtonProps & { + label: string; + title?: string; +}; + +/** @public */ +export interface DataViewPickerProps { + trigger: ChangeDataViewTriggerProps; + isMissingCurrent?: boolean; + onChangeDataView: (newId: string) => void; + currentDataViewId?: string; + selectableProps?: EuiSelectableProps; + onAddField?: () => void; + onDataViewCreated?: () => void; + showNewMenuTour?: boolean; +} + +export const DataViewPicker = ({ + isMissingCurrent, + currentDataViewId, + onChangeDataView, + onAddField, + onDataViewCreated, + trigger, + selectableProps, + showNewMenuTour, +}: DataViewPickerProps) => { + return ( + + ); +}; diff --git a/src/plugins/unified_search/public/filter_bar/_global_filter_group.scss b/src/plugins/unified_search/public/filter_bar/_global_filter_group.scss deleted file mode 100644 index 24f3ca05a5685f..00000000000000 --- a/src/plugins/unified_search/public/filter_bar/_global_filter_group.scss +++ /dev/null @@ -1,54 +0,0 @@ -// SASSTODO: Probably not the right file for this selector, but temporary until the files get re-organized -.globalQueryBar { - padding: 0 $euiSizeS $euiSizeS $euiSizeS; -} - -.globalQueryBar:first-child { - padding-top: $euiSizeS; -} - -.globalQueryBar:not(:empty) { - padding-bottom: $euiSizeS; -} - -.globalQueryBar--inPage { - padding: 0; -} - -.globalFilterGroup__filterBar { - margin-top: $euiSizeXS; -} - -.globalFilterBar__addButton { - min-height: $euiSizeL + $euiSizeXS; // same height as the badges -} - -// sass-lint:disable quotes -.globalFilterGroup__branch { - padding: $euiSizeS $euiSizeM 0 0; - background-repeat: no-repeat; - background-position: $euiSizeM ($euiSizeS * -1); - background-image: url("data:image/svg+xml,%0A%3Csvg width='28px' height='28px' viewBox='0 0 28 28' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='#{hexToRGB($euiColorLightShade)}'%3E%3Crect x='14' y='27' width='14' height='1'%3E%3C/rect%3E%3C/g%3E%3C/svg%3E"); -} - -.globalFilterGroup__wrapper { - line-height: 1; // Override kuiLocalNav & kuiLocalNavRow - overflow: hidden; - transition: height $euiAnimSpeedNormal $euiAnimSlightResistance; -} - -.globalFilterGroup__filterFlexItem { - overflow: hidden; - padding-bottom: 2px; // Allow the shadows of the pills to show -} - -.globalFilterBar__flexItem { - max-width: calc(100% - #{$euiSizeXS}); // Width minus margin around each flex itm -} - -@include euiBreakpoint('xs', 's') { - .globalFilterGroup__wrapper-isVisible { - // EUI Flexbox adds too much margin between responded items, this just moves it up - margin-top: $euiSize * -1; - } -} diff --git a/src/plugins/unified_search/public/filter_bar/_index.scss b/src/plugins/unified_search/public/filter_bar/_index.scss deleted file mode 100644 index 5333aff8b87da3..00000000000000 --- a/src/plugins/unified_search/public/filter_bar/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'variables'; -@import 'global_filter_group'; -@import 'global_filter_item'; diff --git a/src/plugins/unified_search/public/filter_bar/filter_bar.styles.ts b/src/plugins/unified_search/public/filter_bar/filter_bar.styles.ts new file mode 100644 index 00000000000000..919655e0af1604 --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_bar.styles.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UseEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +export const filterBarStyles = ({ euiTheme }: UseEuiTheme, afterQueryBar?: boolean) => { + return { + group: css` + gap: ${euiTheme.size.xs}; + + &:not(:empty) { + margin-top: ${afterQueryBar ? euiTheme.size.s : 0}; + } + `, + }; +}; diff --git a/src/plugins/unified_search/public/filter_bar/filter_bar.tsx b/src/plugins/unified_search/public/filter_bar/filter_bar.tsx index 43b511b2c9f7d7..ec7205d3a7df70 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_bar.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_bar.tsx @@ -6,224 +6,49 @@ * Side Public License, v 1. */ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n-react'; -import { - buildEmptyFilter, - Filter, - enableFilter, - disableFilter, - pinFilter, - toggleFilterDisabled, - toggleFilterNegated, - unpinFilter, -} from '@kbn/es-query'; -import classNames from 'classnames'; -import React, { useState, useRef } from 'react'; - -import { METRIC_TYPE } from '@kbn/analytics'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { UI_SETTINGS } from '@kbn/data-plugin/common'; -import { IDataPluginServices } from '@kbn/data-plugin/public'; +import { EuiFlexGroup, useEuiTheme } from '@elastic/eui'; +import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; +import type { Filter } from '@kbn/es-query'; +import React, { useRef } from 'react'; import { DataView } from '@kbn/data-views-plugin/public'; -import { FilterOptions } from './filter_options'; -import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item'; -import { FilterEditor } from './filter_editor'; +import FilterItems from './filter_item/filter_items'; + +import { filterBarStyles } from './filter_bar.styles'; export interface Props { filters: Filter[]; onFiltersUpdated?: (filters: Filter[]) => void; - className: string; + className?: string; indexPatterns: DataView[]; intl: InjectedIntl; - appName: string; timeRangeForSuggestionsOverride?: boolean; + /** + * Applies extra styles necessary when coupled with the query bar + */ + afterQueryBar?: boolean; } const FilterBarUI = React.memo(function FilterBarUI(props: Props) { + const euiTheme = useEuiTheme(); + const styles = filterBarStyles(euiTheme, props.afterQueryBar); const groupRef = useRef(null); - const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); - const kibana = useKibana(); - const { appName, usageCollection, uiSettings } = kibana.services; - if (!uiSettings) return null; - - const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); - - function onFiltersUpdated(filters: Filter[]) { - if (props.onFiltersUpdated) { - props.onFiltersUpdated(filters); - } - } - - const onAddFilterClick = () => setIsAddFilterPopoverOpen(!isAddFilterPopoverOpen); - - function renderItems() { - return props.filters.map((filter, i) => ( - - onUpdate(i, newFilter)} - onRemove={() => onRemove(i)} - indexPatterns={props.indexPatterns} - uiSettings={uiSettings!} - timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride} - /> - - )); - } - - function renderAddFilter() { - const isPinned = uiSettings!.get(UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT); - const [indexPattern] = props.indexPatterns; - const index = indexPattern && indexPattern.id; - const newFilter = buildEmptyFilter(isPinned, index); - - const button = ( - - +{' '} - - - ); - - return ( - - setIsAddFilterPopoverOpen(false)} - anchorPosition="downLeft" - panelPaddingSize="none" - initialFocus=".filterEditor__hiddenItem" - ownFocus - repositionOnScroll - > - -
- setIsAddFilterPopoverOpen(false)} - key={JSON.stringify(newFilter)} - timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride} - /> -
-
-
-
- ); - } - - function onAdd(filter: Filter) { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:added`); - setIsAddFilterPopoverOpen(false); - - const filters = [...props.filters, filter]; - onFiltersUpdated(filters); - } - - function onRemove(i: number) { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:removed`); - const filters = [...props.filters]; - filters.splice(i, 1); - onFiltersUpdated(filters); - groupRef.current?.focus(); - } - - function onUpdate(i: number, filter: Filter) { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:edited`); - const filters = [...props.filters]; - filters[i] = filter; - onFiltersUpdated(filters); - } - - function onEnableAll() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:enable_all`); - const filters = props.filters.map(enableFilter); - onFiltersUpdated(filters); - } - - function onDisableAll() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:disable_all`); - const filters = props.filters.map(disableFilter); - onFiltersUpdated(filters); - } - - function onPinAll() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:pin_all`); - const filters = props.filters.map(pinFilter); - onFiltersUpdated(filters); - } - - function onUnpinAll() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:unpin_all`); - const filters = props.filters.map(unpinFilter); - onFiltersUpdated(filters); - } - - function onToggleAllNegated() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:invert_all`); - const filters = props.filters.map(toggleFilterNegated); - onFiltersUpdated(filters); - } - - function onToggleAllDisabled() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:toggle_all`); - const filters = props.filters.map(toggleFilterDisabled); - onFiltersUpdated(filters); - } - - function onRemoveAll() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:remove_all`); - onFiltersUpdated([]); - } - - const classes = classNames('globalFilterBar', props.className); return ( - - - - - - - {renderItems()} - {renderAddFilter()} - - + ); }); diff --git a/src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.scss b/src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.scss new file mode 100644 index 00000000000000..95b87e1d827c60 --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.scss @@ -0,0 +1,24 @@ +.kbnFilterButtonGroup { + height: $euiFormControlHeight; + background-color: $euiFormInputGroupLabelBackground; + border-radius: $euiFormControlBorderRadius; + box-shadow: 0 0 1px inset rgba($euiFormBorderOpaqueColor, .4); + + // Targets any interactable elements + *:enabled { + transform: none !important; + } + + &--s { + height: $euiFormControlCompressedHeight; + } + + &--attached { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + > *:not(:last-of-type) { + border-right: 1px solid $euiFormBorderColor; + } +} diff --git a/src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.tsx b/src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.tsx new file mode 100644 index 00000000000000..1de5c71f4a301e --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import './filter_button_group.scss'; + +import React, { FC, ReactNode } from 'react'; +import classNames from 'classnames'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +interface Props { + items: ReactNode[]; + /** + * Displays the last item without a border radius as if attached to the next DOM node + */ + attached?: boolean; + /** + * Matches overall height with standard form/button sizes + */ + size?: 'm' | 's'; +} + +export const FilterButtonGroup: FC = ({ items, attached, size = 'm', ...rest }: Props) => { + return ( + + {items.map((item, i) => + item == null ? undefined : ( + + {item} + + ) + )} + + ); +}; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx index 972bf657723fc1..ae7917e7a1c7ac 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx @@ -14,6 +14,7 @@ import { EuiFlexItem, EuiForm, EuiFormRow, + EuiPopoverFooter, EuiPopoverTitle, EuiSpacer, EuiSwitch, @@ -55,6 +56,7 @@ export interface Props { onCancel: () => void; intl: InjectedIntl; timeRangeForSuggestionsOverride?: boolean; + mode?: 'edit' | 'add'; } interface State { @@ -68,6 +70,20 @@ interface State { isCustomEditorOpen: boolean; } +const panelTitleAdd = i18n.translate('unifiedSearch.filter.filterEditor.addFilterPopupTitle', { + defaultMessage: 'Add filter', +}); +const panelTitleEdit = i18n.translate('unifiedSearch.filter.filterEditor.editFilterPopupTitle', { + defaultMessage: 'Edit filter', +}); + +const addButtonLabel = i18n.translate('unifiedSearch.filter.filterEditor.addButtonLabel', { + defaultMessage: 'Add filter', +}); +const updateButtonLabel = i18n.translate('unifiedSearch.filter.filterEditor.updateButtonLabel', { + defaultMessage: 'Update filter', +}); + class FilterEditorUI extends Component { constructor(props: Props) { super(props); @@ -86,14 +102,9 @@ class FilterEditorUI extends Component { public render() { return (
- + - - - + {this.props.mode === 'add' ? panelTitleAdd : panelTitleEdit} { -
- + +
{this.renderIndexPatternInput()} {this.state.isCustomEditorOpen ? this.renderCustomEditor() : this.renderRegularEditor()} @@ -154,9 +165,9 @@ class FilterEditorUI extends Component {
)} +
- - + { isDisabled={!this.isFilterValid()} data-test-subj="saveFilter" > - + {this.props.mode === 'add' ? addButtonLabel : updateButtonLabel} @@ -185,8 +193,8 @@ class FilterEditorUI extends Component { - -
+ +
); } @@ -207,32 +215,31 @@ class FilterEditorUI extends Component { } const { selectedIndexPattern } = this.state; return ( - - - + + - indexPattern.title} - onChange={this.onIndexPatternChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - data-test-subj="filterIndexPatternsSelect" - /> - - - + options={this.props.indexPatterns} + selectedOptions={selectedIndexPattern ? [selectedIndexPattern] : []} + getLabel={(indexPattern) => indexPattern.title} + onChange={this.onIndexPatternChange} + singleSelection={{ asPlainText: true }} + isClearable={false} + data-test-subj="filterIndexPatternsSelect" + /> + + + ); } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx index 3f2aaa50af0fcf..601cf68141c499 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx @@ -11,7 +11,7 @@ import { EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Filter, FILTERS } from '@kbn/data-plugin/common'; import { existsOperator, isOneOfOperator } from './filter_operators'; -import type { FilterLabelStatus } from '../../filter_item'; +import type { FilterLabelStatus } from '../../filter_item/filter_item'; export interface FilterLabelProps { filter: Filter; diff --git a/src/plugins/unified_search/public/filter_bar/_variables.scss b/src/plugins/unified_search/public/filter_bar/filter_item/_variables.scss similarity index 100% rename from src/plugins/unified_search/public/filter_bar/_variables.scss rename to src/plugins/unified_search/public/filter_bar/filter_item/_variables.scss diff --git a/src/plugins/unified_search/public/filter_bar/_global_filter_item.scss b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.scss similarity index 82% rename from src/plugins/unified_search/public/filter_bar/_global_filter_item.scss rename to src/plugins/unified_search/public/filter_bar/filter_item/filter_item.scss index 1c9cea72917708..94f64bdce2f65d 100644 --- a/src/plugins/unified_search/public/filter_bar/_global_filter_item.scss +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.scss @@ -1,3 +1,5 @@ +@import './variables'; + /** * 1. Allow wrapping of long filter items */ @@ -6,26 +8,14 @@ line-height: $euiSize; border: none; color: $euiTextColor; - padding-top: $euiSizeM / 2; - padding-bottom: $euiSizeM / 2; + padding-top: $euiSizeM / 2 + 1px; + padding-bottom: $euiSizeM / 2 + 1px; white-space: normal; /* 1 */ - .euiBadge__childButton { - flex-shrink: 1; /* 1 */ - } - - .euiBadge__iconButton:focus { - background-color: transparentize($euiColorPrimary, .9); - } - &:not(.globalFilterItem-isDisabled) { @include euiFormControlDefaultShadow; box-shadow: #{$euiFormControlBoxShadow}, inset 0 0 0 1px $kbnGlobalFilterItemBorderColor; // Make the actual border more visible } - - &:focus-within { - animation: none !important; // Remove focus ring animation otherwise it overrides simulated border via box-shadow - } } .globalFilterItem-isDisabled { @@ -81,7 +71,7 @@ } .globalFilterItem__editorForm { - padding: $euiSizeM; + padding: $euiSizeS; } .globalFilterItem__popover, diff --git a/src/plugins/unified_search/public/filter_bar/filter_item.tsx b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx similarity index 97% rename from src/plugins/unified_search/public/filter_bar/filter_item.tsx rename to src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx index 16234a2953dc7a..6b06461b4f2979 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_item.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import './filter_item.scss'; + import { EuiContextMenu, EuiPopover, EuiPopoverProps } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n-react'; import { @@ -24,9 +26,9 @@ import { getDisplayValueFromFilter, getFieldDisplayValueFromFilter, } from '@kbn/data-plugin/public'; -import { FilterEditor } from './filter_editor'; -import { FilterView } from './filter_view'; -import { getIndexPatterns } from '../services'; +import { FilterEditor } from '../filter_editor'; +import { FilterView } from '../filter_view'; +import { getIndexPatterns } from '../../services'; type PanelOptions = 'pinFilter' | 'editFilter' | 'negateFilter' | 'disableFilter' | 'deleteFilter'; @@ -101,6 +103,11 @@ export function FilterItem(props: FilterItemProps) { } } + function handleIconClick(e: MouseEvent) { + props.onRemove(); + setIsPopoverOpen(false); + } + function onSubmit(f: Filter) { setIsPopoverOpen(false); props.onUpdate(f); @@ -363,7 +370,7 @@ export function FilterItem(props: FilterItemProps) { filterLabelStatus: valueLabelConfig.status, errorMessage: valueLabelConfig.message, className: getClasses(!!filter.meta.negate, valueLabelConfig), - iconOnClick: props.onRemove, + iconOnClick: handleIconClick, onClick: handleBadgeClick, 'data-test-subj': getDataTestSubj(valueLabelConfig), readonly: props.readonly, diff --git a/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx b/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx new file mode 100644 index 00000000000000..95d49450dd3908 --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useRef } from 'react'; +import { css } from '@emotion/react'; +import { EuiFlexItem } from '@elastic/eui'; +import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; +import type { Filter } from '@kbn/es-query'; +import { IDataPluginServices } from '@kbn/data-plugin/public'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { DataView } from '@kbn/data-views-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { FilterItem } from './filter_item'; + +export interface Props { + filters: Filter[]; + onFiltersUpdated?: (filters: Filter[]) => void; + indexPatterns: DataView[]; + intl: InjectedIntl; + timeRangeForSuggestionsOverride?: boolean; +} + +const FilterItemsUI = React.memo(function FilterItemsUI(props: Props) { + const groupRef = useRef(null); + const kibana = useKibana(); + const { appName, usageCollection, uiSettings } = kibana.services; + if (!uiSettings) return null; + + const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); + + function onFiltersUpdated(filters: Filter[]) { + if (props.onFiltersUpdated) { + props.onFiltersUpdated(filters); + } + } + + function renderItems() { + return props.filters.map((filter, i) => ( + + onUpdate(i, newFilter)} + onRemove={() => onRemove(i)} + indexPatterns={props.indexPatterns} + uiSettings={uiSettings!} + timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride} + /> + + )); + } + + function onRemove(i: number) { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:removed`); + const filters = [...props.filters]; + filters.splice(i, 1); + onFiltersUpdated(filters); + groupRef.current?.focus(); + } + + function onUpdate(i: number, filter: Filter) { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:edited`); + const filters = [...props.filters]; + filters[i] = filter; + onFiltersUpdated(filters); + } + + return <>{renderItems()}; +}); + +const FilterItems = injectI18n(FilterItemsUI); +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default FilterItems; diff --git a/src/plugins/unified_search/public/filter_bar/filter_options.tsx b/src/plugins/unified_search/public/filter_bar/filter_options.tsx deleted file mode 100644 index d2e229c9887119..00000000000000 --- a/src/plugins/unified_search/public/filter_bar/filter_options.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EuiButtonIcon, EuiContextMenu, EuiPopover, EuiPopoverTitle } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n-react'; -import { Component } from 'react'; -import React from 'react'; - -interface Props { - onEnableAll: () => void; - onDisableAll: () => void; - onPinAll: () => void; - onUnpinAll: () => void; - onToggleAllNegated: () => void; - onToggleAllDisabled: () => void; - onRemoveAll: () => void; - intl: InjectedIntl; -} - -interface State { - isPopoverOpen: boolean; -} - -class FilterOptionsUI extends Component { - private buttonRef = React.createRef(); - - public state: State = { - isPopoverOpen: false, - }; - - public togglePopover = () => { - this.setState((prevState) => ({ - isPopoverOpen: !prevState.isPopoverOpen, - })); - }; - - public closePopover = () => { - this.setState({ isPopoverOpen: false }); - this.buttonRef.current?.focus(); - }; - - public render() { - const panelTree = { - id: 0, - items: [ - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.enableAllFiltersButtonLabel', - defaultMessage: 'Enable all', - }), - icon: 'eye', - onClick: () => { - this.closePopover(); - this.props.onEnableAll(); - }, - 'data-test-subj': 'enableAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.disableAllFiltersButtonLabel', - defaultMessage: 'Disable all', - }), - icon: 'eyeClosed', - onClick: () => { - this.closePopover(); - this.props.onDisableAll(); - }, - 'data-test-subj': 'disableAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.pinAllFiltersButtonLabel', - defaultMessage: 'Pin all', - }), - icon: 'pin', - onClick: () => { - this.closePopover(); - this.props.onPinAll(); - }, - 'data-test-subj': 'pinAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.unpinAllFiltersButtonLabel', - defaultMessage: 'Unpin all', - }), - icon: 'pin', - onClick: () => { - this.closePopover(); - this.props.onUnpinAll(); - }, - 'data-test-subj': 'unpinAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.invertNegatedFiltersButtonLabel', - defaultMessage: 'Invert inclusion', - }), - icon: 'invert', - onClick: () => { - this.closePopover(); - this.props.onToggleAllNegated(); - }, - 'data-test-subj': 'invertInclusionAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.invertDisabledFiltersButtonLabel', - defaultMessage: 'Invert enabled/disabled', - }), - icon: 'eye', - onClick: () => { - this.closePopover(); - this.props.onToggleAllDisabled(); - }, - 'data-test-subj': 'invertEnableDisableAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.deleteAllFiltersButtonLabel', - defaultMessage: 'Remove all', - }), - icon: 'trash', - onClick: () => { - this.closePopover(); - this.props.onRemoveAll(); - }, - 'data-test-subj': 'removeAllFilters', - }, - ], - }; - - return ( - - } - anchorPosition="rightUp" - panelPaddingSize="none" - repositionOnScroll - > - - - - - - ); - } -} - -export const FilterOptions = injectI18n(FilterOptionsUI); diff --git a/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx b/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx index 29ff160d50db6a..d399bb0025a109 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import React, { FC } from 'react'; import { Filter, isFilterPinned } from '@kbn/es-query'; import { FilterLabel } from '..'; -import type { FilterLabelStatus } from '../filter_item'; +import type { FilterLabelStatus } from '../filter_item/filter_item'; interface Props { filter: Filter; diff --git a/src/plugins/unified_search/public/filter_bar/index.tsx b/src/plugins/unified_search/public/filter_bar/index.tsx index 70a108f3597906..30f94c3972ee10 100644 --- a/src/plugins/unified_search/public/filter_bar/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/index.tsx @@ -17,6 +17,13 @@ export const FilterBar = (props: React.ComponentProps) => ); +const LazyFilterItems = React.lazy(() => import('./filter_item/filter_items')); +export const FilterItems = (props: React.ComponentProps) => ( + }> + + +); + const LazyFilterLabel = React.lazy(() => import('./filter_editor/lib/filter_label')); export const FilterLabel = (props: React.ComponentProps) => ( }> @@ -24,7 +31,7 @@ export const FilterLabel = (props: React.ComponentProps) ); -const LazyFilterItem = React.lazy(() => import('./filter_item')); +const LazyFilterItem = React.lazy(() => import('./filter_item/filter_item')); export const FilterItem = (props: React.ComponentProps) => ( }> diff --git a/src/plugins/unified_search/public/index.scss b/src/plugins/unified_search/public/index.scss index 7f7704c64e9b41..72e1c0c313f74f 100755 --- a/src/plugins/unified_search/public/index.scss +++ b/src/plugins/unified_search/public/index.scss @@ -3,5 +3,3 @@ @import './saved_query_management/index'; @import './query_string_input/index'; - -@import './filter_bar/index'; diff --git a/src/plugins/unified_search/public/index.ts b/src/plugins/unified_search/public/index.ts index 93805c6cfec1c0..bc7974b42efb35 100755 --- a/src/plugins/unified_search/public/index.ts +++ b/src/plugins/unified_search/public/index.ts @@ -15,6 +15,8 @@ export type { StatefulSearchBarProps, SearchBarProps } from './search_bar'; export type { UnifiedSearchPublicPluginStart, UnifiedSearchPluginSetup } from './types'; export { SearchBar } from './search_bar'; export { FilterLabel, FilterItem } from './filter_bar'; +export { DataViewsList } from './dataview_picker/dataview_list'; +export { DataViewPicker } from './dataview_picker'; export type { ApplyGlobalFilterActionContext } from './actions'; export { ACTION_GLOBAL_APPLY_FILTER } from './actions'; diff --git a/src/plugins/unified_search/public/plugin.ts b/src/plugins/unified_search/public/plugin.ts index 5ba24740662759..26727b56094a00 100755 --- a/src/plugins/unified_search/public/plugin.ts +++ b/src/plugins/unified_search/public/plugin.ts @@ -5,9 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import './index.scss'; - import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { Storage, IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; diff --git a/src/plugins/unified_search/public/query_string_input/_query_bar.scss b/src/plugins/unified_search/public/query_string_input/_query_bar.scss index f8c2f067d9ec5c..7b9a735d1556fc 100644 --- a/src/plugins/unified_search/public/query_string_input/_query_bar.scss +++ b/src/plugins/unified_search/public/query_string_input/_query_bar.scss @@ -1,61 +1,34 @@ .kbnQueryBar__wrap { - max-width: 100%; + width: 100%; z-index: $euiZContentMenu; -} + height: $euiFormControlHeight; + display: flex; -// Uses the append style, but no bordering -.kqlQueryBar__languageSwitcherButton { - border-right: none !important; - border-left: $euiFormInputGroupBorder; + > [aria-expanded='true'] { + // Using filter allows it to adhere the children's bounds + filter: drop-shadow(0 5.7px 12px rgba($euiShadowColor, shadowOpacity(.05))); + } } .kbnQueryBar__textareaWrap { + position: relative; overflow: visible !important; // Override EUI form control display: flex; flex: 1 1 100%; - position: relative; - background-color: $euiFormBackgroundColor; - border-radius: $euiFormControlBorderRadius; - - &.kbnQueryBar__textareaWrap--hasPrepend { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - - &.kbnQueryBar__textareaWrap--hasAppend { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } } .kbnQueryBar__textarea { z-index: $euiZContentMenu; resize: none !important; // When in the group, it will autosize - height: $euiFormControlHeight - 2px; + height: $euiFormControlHeight; // Unlike most inputs within layout control groups, the text area still needs a border // for multi-line content. These adjusts help it sit above the control groups // shadow to line up correctly. - padding: $euiSizeS; - box-shadow: 0 0 0 1px $euiFormBorderColor; - padding-bottom: $euiSizeS + 1px; + padding: ($euiSizeS + 2px) $euiSizeS $euiSizeS; // Firefox adds margin to textarea margin: 0; - &.kbnQueryBar__textarea--hasPrepend { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - &.kbnQueryBar__textarea--hasAppend { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - &:not(.kbnQueryBar__textarea--autoHeight):not(:invalid) { - @include euiYScrollWithShadows; - } - &:not(.kbnQueryBar__textarea--autoHeight) { - white-space: nowrap; overflow-y: hidden; overflow-x: hidden; } @@ -65,18 +38,35 @@ overflow-x: auto; overflow-y: auto; white-space: normal; - box-shadow: 0 0 0 1px $euiFormBorderColor; + + } + + &.kbnQueryBar__textarea--isSuggestionsVisible { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + + &--isClearable { + @include euiFormControlWithIcon($isIconOptional: false, $side: 'right'); } @include euiFormControlWithIcon($isIconOptional: true); + ~ .euiFormControlLayoutIcons { // By default form control layout icon is vertically centered, but our textarea // can expand to be multi-line, so we position it with padding that matches // the parent textarea padding z-index: $euiZContentMenu + 1; - top: $euiSizeS + 3px; + top: $euiSizeM; bottom: unset; } + + &--withPrepend { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + margin-left: -1px; + width: calc(100% + 1px); + } } .kbnQueryBar__datePickerWrapper { @@ -92,32 +82,3 @@ } } } - -@include euiBreakpoint('xs', 's') { - .kbnQueryBar--withDatePicker { - > :first-child { - // Change the order of the query bar and date picker so that the date picker is top and the query bar still aligns with filters - order: 1; - // EUI Flexbox adds too much margin between responded items, this just moves it up - margin-top: $euiSizeS * -1; - } - } -} - -// IE specific fix for the datepicker to not collapse -@include euiBreakpoint('m', 'l', 'xl') { - .kbnQueryBar__datePickerWrapper { - max-width: 40vw; - // sass-lint:disable-block no-important - flex-grow: 0 !important; - flex-basis: auto !important; - - &.kbnQueryBar__datePickerWrapper-isHidden { - // sass-lint:disable-block no-important - margin-right: -$euiSizeXS !important; - width: 0; - overflow: hidden; - max-width: 0; - } - } -} diff --git a/src/plugins/unified_search/public/query_string_input/add_filter_popover.tsx b/src/plugins/unified_search/public/query_string_input/add_filter_popover.tsx new file mode 100644 index 00000000000000..b86d7d7f02498a --- /dev/null +++ b/src/plugins/unified_search/public/query_string_input/add_filter_popover.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexItem, + EuiButtonIcon, + EuiPopover, + EuiButtonIconProps, + EuiToolTip, +} from '@elastic/eui'; +import { Filter } from '@kbn/es-query'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { FilterEditorWrapper } from './filter_editor_wrapper'; + +interface AddFilterPopoverProps { + indexPatterns?: Array; + filters: Filter[]; + timeRangeForSuggestionsOverride?: boolean; + onFiltersUpdated?: (filters: Filter[]) => void; + buttonProps?: Partial; +} + +export const AddFilterPopover = React.memo(function AddFilterPopover({ + indexPatterns, + filters, + timeRangeForSuggestionsOverride, + onFiltersUpdated, + buttonProps, +}: AddFilterPopoverProps) { + const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); + + const buttonIconLabel = i18n.translate('unifiedSearch.filter.filterBar.addFilterButtonLabel', { + defaultMessage: 'Add filter', + }); + + const button = ( + + setIsAddFilterPopoverOpen(!isAddFilterPopoverOpen)} + size="m" + {...buttonProps} + /> + + ); + + return ( + + setIsAddFilterPopoverOpen(false)} + anchorPosition="downLeft" + panelPaddingSize="none" + initialFocus=".filterEditor__hiddenItem" + ownFocus + repositionOnScroll + > + setIsAddFilterPopoverOpen(false)} + /> + + + ); +}); diff --git a/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx b/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx new file mode 100644 index 00000000000000..dd106607353f2f --- /dev/null +++ b/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useEffect } from 'react'; +import { Filter, buildEmptyFilter } from '@kbn/es-query'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import { IDataPluginServices } from '@kbn/data-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { FILTER_EDITOR_WIDTH } from '../filter_bar/filter_item/filter_item'; +import { FilterEditor } from '../filter_bar/filter_editor'; +import { fetchIndexPatterns } from './fetch_index_patterns'; + +interface FilterEditorWrapperProps { + indexPatterns?: Array; + filters: Filter[]; + timeRangeForSuggestionsOverride?: boolean; + closePopover?: () => void; + onFiltersUpdated?: (filters: Filter[]) => void; +} + +export const FilterEditorWrapper = React.memo(function FilterEditorWrapper({ + indexPatterns, + filters, + timeRangeForSuggestionsOverride, + closePopover, + onFiltersUpdated, +}: FilterEditorWrapperProps) { + const kibana = useKibana(); + const { uiSettings, data, usageCollection, appName } = kibana.services; + const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); + const [dataViews, setDataviews] = useState([]); + const [newFilter, setNewFilter] = useState(undefined); + const isPinned = uiSettings!.get(UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT); + + useEffect(() => { + const fetchDataViews = async () => { + const stringPatterns = indexPatterns?.filter( + (indexPattern) => typeof indexPattern === 'string' + ) as string[]; + const objectPatterns = indexPatterns?.filter( + (indexPattern) => typeof indexPattern !== 'string' + ) as DataView[]; + + const objectPatternsFromStrings = (await fetchIndexPatterns( + data.dataViews, + stringPatterns + )) as DataView[]; + setDataviews([...objectPatterns, ...objectPatternsFromStrings]); + const [dataView] = [...objectPatterns, ...objectPatternsFromStrings]; + const index = dataView && dataView.id; + const emptyFilter = buildEmptyFilter(isPinned, index); + setNewFilter(emptyFilter); + }; + if (indexPatterns) { + fetchDataViews(); + } + }, [data.dataViews, indexPatterns, isPinned]); + + function onAdd(filter: Filter) { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:added`); + closePopover?.(); + const updatedFilters = [...filters, filter]; + onFiltersUpdated?.(updatedFilters); + } + + return ( +
+ {newFilter && ( + closePopover?.()} + key={JSON.stringify(newFilter)} + timeRangeForSuggestionsOverride={timeRangeForSuggestionsOverride} + mode="add" + /> + )} +
+ ); +}); diff --git a/src/plugins/unified_search/public/query_string_input/language_switcher.test.tsx b/src/plugins/unified_search/public/query_string_input/language_switcher.test.tsx index 0223fc85a3ddbe..591fe943607935 100644 --- a/src/plugins/unified_search/public/query_string_input/language_switcher.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/language_switcher.test.tsx @@ -11,7 +11,7 @@ import { QueryLanguageSwitcher, QueryLanguageSwitcherProps } from './language_sw import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { coreMock } from '@kbn/core/public/mocks'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { EuiButtonEmpty, EuiIcon, EuiPopover } from '@elastic/eui'; +import { EuiButtonIcon, EuiIcon, EuiPopover } from '@elastic/eui'; const startMock = coreMock.createStart(); describe('LanguageSwitcher', () => { @@ -28,7 +28,7 @@ describe('LanguageSwitcher', () => { ); } - it('should toggle off if language is lucene', () => { + it('should select the lucene context menu if language is lucene', () => { const component = mountWithIntl( wrapInContext({ language: 'lucene', @@ -37,12 +37,14 @@ describe('LanguageSwitcher', () => { }, }) ); - component.find(EuiButtonEmpty).simulate('click'); + component.find(EuiButtonIcon).simulate('click'); expect(component.find(EuiPopover).prop('isOpen')).toBe(true); - expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeFalsy(); + expect(component.find('[data-test-subj="luceneLanguageMenuItem"]').get(0).props.icon).toBe( + 'check' + ); }); - it('should toggle on if language is kuery', () => { + it('should select the kql context menu if language is kuery', () => { const component = mountWithIntl( wrapInContext({ language: 'kuery', @@ -51,12 +53,14 @@ describe('LanguageSwitcher', () => { }, }) ); - component.find(EuiButtonEmpty).simulate('click'); + component.find(EuiButtonIcon).simulate('click'); expect(component.find(EuiPopover).prop('isOpen')).toBe(true); - expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeTruthy(); + expect(component.find('[data-test-subj="kqlLanguageMenuItem"]').get(0).props.icon).toBe( + 'check' + ); }); - it('should toggle off if language is text', () => { + it('should select the lucene context menu if language is text', () => { const component = mountWithIntl( wrapInContext({ language: 'text', @@ -65,9 +69,11 @@ describe('LanguageSwitcher', () => { }, }) ); - component.find(EuiButtonEmpty).simulate('click'); + component.find(EuiButtonIcon).simulate('click'); expect(component.find(EuiPopover).prop('isOpen')).toBe(true); - expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeFalsy(); + expect(component.find('[data-test-subj="luceneLanguageMenuItem"]').get(0).props.icon).toBe( + 'check' + ); }); it('it set language on nonKql mode text', () => { const onSelectLanguage = jest.fn(); @@ -79,11 +85,13 @@ describe('LanguageSwitcher', () => { onSelectLanguage, }) ); - component.find(EuiButtonEmpty).simulate('click'); + component.find(EuiButtonIcon).simulate('click'); expect(component.find(EuiPopover).prop('isOpen')).toBe(true); - expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeTruthy(); + expect(component.find('[data-test-subj="kqlLanguageMenuItem"]').get(0).props.icon).toBe( + 'check' + ); - component.find('[data-test-subj="languageToggle"]').at(1).simulate('click'); + component.find('[data-test-subj="luceneLanguageMenuItem"]').at(1).simulate('click'); expect(onSelectLanguage).toHaveBeenCalledWith('text'); }); @@ -97,8 +105,8 @@ describe('LanguageSwitcher', () => { onSelectLanguage, }) ); - component.find(EuiButtonEmpty).simulate('click'); - component.find('[data-test-subj="languageToggle"]').at(1).simulate('click'); + component.find(EuiButtonIcon).simulate('click'); + component.find('[data-test-subj="luceneLanguageMenuItem"]').at(1).simulate('click'); expect(onSelectLanguage).toHaveBeenCalledWith('lucene'); }); @@ -114,10 +122,10 @@ describe('LanguageSwitcher', () => { }) ); - expect(component.find(EuiIcon).prop('type')).toBe('boxesVertical'); + expect(component.find(EuiIcon).prop('type')).toBe('filter'); - component.find(EuiButtonEmpty).simulate('click'); - component.find('[data-test-subj="languageToggle"]').at(1).simulate('click'); + component.find(EuiButtonIcon).simulate('click'); + component.find('[data-test-subj="kqlLanguageMenuItem"]').at(1).simulate('click'); expect(onSelectLanguage).toHaveBeenCalledWith('kuery'); }); @@ -132,13 +140,12 @@ describe('LanguageSwitcher', () => { onSelectLanguage, }) ); - - expect(component.find('[data-test-subj="switchQueryLanguageButton"]').at(0).text()).toBe( - 'Lucene' + component.find(EuiButtonIcon).simulate('click'); + expect(component.find('[data-test-subj="luceneLanguageMenuItem"]').get(0).props.icon).toBe( + 'check' ); - component.find(EuiButtonEmpty).simulate('click'); - component.find('[data-test-subj="languageToggle"]').at(1).simulate('click'); + component.find('[data-test-subj="kqlLanguageMenuItem"]').at(1).simulate('click'); expect(onSelectLanguage).toHaveBeenCalledWith('kuery'); }); diff --git a/src/plugins/unified_search/public/query_string_input/language_switcher.tsx b/src/plugins/unified_search/public/query_string_input/language_switcher.tsx index db42339e464c36..a48901ef17f861 100644 --- a/src/plugins/unified_search/public/query_string_input/language_switcher.tsx +++ b/src/plugins/unified_search/public/query_string_input/language_switcher.tsx @@ -7,17 +7,13 @@ */ import { - EuiButtonEmpty, - EuiForm, - EuiFormRow, - EuiIcon, - EuiLink, EuiPopover, EuiPopoverTitle, - EuiSpacer, - EuiSwitch, - EuiText, PopoverAnchorPosition, + EuiContextMenuItem, + toSentenceCase, + EuiHorizontalRule, + EuiButtonIcon, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; @@ -29,7 +25,7 @@ export interface QueryLanguageSwitcherProps { onSelectLanguage: (newLanguage: string) => void; anchorPosition?: PopoverAnchorPosition; nonKqlMode?: 'lucene' | 'text'; - nonKqlModeHelpText?: string; + isOnTopBarMenu?: boolean; } export const QueryLanguageSwitcher = React.memo(function QueryLanguageSwitcher({ @@ -37,124 +33,78 @@ export const QueryLanguageSwitcher = React.memo(function QueryLanguageSwitcher({ anchorPosition, onSelectLanguage, nonKqlMode = 'lucene', - nonKqlModeHelpText, + isOnTopBarMenu, }: QueryLanguageSwitcherProps) { const kibana = useKibana(); const kueryQuerySyntaxDocs = kibana.services.docLinks!.links.query.kueryQuerySyntax; const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const luceneLabel = ( - - ); - const kqlLabel = ( - - ); - - const kqlFullName = ( - - ); - - const kqlModeTitle = i18n.translate('unifiedSearch.query.queryBar.languageSwitcher.toText', { - defaultMessage: 'Switch to Kibana Query Language for search', - }); const button = ( - setIsPopoverOpen(!isPopoverOpen)} className="euiFormControlLayout__append kqlQueryBar__languageSwitcherButton" data-test-subj={'switchQueryLanguageButton'} - > - {language === 'kuery' ? ( - kqlLabel - ) : nonKqlMode === 'lucene' ? ( - luceneLabel - ) : ( - - )} - + aria-label={i18n.translate('unifiedSearch.switchLanguage.buttonText', { + defaultMessage: 'Switch language button.', + })} + /> + ); + + const languageMenuItem = ( +
+ { + onSelectLanguage('kuery'); + }} + > + KQL + + { + onSelectLanguage(nonKqlMode); + }} + > + {toSentenceCase(nonKqlMode)} + + + + Documentation + +
); - return ( + const languageQueryStringComponent = ( setIsPopoverOpen(false)} repositionOnScroll - ownFocus={true} - initialFocus={'[role="switch"]'} + panelPaddingSize="none" > - + -
- -

- - {kqlFullName} - - ), - nonKqlModeHelpText: - nonKqlModeHelpText || - i18n.translate( - 'unifiedSearch.query.queryBar.syntaxOptionsDescription.nonKqlModeHelpText', - { - defaultMessage: 'Kibana uses Lucene.', - } - ), - }} - /> -

-
- - - - - - - ) : ( - - ) - } - checked={language === 'kuery'} - onChange={() => { - const newLanguage = language === 'kuery' ? nonKqlMode : 'kuery'; - onSelectLanguage(newLanguage); - }} - data-test-subj="languageToggle" - /> - - -
+ {languageMenuItem}
); + + return Boolean(isOnTopBarMenu) ? languageMenuItem : languageQueryStringComponent; }); diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx new file mode 100644 index 00000000000000..7a04b92c7e0637 --- /dev/null +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx @@ -0,0 +1,278 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { act } from 'react-dom/test-utils'; +import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { coreMock } from '@kbn/core/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { Filter } from '@kbn/es-query'; +import { QueryBarMenuProps, QueryBarMenu } from './query_bar_menu'; +import { EuiPopover } from '@elastic/eui'; + +describe('Querybar Menu component', () => { + const createMockWebStorage = () => ({ + clear: jest.fn(), + getItem: jest.fn(), + key: jest.fn(), + removeItem: jest.fn(), + setItem: jest.fn(), + length: 0, + }); + + const createMockStorage = () => ({ + storage: createMockWebStorage(), + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }); + const getStorage = (v: string) => { + const storage = createMockStorage(); + storage.get.mockReturnValue(v); + return storage; + }; + + const startMock = coreMock.createStart(); + let dataMock = dataPluginMock.createStartContract(); + function wrapQueryBarMenuComponentInContext(testProps: QueryBarMenuProps, storageValue: string) { + dataMock = { + ...dataMock, + dataViews: { + ...dataMock.dataViews, + getIdsWithTitle: jest.fn(), + }, + }; + const services = { + data: dataMock, + storage: getStorage(storageValue), + uiSettings: startMock.uiSettings, + }; + + return ( + + + + + + ); + } + let props: QueryBarMenuProps; + beforeEach(() => { + props = { + language: 'kuery', + onQueryChange: jest.fn(), + onQueryBarSubmit: jest.fn(), + toggleFilterBarMenuPopover: jest.fn(), + openQueryBarMenu: false, + savedQueryService: { + ...dataMock.query.savedQueries, + findSavedQueries: jest.fn().mockResolvedValue({ + queries: [ + { + id: '8a0b7cd0-b0c4-11ec-92b2-73d62e0d28a9', + attributes: { + title: 'Test', + description: '', + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + filters: [], + }, + }, + ], + }), + }, + }; + }); + it('should not render the popover if the openQueryBarMenu prop is false', async () => { + await act(async () => { + const component = mount(wrapQueryBarMenuComponentInContext(props, 'kuery')); + expect(component.find(EuiPopover).prop('isOpen')).toBe(false); + }); + }); + + it('should render the popover if the openQueryBarMenu prop is true', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + }; + await act(async () => { + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + expect(component.find(EuiPopover).prop('isOpen')).toBe(true); + }); + }); + + it('should render the context menu by default', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + expect(component.find('[data-test-subj="queryBarMenuPanel"]')).toBeTruthy(); + }); + + it('should render the saved filter sets panels if the showQueryInput prop is true but disabled', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + showQueryInput: true, + showFilterBar: true, + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + const saveFilterSetButton = component.find( + '[data-test-subj="saved-query-management-save-button"]' + ); + const loadFilterSetButton = component.find( + '[data-test-subj="saved-query-management-load-button"]' + ); + expect(saveFilterSetButton.length).toBeTruthy(); + expect(saveFilterSetButton.first().prop('disabled')).toBe(true); + expect(loadFilterSetButton.length).toBeTruthy(); + expect(loadFilterSetButton.first().prop('disabled')).toBe(true); + }); + + it('should render the filter sets panels if the showFilterBar is true but disabled', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + showFilterBar: true, + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + const applyToAllFiltersButton = component.find( + '[data-test-subj="filter-sets-applyToAllFilters"]' + ); + const removeAllFiltersButton = component.find( + '[data-test-subj="filter-sets-removeAllFilters"]' + ); + expect(applyToAllFiltersButton.length).toBeTruthy(); + expect(applyToAllFiltersButton.first().prop('disabled')).toBe(true); + expect(removeAllFiltersButton.length).toBeTruthy(); + expect(removeAllFiltersButton.first().prop('disabled')).toBe(true); + }); + + it('should enable the clear all button if query is given', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + showFilterBar: true, + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + const removeAllFiltersButton = component.find( + '[data-test-subj="filter-sets-removeAllFilters"]' + ); + expect(removeAllFiltersButton.first().prop('disabled')).toBe(false); + }); + + it('should enable the apply to all button if filter is given', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + showFilterBar: true, + filters: [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ] as Filter[], + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + const applyToAllFiltersButton = component.find( + '[data-test-subj="filter-sets-applyToAllFilters"]' + ); + expect(applyToAllFiltersButton.first().prop('disabled')).toBe(false); + }); + + it('should render the language switcher panel', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + showFilterBar: true, + showQueryInput: true, + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + const languageSwitcher = component.find('[data-test-subj="switchQueryLanguageButton"]'); + expect(languageSwitcher.length).toBeTruthy(); + }); + + it('should render the save query quick buttons', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + showSaveQuery: true, + filters: [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ] as Filter[], + savedQuery: { + id: '8a0b7cd0-b0c4-11ec-92b2-73d62e0d28a9', + attributes: { + title: 'Test', + description: '', + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + filters: [], + }, + }, + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + const saveChangesButton = component.find( + '[data-test-subj="saved-query-management-save-changes-button"]' + ); + expect(saveChangesButton.length).toBeTruthy(); + const saveChangesAsNewButton = component.find( + '[data-test-subj="saved-query-management-save-as-new-button"]' + ); + expect(saveChangesAsNewButton.length).toBeTruthy(); + }); +}); diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx new file mode 100644 index 00000000000000..2b34aef33eeeeb --- /dev/null +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useEffect } from 'react'; +import { + EuiButtonIcon, + EuiContextMenu, + EuiContextMenuPanel, + EuiPopover, + useGeneratedHtmlId, + EuiButtonIconProps, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { Filter, Query } from '@kbn/es-query'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { TimeRange, SavedQueryService, SavedQuery } from '@kbn/data-plugin/public'; +import { QueryBarMenuPanels } from './query_bar_menu_panels'; +import { FilterEditorWrapper } from './filter_editor_wrapper'; + +export interface QueryBarMenuProps { + language: string; + onQueryChange: (payload: { dateRange: TimeRange; query?: Query }) => void; + onQueryBarSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void; + toggleFilterBarMenuPopover: (value: boolean) => void; + openQueryBarMenu: boolean; + nonKqlMode?: 'lucene' | 'text'; + dateRangeFrom?: string; + dateRangeTo?: string; + savedQueryService: SavedQueryService; + saveAsNewQueryFormComponent?: JSX.Element; + saveFormComponent?: JSX.Element; + manageFilterSetComponent?: JSX.Element; + onFiltersUpdated?: (filters: Filter[]) => void; + filters?: Filter[]; + query?: Query; + savedQuery?: SavedQuery; + onClearSavedQuery?: () => void; + showQueryInput?: boolean; + showFilterBar?: boolean; + showSaveQuery?: boolean; + timeRangeForSuggestionsOverride?: boolean; + indexPatterns?: Array; + buttonProps?: Partial; +} + +export function QueryBarMenu({ + language, + nonKqlMode, + dateRangeFrom, + dateRangeTo, + onQueryChange, + onQueryBarSubmit, + savedQueryService, + saveAsNewQueryFormComponent, + saveFormComponent, + manageFilterSetComponent, + openQueryBarMenu, + toggleFilterBarMenuPopover, + onFiltersUpdated, + filters, + query, + savedQuery, + onClearSavedQuery, + showQueryInput, + showFilterBar, + showSaveQuery, + indexPatterns, + timeRangeForSuggestionsOverride, + buttonProps, +}: QueryBarMenuProps) { + const [renderedComponent, setRenderedComponent] = useState('menu'); + + useEffect(() => { + if (openQueryBarMenu) { + setRenderedComponent('menu'); + } + }, [openQueryBarMenu]); + + const normalContextMenuPopoverId = useGeneratedHtmlId({ + prefix: 'normalContextMenuPopover', + }); + const onButtonClick = () => { + toggleFilterBarMenuPopover(!openQueryBarMenu); + }; + + const closePopover = () => { + toggleFilterBarMenuPopover(false); + }; + + const buttonLabel = i18n.translate('unifiedSearch.filter.options.filterSetButtonLabel', { + defaultMessage: 'Filter set menu', + }); + + const button = ( + + + + ); + + const panels = QueryBarMenuPanels({ + filters, + savedQuery, + language, + dateRangeFrom, + dateRangeTo, + query, + showSaveQuery, + showFilterBar, + showQueryInput, + savedQueryService, + saveAsNewQueryFormComponent, + manageFilterSetComponent, + nonKqlMode, + closePopover, + onQueryBarSubmit, + onFiltersUpdated, + onClearSavedQuery, + onQueryChange, + setRenderedComponent, + }); + + const renderComponent = () => { + switch (renderedComponent) { + case 'menu': + default: + return ( + + ); + case 'saveForm': + return ( + {saveFormComponent}]} /> + ); + case 'saveAsNewForm': + return ( + {saveAsNewQueryFormComponent}]} + /> + ); + case 'addFilter': + return ( + , + ]} + /> + ); + } + }; + + return ( + <> + + {renderComponent()} + + + ); +} diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx new file mode 100644 index 00000000000000..de70e66fda5fcf --- /dev/null +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx @@ -0,0 +1,494 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { isEqual } from 'lodash'; +import { + EuiContextMenuPanelDescriptor, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; +import { + Filter, + Query, + enableFilter, + disableFilter, + toggleFilterNegated, + pinFilter, + unpinFilter, +} from '@kbn/es-query'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { KIBANA_USER_QUERY_LANGUAGE_KEY, UI_SETTINGS } from '@kbn/data-plugin/common'; +import type { + IDataPluginServices, + TimeRange, + SavedQueryService, + SavedQuery, +} from '@kbn/data-plugin/public'; +import { fromUser } from './from_user'; +import { QueryLanguageSwitcher } from './language_switcher'; + +interface QueryBarMenuPanelProps { + filters?: Filter[]; + savedQuery?: SavedQuery; + language: string; + dateRangeFrom?: string; + dateRangeTo?: string; + query?: Query; + showSaveQuery?: boolean; + showQueryInput?: boolean; + showFilterBar?: boolean; + savedQueryService: SavedQueryService; + saveAsNewQueryFormComponent?: JSX.Element; + manageFilterSetComponent?: JSX.Element; + nonKqlMode?: 'lucene' | 'text'; + closePopover: () => void; + onQueryBarSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void; + onFiltersUpdated?: (filters: Filter[]) => void; + onClearSavedQuery?: () => void; + onQueryChange: (payload: { dateRange: TimeRange; query?: Query }) => void; + setRenderedComponent: (component: string) => void; +} + +export function QueryBarMenuPanels({ + filters, + savedQuery, + language, + dateRangeFrom, + dateRangeTo, + query, + showSaveQuery, + showFilterBar, + showQueryInput, + savedQueryService, + saveAsNewQueryFormComponent, + manageFilterSetComponent, + nonKqlMode, + closePopover, + onQueryBarSubmit, + onFiltersUpdated, + onClearSavedQuery, + onQueryChange, + setRenderedComponent, +}: QueryBarMenuPanelProps) { + const kibana = useKibana(); + const { appName, usageCollection, uiSettings, http, storage } = kibana.services; + const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); + const cancelPendingListingRequest = useRef<() => void>(() => {}); + + const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]); + const [hasFiltersOrQuery, setHasFiltersOrQuery] = useState(false); + const [savedQueryHasChanged, setSavedQueryHasChanged] = useState(false); + + useEffect(() => { + const fetchSavedQueries = async () => { + cancelPendingListingRequest.current(); + let requestGotCancelled = false; + cancelPendingListingRequest.current = () => { + requestGotCancelled = true; + }; + + const { queries: savedQueryItems } = await savedQueryService.findSavedQueries(''); + + if (requestGotCancelled) return; + + setSavedQueries(savedQueryItems.reverse().slice(0, 5)); + }; + if (showQueryInput && showFilterBar) { + fetchSavedQueries(); + } + }, [savedQueryService, savedQuery, showQueryInput, showFilterBar]); + + useEffect(() => { + if (savedQuery) { + let filtersHaveChanged = filters?.length !== savedQuery.attributes?.filters?.length; + if (filters?.length === savedQuery.attributes?.filters?.length) { + filtersHaveChanged = Boolean( + filters?.some( + (filter, index) => + !isEqual(filter.query, savedQuery.attributes?.filters?.[index]?.query) + ) + ); + } + if (filtersHaveChanged || !isEqual(query, savedQuery?.attributes.query)) { + setSavedQueryHasChanged(true); + } else { + setSavedQueryHasChanged(false); + } + } + }, [filters, query, savedQuery, savedQuery?.attributes.filters, savedQuery?.attributes.query]); + + useEffect(() => { + const hasFilters = Boolean(filters && filters.length > 0); + const hasQuery = Boolean(query && query.query); + setHasFiltersOrQuery(hasFilters || hasQuery); + }, [filters, onClearSavedQuery, query, savedQuery]); + + const getDateRange = () => { + const defaultTimeSetting = uiSettings!.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS); + return { + from: dateRangeFrom || defaultTimeSetting.from, + to: dateRangeTo || defaultTimeSetting.to, + }; + }; + + const handleSaveAsNew = useCallback(() => { + setRenderedComponent('saveAsNewForm'); + }, [setRenderedComponent]); + + const handleSave = useCallback(() => { + setRenderedComponent('saveForm'); + }, [setRenderedComponent]); + + const onEnableAll = () => { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:enable_all`); + const enabledFilters = filters?.map(enableFilter); + if (enabledFilters) { + onFiltersUpdated?.(enabledFilters); + } + }; + + const onDisableAll = () => { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:disable_all`); + const disabledFilters = filters?.map(disableFilter); + if (disabledFilters) { + onFiltersUpdated?.(disabledFilters); + } + }; + + const onToggleAllNegated = () => { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:invert_all`); + const negatedFilters = filters?.map(toggleFilterNegated); + if (negatedFilters) { + onFiltersUpdated?.(negatedFilters); + } + }; + + const onRemoveAll = () => { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:remove_all`); + onFiltersUpdated?.([]); + }; + + const onPinAll = () => { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:pin_all`); + const pinnedFilters = filters?.map(pinFilter); + if (pinnedFilters) { + onFiltersUpdated?.(pinnedFilters); + } + }; + + const onUnpinAll = () => { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:unpin_all`); + const unPinnedFilters = filters?.map(unpinFilter); + if (unPinnedFilters) { + onFiltersUpdated?.(unPinnedFilters); + } + }; + + const onQueryStringChange = (value: string) => { + onQueryChange({ + query: { query: value, language }, + dateRange: getDateRange(), + }); + }; + + const onSelectLanguage = (lang: string) => { + http.post('/api/kibana/kql_opt_in_stats', { + body: JSON.stringify({ opt_in: lang === 'kuery' }), + }); + + const storageKey = KIBANA_USER_QUERY_LANGUAGE_KEY; + storage.set(storageKey!, lang); + + const newQuery = { query: '', language: lang }; + onQueryStringChange(newQuery.query); + onQueryBarSubmit({ + query: { query: fromUser(newQuery.query), language: newQuery.language }, + dateRange: getDateRange(), + }); + }; + + const luceneLabel = i18n.translate('unifiedSearch.query.queryBar.luceneLanguageName', { + defaultMessage: 'Lucene', + }); + const kqlLabel = i18n.translate('unifiedSearch.query.queryBar.kqlLanguageName', { + defaultMessage: 'KQL', + }); + + const filtersRelatedPanels = [ + { + name: i18n.translate('unifiedSearch.filter.options.addFilterButtonLabel', { + defaultMessage: 'Add filter', + }), + icon: 'plus', + onClick: () => { + setRenderedComponent('addFilter'); + }, + }, + { + name: i18n.translate('unifiedSearch.filter.options.applyAllFiltersButtonLabel', { + defaultMessage: 'Apply to all', + }), + icon: 'filter', + panel: 2, + disabled: !Boolean(filters && filters.length > 0), + 'data-test-subj': 'filter-sets-applyToAllFilters', + }, + ]; + + const queryAndFiltersRelatedPanels = [ + { + name: savedQuery + ? i18n.translate('unifiedSearch.filter.options.loadOtherFilterSetLabel', { + defaultMessage: 'Load other filter set', + }) + : i18n.translate('unifiedSearch.filter.options.loadCurrentFilterSetLabel', { + defaultMessage: 'Load filter set', + }), + panel: 4, + width: 350, + icon: 'filter', + 'data-test-subj': 'saved-query-management-load-button', + disabled: !savedQueries.length, + }, + { + name: savedQuery + ? i18n.translate('unifiedSearch.filter.options.saveAsNewFilterSetLabel', { + defaultMessage: 'Save as new', + }) + : i18n.translate('unifiedSearch.filter.options.saveFilterSetLabel', { + defaultMessage: 'Save filter set', + }), + icon: 'save', + disabled: + !Boolean(showSaveQuery) || !hasFiltersOrQuery || (savedQuery && !savedQueryHasChanged), + panel: 1, + 'data-test-subj': 'saved-query-management-save-button', + }, + { isSeparator: true }, + ]; + + const items = []; + // apply to all actions are only shown when there are filters + if (showFilterBar) { + items.push(...filtersRelatedPanels); + } + // clear all actions are only shown when there are filters or query + if (showFilterBar || showQueryInput) { + items.push( + { + name: i18n.translate('unifiedSearch.filter.options.clearllFiltersButtonLabel', { + defaultMessage: 'Clear all', + }), + disabled: !hasFiltersOrQuery && !Boolean(savedQuery), + icon: 'crossInACircleFilled', + 'data-test-subj': 'filter-sets-removeAllFilters', + onClick: () => { + closePopover(); + onQueryBarSubmit({ + query: { query: '', language }, + dateRange: getDateRange(), + }); + onRemoveAll(); + onClearSavedQuery?.(); + }, + }, + { isSeparator: true } + ); + } + // saved queries actions are only shown when the showQueryInput and showFilterBar is true + if (showQueryInput && showFilterBar) { + items.push(...queryAndFiltersRelatedPanels); + } + + // language menu appears when the showQueryInput is true + if (showQueryInput) { + items.push({ + name: `Language: ${language === 'kuery' ? kqlLabel : luceneLabel}`, + panel: 3, + 'data-test-subj': 'switchQueryLanguageButton', + }); + } + + const panels = [ + { + id: 0, + title: ( + <> + + + + {savedQuery ? savedQuery.attributes.title : 'Filter set'} + + + {savedQuery && savedQueryHasChanged && Boolean(showSaveQuery) && hasFiltersOrQuery && ( + + + + + {i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText', + { + defaultMessage: 'Save changes', + } + )} + + + + + {i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText', + { + defaultMessage: 'Save as new', + } + )} + + + + + )} + + + ), + items, + }, + { + id: 1, + title: i18n.translate('unifiedSearch.filter.options.saveCurrentFilterSetLabel', { + defaultMessage: 'Save current filter set', + }), + disabled: !Boolean(showSaveQuery), + content:
{saveAsNewQueryFormComponent}
, + }, + { + id: 2, + initialFocusedItemIndex: 1, + title: i18n.translate('unifiedSearch.filter.options.applyAllFiltersButtonLabel', { + defaultMessage: 'Apply to all', + }), + items: [ + { + name: i18n.translate('unifiedSearch.filter.options.enableAllFiltersButtonLabel', { + defaultMessage: 'Enable all', + }), + icon: 'eye', + 'data-test-subj': 'filter-sets-enableAllFilters', + onClick: () => { + closePopover(); + onEnableAll(); + }, + }, + { + name: i18n.translate('unifiedSearch.filter.options.disableAllFiltersButtonLabel', { + defaultMessage: 'Disable all', + }), + 'data-test-subj': 'filter-sets-disableAllFilters', + icon: 'eyeClosed', + onClick: () => { + closePopover(); + onDisableAll(); + }, + }, + { + name: i18n.translate('unifiedSearch.filter.options.invertNegatedFiltersButtonLabel', { + defaultMessage: 'Invert inclusion', + }), + 'data-test-subj': 'filter-sets-invertAllFilters', + icon: 'invert', + onClick: () => { + closePopover(); + onToggleAllNegated(); + }, + }, + { + name: i18n.translate('unifiedSearch.filter.options.pinAllFiltersButtonLabel', { + defaultMessage: 'Pin all', + }), + 'data-test-subj': 'filter-sets-pinAllFilters', + icon: 'pin', + onClick: () => { + closePopover(); + onPinAll(); + }, + }, + { + name: i18n.translate('unifiedSearch.filter.options.unpinAllFiltersButtonLabel', { + defaultMessage: 'Unpin all', + }), + 'data-test-subj': 'filter-sets-unpinAllFilters', + icon: 'pin', + onClick: () => { + closePopover(); + onUnpinAll(); + }, + }, + ], + }, + { + id: 3, + title: i18n.translate('unifiedSearch.filter.options.filterLanguageLabel', { + defaultMessage: 'Filter language', + }), + content: ( + + ), + }, + { + id: 4, + title: i18n.translate('unifiedSearch.filter.options.loadCurrentFilterSetLabel', { + defaultMessage: 'Load filter set', + }), + width: 400, + content:
{manageFilterSetComponent}
, + }, + ] as EuiContextMenuPanelDescriptor[]; + + return panels; +} diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index de1fa659aa1336..bb01338d8d5a06 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -11,6 +11,7 @@ import classNames from 'classnames'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; import useObservable from 'react-use/lib/useObservable'; +import type { Filter } from '@kbn/es-query'; import { EMPTY } from 'rxjs'; import { map } from 'rxjs/operators'; import { @@ -20,8 +21,9 @@ import { EuiFieldText, usePrettyDuration, EuiIconProps, - EuiSuperUpdateButton, OnRefreshProps, + useIsWithinBreakpoints, + EuiSuperUpdateButton, } from '@elastic/eui'; import { IDataPluginServices, @@ -30,20 +32,21 @@ import { Query, getQueryLog, } from '@kbn/data-plugin/public'; +import { i18n } from '@kbn/i18n'; import { DataView } from '@kbn/data-views-plugin/public'; import type { PersistedLog } from '@kbn/data-plugin/public'; import { useKibana, withKibana } from '@kbn/kibana-react-plugin/public'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import QueryStringInputUI from './query_string_input'; import { NoDataPopover } from './no_data_popover'; -import { shallowEqual } from '../utils'; +import { shallowEqual } from '../utils/shallow_equal'; +import { AddFilterPopover } from './add_filter_popover'; +import { DataViewPicker, DataViewPickerProps } from '../dataview_picker'; +import { FilterButtonGroup } from '../filter_bar/filter_button_group/filter_button_group'; const SuperDatePicker = React.memo( EuiSuperDatePicker as any ) as unknown as typeof EuiSuperDatePicker; -const SuperUpdateButton = React.memo( - EuiSuperUpdateButton as any -) as unknown as typeof EuiSuperUpdateButton; const QueryStringInput = withKibana(QueryStringInputUI); @@ -63,7 +66,6 @@ export interface QueryBarTopRowProps { isLoading?: boolean; isRefreshPaused?: boolean; nonKqlMode?: 'lucene' | 'text'; - nonKqlModeHelpText?: string; onChange: (payload: { dateRange: TimeRange; query?: Query }) => void; onRefresh?: (payload: { dateRange: TimeRange }) => void; onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; @@ -74,10 +76,17 @@ export interface QueryBarTopRowProps { refreshInterval?: number; screenTitle?: string; showQueryInput?: boolean; + showAddFilter?: boolean; showDatePicker?: boolean; showAutoRefreshOnly?: boolean; timeHistory?: TimeHistoryContract; timeRangeForSuggestionsOverride?: boolean; + filters: Filter[]; + onFiltersUpdated?: (filters: Filter[]) => void; + dataViewPickerComponentProps?: DataViewPickerProps; + filterBar?: React.ReactNode; + showDatePickerAsBadge?: boolean; + showSubmitButton?: boolean; } const SharingMetaFields = React.memo(function SharingMetaFields({ @@ -114,7 +123,13 @@ const SharingMetaFields = React.memo(function SharingMetaFields({ export const QueryBarTopRow = React.memo( function QueryBarTopRow(props: QueryBarTopRowProps) { - const { showQueryInput = true, showDatePicker = true, showAutoRefreshOnly = false } = props; + const isMobile = useIsWithinBreakpoints(['xs', 's']); + const { + showQueryInput = true, + showDatePicker = true, + showAutoRefreshOnly = false, + showSubmitButton = true, + } = props; const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); const [isQueryInputFocused, setIsQueryInputFocused] = useState(false); @@ -283,14 +298,27 @@ export const QueryBarTopRow = React.memo( return Boolean(showDatePicker || showAutoRefreshOnly); } + function renderFilterMenuOnly(): boolean { + return !Boolean(props.showAddFilter) && Boolean(props.prepend); + } + + function shouldRenderUpdatebutton(): boolean { + return ( + Boolean(showSubmitButton) && + Boolean(showQueryInput || showDatePicker || showAutoRefreshOnly) + ); + } + + function shouldShowDatePickerAsBadge(): boolean { + return Boolean(props.showDatePickerAsBadge) && !shouldRenderQueryInput(); + } + function renderDatePicker() { if (!shouldRenderDatePicker()) { return null; } - const wrapperClasses = classNames('kbnQueryBar__datePickerWrapper', { - 'kbnQueryBar__datePickerWrapper-isHidden': isQueryInputFocused, - }); + const wrapperClasses = classNames('kbnQueryBar__datePickerWrapper'); return ( @@ -308,23 +336,52 @@ export const QueryBarTopRow = React.memo( dateFormat={uiSettings.get('dateFormat')} isAutoRefreshOnly={showAutoRefreshOnly} className="kbnQueryBar__datePicker" + isQuickSelectOnly={isMobile ? false : isQueryInputFocused} + width={isMobile ? 'full' : 'auto'} + compressed={shouldShowDatePickerAsBadge()} /> ); } function renderUpdateButton() { + if (!shouldRenderUpdatebutton()) { + return null; + } + + const buttonLabelUpdate = i18n.translate('unifiedSearch.queryBarTopRow.submitButton.update', { + defaultMessage: 'Needs updating', + }); + const buttonLabelRefresh = i18n.translate( + 'unifiedSearch.queryBarTopRow.submitButton.refresh', + { + defaultMessage: 'Refresh query', + } + ); + const button = props.customSubmitButton ? ( React.cloneElement(props.customSubmitButton, { onClick: onClickSubmitButton }) ) : ( - + + + ); if (!shouldRenderDatePicker()) { @@ -332,61 +389,118 @@ export const QueryBarTopRow = React.memo( } return ( - - - {renderDatePicker()} - {button} - - + + + + {renderDatePicker()} + {button} + + + ); } - function renderQueryInput() { - if (!shouldRenderQueryInput()) return; + function renderDataViewsPicker() { + if (!props.dataViewPickerComponentProps) return; return ( - - + ); } - const classes = classNames('kbnQueryBar', { - 'kbnQueryBar--withDatePicker': showDatePicker, - }); + function renderAddButton() { + return ( + Boolean(props.showAddFilter) && ( + + + + ) + ); + } + + function renderFilterButtonGroup() { + return ( + (Boolean(props.showAddFilter) || Boolean(props.prepend)) && ( + + + + ) + ); + } + + function renderQueryInput() { + return ( + + {!renderFilterMenuOnly() && renderFilterButtonGroup()} + {shouldRenderQueryInput() && ( + + + + )} + + ); + } return ( - - {renderQueryInput()} + <> - {renderUpdateButton()} - + + {renderDataViewsPicker()} + + {renderQueryInput()} + + {shouldShowDatePickerAsBadge() && props.filterBar} + {renderUpdateButton()} + + {!shouldShowDatePickerAsBadge() && props.filterBar} + ); }, ({ query: prevQuery, ...prevProps }, { query: nextQuery, ...nextProps }) => { diff --git a/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx b/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx index b4eed13da7f582..7437bf5fd4ece9 100644 --- a/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx @@ -110,7 +110,6 @@ describe('QueryStringInput', () => { ); await waitFor(() => getByText(kqlQuery.query)); - await waitFor(() => getByText('KQL')); }); it('Should pass the query language to the language switcher', () => { diff --git a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx index a9f4127809ab70..31ff302f9e6991 100644 --- a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx @@ -24,9 +24,10 @@ import { EuiTextArea, htmlIdGenerator, PopoverAnchorPosition, + toSentenceCase, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { compact, debounce, isEqual, isFunction } from 'lodash'; +import { compact, debounce, isEmpty, isEqual, isFunction } from 'lodash'; import { Toast } from '@kbn/core/public'; import { IDataPluginServices, Query, getQueryLog } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; @@ -41,6 +42,7 @@ import { QueryLanguageSwitcher } from './language_switcher'; import type { SuggestionsListSize } from '../typeahead/suggestions_component'; import { SuggestionsComponent } from '../typeahead'; import { onRaf } from '../utils'; +import { FilterButtonGroup } from '../filter_bar/filter_button_group/filter_button_group'; import { QuerySuggestion, QuerySuggestionTypes } from '../autocomplete'; import { getTheme, getAutocomplete } from '../services'; @@ -72,7 +74,6 @@ export interface QueryStringInputProps { * this params add another option text, which is just a simple keyword search mode, the way a simple search box works */ nonKqlMode?: 'lucene' | 'text'; - nonKqlModeHelpText?: string; /** * @param autoSubmit if user selects a value, in that case kuery will be auto submitted */ @@ -124,6 +125,8 @@ const KEY_CODES = { export default class QueryStringInputUI extends PureComponent { static defaultProps = { storageKey: KIBANA_USER_QUERY_LANGUAGE_KEY, + iconType: 'search', + isClearable: true, }; public state: State = { @@ -678,31 +681,59 @@ export default class QueryStringInputUI extends PureComponent { this.handleAutoHeight(); }; + getSearchInputPlaceholder = () => { + let placeholder = ''; + if (!this.props.query.language || this.props.query.language === 'text') { + placeholder = i18n.translate('unifiedSearch.query.queryBar.searchInputPlaceholderForText', { + defaultMessage: 'Filter your data', + }); + } else { + const language = + this.props.query.language === 'kuery' ? 'KQL' : toSentenceCase(this.props.query.language); + + placeholder = i18n.translate('unifiedSearch.query.queryBar.searchInputPlaceholder', { + defaultMessage: 'Filter your data using {language} syntax', + values: { language }, + }); + } + + return placeholder; + }; + public render() { const isSuggestionsVisible = this.state.isSuggestionsVisible && { 'aria-controls': 'kbnTypeahead__items', 'aria-owns': 'kbnTypeahead__items', }; const ariaCombobox = { ...isSuggestionsVisible, role: 'combobox' }; - const containerClassName = classNames( - 'euiFormControlLayout euiFormControlLayout--group kbnQueryBar__wrap', - this.props.className - ); - const inputClassName = classNames( - 'kbnQueryBar__textarea', - this.props.iconType ? 'kbnQueryBar__textarea--withIcon' : null, - this.props.prepend ? 'kbnQueryBar__textarea--hasPrepend' : null, - !this.props.disableLanguageSwitcher ? 'kbnQueryBar__textarea--hasAppend' : null - ); - const inputWrapClassName = classNames( - 'euiFormControlLayout__childrenWrapper kbnQueryBar__textareaWrap', - this.props.prepend ? 'kbnQueryBar__textareaWrap--hasPrepend' : null, - !this.props.disableLanguageSwitcher ? 'kbnQueryBar__textareaWrap--hasAppend' : null + + const simpleLanguageSwitcher = this.props.disableLanguageSwitcher ? null : ( + ); + const prependElement = + this.props.prepend || simpleLanguageSwitcher ? ( + + ) : undefined; + + const containerClassName = classNames('kbnQueryBar__wrap', this.props.className); + const inputClassName = classNames('kbnQueryBar__textarea', { + 'kbnQueryBar__textarea--withIcon': this.props.iconType, + 'kbnQueryBar__textarea--isClearable': this.props.isClearable, + 'kbnQueryBar__textarea--withPrepend': prependElement, + 'kbnQueryBar__textarea--isSuggestionsVisible': + isSuggestionsVisible && !isEmpty(this.state.suggestions), + }); + const inputWrapClassName = classNames('kbnQueryBar__textareaWrap'); return (
- {this.props.prepend} + {prependElement} +
{ >
{
- {this.props.disableLanguageSwitcher ? null : ( - - )}
); } diff --git a/src/plugins/unified_search/public/saved_query_form/save_query_form.tsx b/src/plugins/unified_search/public/saved_query_form/save_query_form.tsx index 53c5ec3310da25..186c1f072aeddc 100644 --- a/src/plugins/unified_search/public/saved_query_form/save_query_form.tsx +++ b/src/plugins/unified_search/public/saved_query_form/save_query_form.tsx @@ -7,20 +7,7 @@ */ import React, { useEffect, useState, useCallback } from 'react'; -import { - EuiButtonEmpty, - EuiModal, - EuiButton, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiModalFooter, - EuiForm, - EuiFormRow, - EuiFieldText, - EuiSwitch, - EuiText, -} from '@elastic/eui'; +import { EuiButton, EuiForm, EuiFormRow, EuiFieldText, EuiSwitch } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { sortBy, isEqual } from 'lodash'; import { SavedQuery, SavedQueryService } from '@kbn/data-plugin/public'; @@ -51,8 +38,6 @@ export function SaveQueryForm({ showTimeFilterOption = true, }: Props) { const [title, setTitle] = useState(savedQuery?.attributes.title ?? ''); - const [enabledSaveButton, setEnabledSaveButton] = useState(Boolean(savedQuery)); - const [description, setDescription] = useState(savedQuery?.attributes.description ?? ''); const [savedQueries, setSavedQueries] = useState([]); const [shouldIncludeFilters, setShouldIncludeFilters] = useState( Boolean(savedQuery?.attributes.filters ?? true) @@ -72,10 +57,10 @@ export function SaveQueryForm({ } ); - const savedQueryDescriptionText = i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryDescriptionText', + const titleExistsErrorText = i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryForm.titleExistsText', { - defaultMessage: 'Save query text and filters that you want to use again.', + defaultMessage: 'Name is required.', } ); @@ -98,36 +83,40 @@ export function SaveQueryForm({ errors.push(titleConflictErrorText); } + if (!title) { + errors.push(titleExistsErrorText); + } + if (!isEqual(errors, formErrors)) { setFormErrors(errors); return false; } return !formErrors.length; - }, [savedQueries, savedQuery, title, titleConflictErrorText, formErrors]); + }, [savedQueries, formErrors, title, savedQuery, titleConflictErrorText, titleExistsErrorText]); const onClickSave = useCallback(() => { if (validate()) { onSave({ id: savedQuery?.id, title, - description, + description: '', shouldIncludeFilters, shouldIncludeTimefilter, }); + onClose(); } }, [ validate, onSave, + onClose, savedQuery?.id, title, - description, shouldIncludeFilters, shouldIncludeTimefilter, ]); const onInputChange = useCallback((event) => { - setEnabledSaveButton(Boolean(event.target.value)); setFormErrors([]); setTitle(event.target.value); }, []); @@ -143,18 +132,16 @@ export function SaveQueryForm({ const saveQueryForm = ( - - {savedQueryDescriptionText} - - - { - setDescription(event.target.value); - }} - data-test-subj="saveQueryFormDescription" - /> - {showFilterOption && ( - + + )} - - ); - - return ( - - - - {i18n.translate('unifiedSearch.search.searchBar.savedQueryFormTitle', { - defaultMessage: 'Save query', - })} - - - - {saveQueryForm} - - - - {i18n.translate('unifiedSearch.search.searchBar.savedQueryFormCancelButtonText', { - defaultMessage: 'Cancel', - })} - + {i18n.translate('unifiedSearch.search.searchBar.savedQueryFormSaveButtonText', { - defaultMessage: 'Save', + defaultMessage: 'Save filter set', })} - - + + ); + + return <>{saveQueryForm}; } diff --git a/src/plugins/unified_search/public/saved_query_management/_index.scss b/src/plugins/unified_search/public/saved_query_management/_index.scss index 0580e857e8494b..0c90d7817b6851 100644 --- a/src/plugins/unified_search/public/saved_query_management/_index.scss +++ b/src/plugins/unified_search/public/saved_query_management/_index.scss @@ -1,2 +1 @@ -@import './saved_query_management_component'; -@import './saved_query_list_item'; +@import './saved_query_management_list'; diff --git a/src/plugins/unified_search/public/saved_query_management/_saved_query_list_item.scss b/src/plugins/unified_search/public/saved_query_management/_saved_query_list_item.scss deleted file mode 100644 index 714ba82dfb4764..00000000000000 --- a/src/plugins/unified_search/public/saved_query_management/_saved_query_list_item.scss +++ /dev/null @@ -1,21 +0,0 @@ -.kbnSavedQueryListItem { - margin-top: 0; - color: $euiLinkColor; -} - -// Can't actually target the button with classes, but styles to override -// are just user agent styles -.kbnSavedQueryListItem-selected button { - font-weight: $euiFontWeightBold; -} - -// This will ensure the info icon and tooltip shows even if the label gets truncated -.kbnSavedQueryListItem__label { - display: flex; - align-items: center; -} - -.kbnSavedQueryListItem__labelText { - @include euiTextTruncate; - margin-right: $euiSizeXS; -} diff --git a/src/plugins/unified_search/public/saved_query_management/_saved_query_management_component.scss b/src/plugins/unified_search/public/saved_query_management/_saved_query_management_list.scss similarity index 76% rename from src/plugins/unified_search/public/saved_query_management/_saved_query_management_component.scss rename to src/plugins/unified_search/public/saved_query_management/_saved_query_management_list.scss index 928cb5a34d6deb..7ce304310ae56a 100644 --- a/src/plugins/unified_search/public/saved_query_management/_saved_query_management_component.scss +++ b/src/plugins/unified_search/public/saved_query_management/_saved_query_management_list.scss @@ -1,18 +1,9 @@ -.kbnSavedQueryManagement__popover { - max-width: $euiFormMaxWidth; -} - .kbnSavedQueryManagement__listWrapper { // Addition height will ensure one item is "cutoff" to indicate more below the scroll max-height: $euiFormMaxWidth + $euiSize; overflow-y: hidden; } -.kbnSavedQueryManagement__pagination { - justify-content: center; - padding: ($euiSizeM / 2) $euiSizeM $euiSizeM; -} - .kbnSavedQueryManagement__text { padding: $euiSizeM $euiSizeM ($euiSizeM / 2) $euiSizeM; } diff --git a/src/plugins/unified_search/public/saved_query_management/index.ts b/src/plugins/unified_search/public/saved_query_management/index.ts index 4ead1907cd23bd..134b24a4fb85c9 100644 --- a/src/plugins/unified_search/public/saved_query_management/index.ts +++ b/src/plugins/unified_search/public/saved_query_management/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { SavedQueryManagementComponent } from './saved_query_management_component'; +export { SavedQueryManagementList } from './saved_query_management_list'; diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_list_item.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_list_item.tsx deleted file mode 100644 index 71fbd8aad6e48f..00000000000000 --- a/src/plugins/unified_search/public/saved_query_management/saved_query_list_item.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EuiListGroupItem, EuiConfirmModal, EuiIconTip } from '@elastic/eui'; - -import React, { Fragment, useState } from 'react'; -import classNames from 'classnames'; -import { i18n } from '@kbn/i18n'; -import { SavedQuery } from '@kbn/data-plugin/public'; - -interface Props { - savedQuery: SavedQuery; - isSelected: boolean; - showWriteOperations: boolean; - onSelect: (savedQuery: SavedQuery) => void; - onDelete: (savedQuery: SavedQuery) => void; -} - -export const SavedQueryListItem = ({ - savedQuery, - isSelected, - onSelect, - onDelete, - showWriteOperations, -}: Props) => { - const [showDeletionConfirmationModal, setShowDeletionConfirmationModal] = useState(false); - - const selectButtonAriaLabelText = isSelected - ? i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel', - { - defaultMessage: - 'Saved query button selected {savedQueryName}. Press to clear any changes.', - values: { savedQueryName: savedQuery.attributes.title }, - } - ) - : i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel', - { - defaultMessage: 'Saved query button {savedQueryName}', - values: { savedQueryName: savedQuery.attributes.title }, - } - ); - - const selectButtonDataTestSubj = isSelected - ? `load-saved-query-${savedQuery.attributes.title}-button saved-query-list-item-selected` - : `load-saved-query-${savedQuery.attributes.title}-button`; - - const classes = classNames('kbnSavedQueryListItem', { - 'kbnSavedQueryListItem-selected': isSelected, - }); - - const label = ( - - {savedQuery.attributes.title}{' '} - {savedQuery.attributes.description && ( - - )} - - ); - - return ( - - { - onSelect(savedQuery); - }} - aria-label={selectButtonAriaLabelText} - label={label} - iconType={isSelected ? 'check' : undefined} - extraAction={ - showWriteOperations - ? { - color: 'danger', - onClick: () => setShowDeletionConfirmationModal(true), - iconType: 'trash', - iconSize: 's', - 'aria-label': i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel', - { - defaultMessage: 'Delete saved query {savedQueryName}', - values: { savedQueryName: savedQuery.attributes.title }, - } - ), - title: i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel', - { - defaultMessage: 'Delete saved query {savedQueryName}', - values: { savedQueryName: savedQuery.attributes.title }, - } - ), - 'data-test-subj': `delete-saved-query-${savedQuery.attributes.title}-button`, - } - : undefined - } - /> - - {showDeletionConfirmationModal && ( - { - onDelete(savedQuery); - setShowDeletionConfirmationModal(false); - }} - buttonColor="danger" - onCancel={() => { - setShowDeletionConfirmationModal(false); - }} - /> - )} - - ); -}; diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_management_component.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_management_component.tsx deleted file mode 100644 index 07d3a9d799a66d..00000000000000 --- a/src/plugins/unified_search/public/saved_query_management/saved_query_management_component.tsx +++ /dev/null @@ -1,340 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - EuiPopover, - EuiPopoverTitle, - EuiPopoverFooter, - EuiButtonEmpty, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiListGroup, - EuiPagination, - EuiText, - EuiSpacer, - EuiIcon, -} from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import React, { useCallback, useEffect, useState, Fragment, useRef } from 'react'; -import { sortBy } from 'lodash'; -import { SavedQuery, SavedQueryService } from '@kbn/data-plugin/public'; -import { SavedQueryListItem } from './saved_query_list_item'; - -const perPage = 50; -interface Props { - showSaveQuery?: boolean; - loadedSavedQuery?: SavedQuery; - savedQueryService: SavedQueryService; - onSave: () => void; - onSaveAsNew: () => void; - onLoad: (savedQuery: SavedQuery) => void; - onClearSavedQuery: () => void; -} - -export function SavedQueryManagementComponent({ - showSaveQuery, - loadedSavedQuery, - onSave, - onSaveAsNew, - onLoad, - onClearSavedQuery, - savedQueryService, -}: Props) { - const [isOpen, setIsOpen] = useState(false); - const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]); - const [count, setTotalCount] = useState(0); - const [activePage, setActivePage] = useState(0); - const cancelPendingListingRequest = useRef<() => void>(() => {}); - - useEffect(() => { - const fetchCountAndSavedQueries = async () => { - cancelPendingListingRequest.current(); - let requestGotCancelled = false; - cancelPendingListingRequest.current = () => { - requestGotCancelled = true; - }; - - const { total: savedQueryCount, queries: savedQueryItems } = - await savedQueryService.findSavedQueries('', perPage, activePage + 1); - - if (requestGotCancelled) return; - - const sortedSavedQueryItems = sortBy(savedQueryItems, 'attributes.title'); - setTotalCount(savedQueryCount); - setSavedQueries(sortedSavedQueryItems); - }; - if (isOpen) { - fetchCountAndSavedQueries(); - } - }, [isOpen, activePage, savedQueryService]); - - const handleTogglePopover = useCallback( - () => setIsOpen((currentState) => !currentState), - [setIsOpen] - ); - - const handleClosePopover = useCallback(() => setIsOpen(false), []); - - const handleSave = useCallback(() => { - handleClosePopover(); - onSave(); - }, [handleClosePopover, onSave]); - - const handleSaveAsNew = useCallback(() => { - handleClosePopover(); - onSaveAsNew(); - }, [handleClosePopover, onSaveAsNew]); - - const handleSelect = useCallback( - (savedQueryToSelect) => { - handleClosePopover(); - onLoad(savedQueryToSelect); - }, - [handleClosePopover, onLoad] - ); - - const handleDelete = useCallback( - (savedQueryToDelete: SavedQuery) => { - const onDeleteSavedQuery = async (savedQuery: SavedQuery) => { - cancelPendingListingRequest.current(); - setSavedQueries( - savedQueries.filter((currentSavedQuery) => currentSavedQuery.id !== savedQuery.id) - ); - - if (loadedSavedQuery && loadedSavedQuery.id === savedQuery.id) { - onClearSavedQuery(); - } - - await savedQueryService.deleteSavedQuery(savedQuery.id); - setActivePage(0); - }; - - onDeleteSavedQuery(savedQueryToDelete); - handleClosePopover(); - }, - [handleClosePopover, loadedSavedQuery, onClearSavedQuery, savedQueries, savedQueryService] - ); - - const savedQueryDescriptionText = i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryDescriptionText', - { - defaultMessage: 'Save query text and filters that you want to use again.', - } - ); - - const noSavedQueriesDescriptionText = - i18n.translate('unifiedSearch.search.searchBar.savedQueryNoSavedQueriesText', { - defaultMessage: 'There are no saved queries.', - }) + - ' ' + - savedQueryDescriptionText; - - const savedQueryPopoverTitleText = i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverTitleText', - { - defaultMessage: 'Saved Queries', - } - ); - - const goToPage = (pageNumber: number) => { - setActivePage(pageNumber); - }; - - const savedQueryPopoverButton = ( - - - - - ); - - const savedQueryRows = () => { - const savedQueriesWithoutCurrent = savedQueries.filter((savedQuery) => { - if (!loadedSavedQuery) return true; - return savedQuery.id !== loadedSavedQuery.id; - }); - const savedQueriesReordered = - loadedSavedQuery && savedQueriesWithoutCurrent.length !== savedQueries.length - ? [loadedSavedQuery, ...savedQueriesWithoutCurrent] - : [...savedQueriesWithoutCurrent]; - return savedQueriesReordered.map((savedQuery) => ( - - )); - }; - - return ( - - -
- - {savedQueryPopoverTitleText} - - {savedQueries.length > 0 ? ( - - -

{savedQueryDescriptionText}

-
-
- - {savedQueryRows()} - -
- -
- ) : ( - - -

{noSavedQueriesDescriptionText}

-
- -
- )} - - - {showSaveQuery && loadedSavedQuery && ( - - - - {i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText', - { - defaultMessage: 'Save changes', - } - )} - - - - - {i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText', - { - defaultMessage: 'Save as new', - } - )} - - - - )} - {showSaveQuery && !loadedSavedQuery && ( - - - {i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonText', - { - defaultMessage: 'Save current query', - } - )} - - - )} - - - {loadedSavedQuery && ( - - {i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverClearButtonText', - { - defaultMessage: 'Clear', - } - )} - - )} - - - -
-
-
- ); -} diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx new file mode 100644 index 00000000000000..7c2d0ebd1faad1 --- /dev/null +++ b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiSelectable } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n-react'; +import { act } from 'react-dom/test-utils'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; +import { ReactWrapper } from 'enzyme'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { coreMock, applicationServiceMock } from '@kbn/core/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { + SavedQueryManagementListProps, + SavedQueryManagementList, +} from './saved_query_management_list'; + +describe('Saved query management list component', () => { + const startMock = coreMock.createStart(); + const dataMock = dataPluginMock.createStartContract(); + const applicationMock = applicationServiceMock.createStartContract(); + const application = { + ...applicationMock, + capabilities: { + ...applicationMock.capabilities, + savedObjectsManagement: { edit: true }, + }, + }; + function wrapSavedQueriesListComponentInContext(testProps: SavedQueryManagementListProps) { + const services = { + uiSettings: startMock.uiSettings, + http: startMock.http, + application, + }; + + return ( + + + + + + ); + } + + function flushEffect(component: ReactWrapper) { + return act(async () => { + await component; + await new Promise((r) => setImmediate(r)); + component.update(); + }); + } + let props: SavedQueryManagementListProps; + beforeEach(() => { + props = { + onLoad: jest.fn(), + onClearSavedQuery: jest.fn(), + onClose: jest.fn(), + showSaveQuery: true, + hasFiltersOrQuery: false, + savedQueryService: { + ...dataMock.query.savedQueries, + findSavedQueries: jest.fn().mockResolvedValue({ + queries: [ + { + id: '8a0b7cd0-b0c4-11ec-92b2-73d62e0d28a9', + attributes: { + title: 'Test', + description: '', + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + filters: [], + }, + }, + ], + }), + deleteSavedQuery: jest.fn(), + }, + }; + }); + it('should render the list component if saved queries exist', async () => { + const component = mount(wrapSavedQueriesListComponentInContext(props)); + await flushEffect(component); + expect(component.find('[data-test-subj="saved-query-management-list"]').length).toBe(1); + }); + + it('should not rendet the list component if not saved queries exist', async () => { + const newProps = { + ...props, + savedQueryService: { + ...dataMock.query.savedQueries, + findSavedQueries: jest.fn().mockResolvedValue({ + queries: [], + }), + }, + }; + const component = mount(wrapSavedQueriesListComponentInContext(newProps)); + await flushEffect(component); + expect(component.find('[data-test-subj="saved-query-management-empty"]').length).toBeTruthy(); + }); + + it('should render the saved queries on the selectable component', async () => { + const component = mount(wrapSavedQueriesListComponentInContext(props)); + await flushEffect(component); + expect(component.find(EuiSelectable).prop('options').length).toBe(1); + expect(component.find(EuiSelectable).prop('options')[0].label).toBe('Test'); + }); + + it('should call the onLoad function', async () => { + const onLoadSpy = jest.fn(); + const newProps = { + ...props, + onLoad: onLoadSpy, + }; + const component = mount(wrapSavedQueriesListComponentInContext(newProps)); + await flushEffect(component); + component.find('[data-test-subj="load-saved-query-Test-button"]').first().simulate('click'); + expect( + component.find('[data-test-subj="saved-query-management-apply-changes-button"]').length + ).toBeTruthy(); + component + .find('[data-test-subj="saved-query-management-apply-changes-button"]') + .first() + .simulate('click'); + expect(onLoadSpy).toBeCalled(); + }); + + it('should render the button with the correct text', async () => { + const component = mount(wrapSavedQueriesListComponentInContext(props)); + await flushEffect(component); + expect( + component + .find('[data-test-subj="saved-query-management-apply-changes-button"]') + .first() + .text() + ).toBe('Apply filter set'); + + const newProps = { + ...props, + hasFiltersOrQuery: true, + }; + const updatedComponent = mount(wrapSavedQueriesListComponentInContext(newProps)); + await flushEffect(component); + expect( + updatedComponent + .find('[data-test-subj="saved-query-management-apply-changes-button"]') + .first() + .text() + ).toBe('Replace with selected filter set'); + }); + + it('should render the modal on delete', async () => { + const component = mount(wrapSavedQueriesListComponentInContext(props)); + await flushEffect(component); + findTestSubject(component, 'delete-saved-query-Test-button').simulate('click'); + expect(component.find('[data-test-subj="confirmModalConfirmButton"]').length).toBeTruthy(); + }); +}); diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx new file mode 100644 index 00000000000000..7568bb9375fa6d --- /dev/null +++ b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx @@ -0,0 +1,385 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiSelectable, + EuiText, + EuiPopoverFooter, + EuiButtonIcon, + EuiButtonEmpty, + EuiConfirmModal, + usePrettyDuration, + ShortDate, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useEffect, useState, useRef } from 'react'; +import { css } from '@emotion/react'; +import { sortBy } from 'lodash'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { IDataPluginServices, SavedQuery, SavedQueryService } from '@kbn/data-plugin/public'; +import type { SavedQueryAttributes } from '@kbn/data-plugin/common'; + +export interface SavedQueryManagementListProps { + showSaveQuery?: boolean; + loadedSavedQuery?: SavedQuery; + savedQueryService: SavedQueryService; + onLoad: (savedQuery: SavedQuery) => void; + onClearSavedQuery: () => void; + onClose: () => void; + hasFiltersOrQuery: boolean; +} + +interface SelectableProps { + key?: string; + label: string; + value?: string; + checked?: 'on' | 'off' | undefined; +} + +interface DurationRange { + end: ShortDate; + label?: string; + start: ShortDate; +} + +const commonDurationRanges: DurationRange[] = [ + { start: 'now/d', end: 'now/d', label: 'Today' }, + { start: 'now/w', end: 'now/w', label: 'This week' }, + { start: 'now/M', end: 'now/M', label: 'This month' }, + { start: 'now/y', end: 'now/y', label: 'This year' }, + { start: 'now-1d/d', end: 'now-1d/d', label: 'Yesterday' }, + { start: 'now/w', end: 'now', label: 'Week to date' }, + { start: 'now/M', end: 'now', label: 'Month to date' }, + { start: 'now/y', end: 'now', label: 'Year to date' }, +]; + +const itemTitle = (attributes: SavedQueryAttributes, format: string) => { + let label = attributes.title; + const prettifier = usePrettyDuration; + + if (attributes.description) { + label += `; ${attributes.description}`; + } + + if (attributes.timefilter) { + label += `; ${prettifier({ + timeFrom: attributes.timefilter?.from, + timeTo: attributes.timefilter?.to, + quickRanges: commonDurationRanges, + dateFormat: format, + })}`; + } + + return label; +}; + +const itemLabel = (attributes: SavedQueryAttributes) => { + let label: React.ReactNode = attributes.title; + + if (attributes.description) { + label = ( + <> + {label} + + ); + } + + if (attributes.timefilter) { + label = ( + <> + {label} + + ); + } + + return label; +}; + +export function SavedQueryManagementList({ + showSaveQuery, + loadedSavedQuery, + onLoad, + onClearSavedQuery, + savedQueryService, + onClose, + hasFiltersOrQuery, +}: SavedQueryManagementListProps) { + const kibana = useKibana(); + const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]); + const [selectedSavedQuery, setSelectedSavedQuery] = useState(null as SavedQuery | null); + const [toBeDeletedSavedQuery, setToBeDeletedSavedQuery] = useState(null as SavedQuery | null); + const [showDeletionConfirmationModal, setShowDeletionConfirmationModal] = useState(false); + const cancelPendingListingRequest = useRef<() => void>(() => {}); + const { uiSettings, http, application } = kibana.services; + const format = uiSettings.get('dateFormat'); + + useEffect(() => { + const fetchCountAndSavedQueries = async () => { + cancelPendingListingRequest.current(); + let requestGotCancelled = false; + cancelPendingListingRequest.current = () => { + requestGotCancelled = true; + }; + + const { queries: savedQueryItems } = await savedQueryService.findSavedQueries(); + + if (requestGotCancelled) return; + + const sortedSavedQueryItems = sortBy(savedQueryItems, 'attributes.title'); + setSavedQueries(sortedSavedQueryItems); + }; + fetchCountAndSavedQueries(); + }, [savedQueryService]); + + const handleLoad = useCallback(() => { + if (selectedSavedQuery) { + onLoad(selectedSavedQuery); + onClose(); + } + }, [onLoad, selectedSavedQuery, onClose]); + + const handleSelect = useCallback((savedQueryToSelect) => { + setSelectedSavedQuery(savedQueryToSelect); + }, []); + + const handleDelete = useCallback((savedQueryToDelete: SavedQuery) => { + setShowDeletionConfirmationModal(true); + setToBeDeletedSavedQuery(savedQueryToDelete); + }, []); + + const onDelete = useCallback( + (savedQueryToDelete: string) => { + const onDeleteSavedQuery = async (savedQueryId: string) => { + cancelPendingListingRequest.current(); + setSavedQueries( + savedQueries.filter((currentSavedQuery) => currentSavedQuery.id !== savedQueryId) + ); + + if (loadedSavedQuery && loadedSavedQuery.id === savedQueryId) { + onClearSavedQuery(); + } + + await savedQueryService.deleteSavedQuery(savedQueryId); + }; + + onDeleteSavedQuery(savedQueryToDelete); + }, + [loadedSavedQuery, onClearSavedQuery, savedQueries, savedQueryService] + ); + + const savedQueryDescriptionText = i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryDescriptionText', + { + defaultMessage: 'Save query text and filters that you want to use again.', + } + ); + + const noSavedQueriesDescriptionText = + i18n.translate('unifiedSearch.search.searchBar.savedQueryNoSavedQueriesText', { + defaultMessage: 'No saved queries.', + }) + + ' ' + + savedQueryDescriptionText; + + const savedQueriesOptions = () => { + const savedQueriesWithoutCurrent = savedQueries.filter((savedQuery) => { + if (!loadedSavedQuery) return true; + return savedQuery.id !== loadedSavedQuery.id; + }); + const savedQueriesReordered = + loadedSavedQuery && savedQueriesWithoutCurrent.length !== savedQueries.length + ? [loadedSavedQuery, ...savedQueriesWithoutCurrent] + : [...savedQueriesWithoutCurrent]; + + return savedQueriesReordered.map((savedQuery) => { + return { + key: savedQuery.id, + label: itemLabel(savedQuery.attributes), + title: itemTitle(savedQuery.attributes, format), + 'data-test-subj': `load-saved-query-${savedQuery.attributes.title}-button`, + value: savedQuery.id, + checked: + (loadedSavedQuery && savedQuery.id === loadedSavedQuery.id) || + (selectedSavedQuery && savedQuery.id === selectedSavedQuery.id) + ? 'on' + : undefined, + append: !!showSaveQuery && ( + handleDelete(savedQuery)} + color="danger" + /> + ), + }; + }) as unknown as SelectableProps[]; + }; + + const canEditSavedObjects = application.capabilities.savedObjectsManagement.edit; + + const listComponent = ( + <> + {savedQueries.length > 0 ? ( + <> +
+ + aria-label="Basic example" + options={savedQueriesOptions()} + searchable + singleSelection="always" + onChange={(choices) => { + const choice = choices.find(({ checked }) => checked) as unknown as { + value: string; + }; + if (choice) { + handleSelect(savedQueries.find((savedQuery) => savedQuery.id === choice.value)); + } + }} + searchProps={{ + compressed: true, + placeholder: i18n.translate( + 'unifiedSearch.query.queryBar.indexPattern.findFilterSet', + { + defaultMessage: 'Find a filter set', + } + ), + }} + listProps={{ + isVirtualized: true, + }} + > + {(list, search) => ( + <> + + {search} + + {list} + + )} + +
+ + ) : ( + <> + +

{noSavedQueriesDescriptionText}

+
+ + )} + + + {canEditSavedObjects && ( + + + Manage + + + )} + + + {hasFiltersOrQuery + ? i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryPopoverReplaceFilterSetLabel', + { + defaultMessage: 'Replace with selected filter set', + } + ) + : i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryPopoverApplyFilterSetLabel', + { + defaultMessage: 'Apply filter set', + } + )} + + + + + {showDeletionConfirmationModal && toBeDeletedSavedQuery && ( + { + onDelete(toBeDeletedSavedQuery.id); + setShowDeletionConfirmationModal(false); + }} + buttonColor="danger" + onCancel={() => { + setShowDeletionConfirmationModal(false); + }} + /> + )} + + ); + + return listComponent; +} diff --git a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx index 3d8aa26af22af4..c4e54995b5979e 100644 --- a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx @@ -191,11 +191,12 @@ export function createSearchBar({ core, storage, data, usageCollection }: Statef onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)} iconType={props.iconType} nonKqlMode={props.nonKqlMode} - nonKqlModeHelpText={props.nonKqlModeHelpText} customSubmitButton={props.customSubmitButton} isClearable={props.isClearable} placeholder={props.placeholder} {...overrideDefaultBehaviors(props)} + dataViewPickerComponentProps={props.dataViewPickerComponentProps} + displayStyle={props.displayStyle} /> ); diff --git a/src/plugins/unified_search/public/search_bar/search_bar.styles.ts b/src/plugins/unified_search/public/search_bar/search_bar.styles.ts new file mode 100644 index 00000000000000..1072a684eeaad8 --- /dev/null +++ b/src/plugins/unified_search/public/search_bar/search_bar.styles.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UseEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +export const searchBarStyles = ({ euiTheme }: UseEuiTheme) => { + return { + uniSearchBar: css` + padding: ${euiTheme.size.s}; + `, + detached: css` + border-bottom: ${euiTheme.border.thin}; + `, + inPage: css` + padding: 0; + `, + }; +}; diff --git a/src/plugins/unified_search/public/search_bar/search_bar.test.tsx b/src/plugins/unified_search/public/search_bar/search_bar.test.tsx index 14310b69809e07..fe5e03ab7fb373 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.test.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.test.tsx @@ -16,24 +16,21 @@ import { coreMock } from '@kbn/core/public/mocks'; const startMock = coreMock.createStart(); import { mount } from 'enzyme'; -import { IIndexPattern } from '@kbn/data-plugin/public'; +import { DataView } from '@kbn/data-views-plugin/public'; +import { EuiThemeProvider } from '@elastic/eui'; const mockTimeHistory = { get: () => { return []; }, + add: jest.fn(), + get$: () => { + return { + pipe: () => {}, + }; + }, }; -jest.mock('../filter_bar', () => { - return { - FilterBar: () =>
, - }; -}); - -jest.mock('../query_string_input/query_bar_top_row', () => { - return () =>
; -}); - const noop = jest.fn(); const createMockWebStorage = () => ({ @@ -66,7 +63,7 @@ const mockIndexPattern = { searchable: true, }, ], -} as IIndexPattern; +} as DataView; const kqlQuery = { query: 'response:200', @@ -88,24 +85,45 @@ function wrapSearchBarInContext(testProps: any) { storage: createMockStorage(), data: { query: { - savedQueries: {}, + savedQueries: { + findSavedQueries: () => + Promise.resolve({ + queries: [ + { + id: 'testwewe', + attributes: { + title: 'Saved query 1', + description: '', + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + filters: [], + }, + }, + ], + }), + }, }, }, }; return ( - - - - - + + + + + + + ); } describe('SearchBar', () => { - const SEARCH_BAR_ROOT = '.globalQueryBar'; - const FILTER_BAR = '.filterBar'; - const QUERY_BAR = '.queryBar'; + const SEARCH_BAR_ROOT = '.uniSearchBar'; + const FILTER_BAR = '[data-test-subj="unifiedFilterBar"]'; + const QUERY_BAR = '.kbnQueryBar'; + const QUERY_INPUT = '[data-test-subj="unifiedQueryInput"]'; beforeEach(() => { jest.clearAllMocks(); @@ -118,22 +136,9 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(1); - }); - - it('Should render empty when timepicker is off and no options provided', () => { - const component = mount( - wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], - showDatePicker: false, - }) - ); - - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT)).toBeTruthy(); + expect(component.find(FILTER_BAR).length).toBeFalsy(); + expect(component.find(QUERY_BAR).length).toBeTruthy(); }); it('Should render filter bar, when required fields are provided', () => { @@ -141,14 +146,16 @@ describe('SearchBar', () => { wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showDatePicker: false, + showQueryInput: true, + showFilterBar: true, onFiltersUpdated: noop, filters: [], }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(1); - expect(component.find(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT)).toBeTruthy(); + expect(component.find(FILTER_BAR).length).toBeTruthy(); + expect(component.find(QUERY_BAR).length).toBeTruthy(); }); it('Should NOT render filter bar, if disabled', () => { @@ -162,9 +169,9 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT)).toBeTruthy(); + expect(component.find(FILTER_BAR).length).toBeFalsy(); + expect(component.find(QUERY_BAR).length).toBeTruthy(); }); it('Should render query bar, when required fields are provided', () => { @@ -177,12 +184,12 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT)).toBeTruthy(); + expect(component.find(FILTER_BAR).length).toBeFalsy(); + expect(component.find(QUERY_BAR).length).toBeTruthy(); }); - it('Should NOT render query bar, if disabled', () => { + it('Should NOT render the input query input, if disabled', () => { const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], @@ -190,12 +197,13 @@ describe('SearchBar', () => { onQuerySubmit: noop, query: kqlQuery, showQueryBar: false, + showQueryInput: false, }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT)).toBeTruthy(); + expect(component.find(FILTER_BAR).length).toBeFalsy(); + expect(component.find(QUERY_INPUT).length).toBeFalsy(); }); it('Should render query bar and filter bar', () => { @@ -203,6 +211,7 @@ describe('SearchBar', () => { wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', + showQueryInput: true, onQuerySubmit: noop, query: kqlQuery, filters: [], @@ -210,8 +219,9 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(1); - expect(component.find(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT)).toBeTruthy(); + expect(component.find(FILTER_BAR).length).toBeTruthy(); + expect(component.find(QUERY_BAR).length).toBeTruthy(); + expect(component.find(QUERY_INPUT).length).toBeTruthy(); }); }); diff --git a/src/plugins/unified_search/public/search_bar/search_bar.tsx b/src/plugins/unified_search/public/search_bar/search_bar.tsx index c829ab66bb60af..ab59511ea6811a 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.tsx @@ -11,22 +11,27 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import classNames from 'classnames'; import React, { Component } from 'react'; import { get, isEqual } from 'lodash'; -import { EuiIconProps } from '@elastic/eui'; +import { EuiIconProps, withEuiTheme, WithEuiThemeProps } from '@elastic/eui'; import memoizeOne from 'memoize-one'; import { METRIC_TYPE } from '@kbn/analytics'; import { Query, Filter } from '@kbn/es-query'; import { withKibana, KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; - import type { TimeHistoryContract, SavedQuery } from '@kbn/data-plugin/public'; import type { SavedQueryAttributes } from '@kbn/data-plugin/common'; import { IDataPluginServices } from '@kbn/data-plugin/public'; import { TimeRange } from '@kbn/data-plugin/common'; import { DataView } from '@kbn/data-views-plugin/public'; -import { FilterBar } from '../filter_bar'; -import QueryBarTopRow from '../query_string_input/query_bar_top_row'; + import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; -import { SavedQueryManagementComponent } from '../saved_query_management'; +import { SavedQueryManagementList } from '../saved_query_management'; +import { QueryBarMenu } from '../query_string_input/query_bar_menu'; +import type { DataViewPickerProps } from '../dataview_picker'; +import QueryBarTopRow from '../query_string_input/query_bar_top_row'; +import { FilterBar, FilterItems } from '../filter_bar'; +import { searchBarStyles } from './search_bar.styles'; + +import '../index.scss'; export interface SearchBarInjectedDeps { kibana: KibanaReactContextValue; @@ -77,19 +82,20 @@ export interface SearchBarOwnProps { isClearable?: boolean; iconType?: EuiIconProps['type']; nonKqlMode?: 'lucene' | 'text'; - nonKqlModeHelpText?: string; - // defines padding; use 'inPage' to avoid extra padding; use 'detached' if the searchBar appears at the very top of the view, without any wrapper + // defines padding and border; use 'inPage' to avoid any padding or border; + // use 'detached' if the searchBar appears at the very top of the view, without any wrapper displayStyle?: 'inPage' | 'detached'; // super update button background fill control fillSubmitButton?: boolean; + dataViewPickerComponentProps?: DataViewPickerProps; + showSubmitButton?: boolean; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; interface State { isFiltersVisible: boolean; - showSaveQueryModal: boolean; - showSaveNewQueryModal: boolean; + openQueryBarMenu: boolean; showSavedQueryPopover: boolean; currentProps?: SearchBarProps; query?: Query; @@ -97,11 +103,12 @@ interface State { dateRangeTo: string; } -class SearchBarUI extends Component { +class SearchBarUI extends Component { public static defaultProps = { showQueryBar: true, showFilterBar: true, showDatePicker: true, + showSubmitButton: true, showAutoRefreshOnly: false, }; @@ -168,8 +175,7 @@ class SearchBarUI extends Component { */ public state = { isFiltersVisible: true, - showSaveQueryModal: false, - showSaveNewQueryModal: false, + openQueryBarMenu: false, showSavedQueryPopover: false, currentProps: this.props, query: this.props.query ? { ...this.props.query } : undefined, @@ -193,13 +199,6 @@ class SearchBarUI extends Component { this.renderSavedQueryManagement.clear(); } - private shouldRenderQueryBar() { - const showDatePicker = this.props.showDatePicker || this.props.showAutoRefreshOnly; - const showQueryInput = - this.props.showQueryInput && this.props.indexPatterns && this.state.query; - return this.props.showQueryBar && (showDatePicker || showQueryInput); - } - private shouldRenderFilterBar() { return ( this.props.showFilterBar && @@ -266,11 +265,6 @@ class SearchBarUI extends Component { `Your query "${response.attributes.title}" was saved` ); - this.setState({ - showSaveQueryModal: false, - showSaveNewQueryModal: false, - }); - if (this.props.onSaved) { this.props.onSaved(response); } @@ -282,18 +276,6 @@ class SearchBarUI extends Component { } }; - public onInitiateSave = () => { - this.setState({ - showSaveQueryModal: true, - }); - }; - - public onInitiateSaveNew = () => { - this.setState({ - showSaveNewQueryModal: true, - }); - }; - public onQueryBarChange = (queryAndDateRange: { dateRange: TimeRange; query?: Query }) => { this.setState({ query: queryAndDateRange.query, @@ -305,6 +287,12 @@ class SearchBarUI extends Component { } }; + public toggleFilterBarMenuPopover = (value: boolean) => { + this.setState({ + openQueryBarMenu: value, + }); + }; + public onQueryBarSubmit = (queryAndDateRange: { dateRange?: TimeRange; query?: Query }) => { this.setState( { @@ -349,12 +337,104 @@ class SearchBarUI extends Component { } }; + private shouldShowDatePickerAsBadge() { + return this.shouldRenderFilterBar() && !this.props.showQueryInput; + } + public render() { + const { theme } = this.props; + const styles = searchBarStyles(theme); + const cssStyles = [ + styles.uniSearchBar, + this.props.displayStyle && styles[this.props.displayStyle], + ]; + + const classes = classNames('uniSearchBar', { + [`uniSearchBar--${this.props.displayStyle}`]: this.props.displayStyle, + }); + const timeRangeForSuggestionsOverride = this.props.showDatePicker ? undefined : false; - let queryBar; - if (this.shouldRenderQueryBar()) { - queryBar = ( + const saveAsNewQueryFormComponent = ( + this.onSave(savedQueryMeta, true)} + onClose={() => this.setState({ openQueryBarMenu: false })} + showFilterOption={this.props.showFilterBar} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} + /> + ); + + const saveQueryFormComponent = ( + this.setState({ openQueryBarMenu: false })} + showFilterOption={this.props.showFilterBar} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} + /> + ); + + const queryBarMenu = ( + + ); + + let filterBar; + if (this.shouldRenderFilterBar()) { + filterBar = this.shouldShowDatePickerAsBadge() ? ( + + ) : ( + + ); + } + + return ( +
{ indexPatterns={this.props.indexPatterns} isLoading={this.props.isLoading} fillSubmitButton={this.props.fillSubmitButton || false} - prepend={ - this.props.showFilterBar && this.state.query - ? this.renderSavedQueryManagement( - this.props.onClearSavedQuery, - this.props.showSaveQuery, - this.props.savedQuery - ) - : undefined - } + prepend={this.props.showFilterBar || this.props.showQueryInput ? queryBarMenu : undefined} showDatePicker={this.props.showDatePicker} dateRangeFrom={this.state.dateRangeFrom} dateRangeTo={this.state.dateRangeTo} @@ -379,6 +451,7 @@ class SearchBarUI extends Component { refreshInterval={this.props.refreshInterval} showAutoRefreshOnly={this.props.showAutoRefreshOnly} showQueryInput={this.props.showQueryInput} + showAddFilter={this.props.showFilterBar} onRefresh={this.props.onRefresh} onRefreshChange={this.props.onRefreshChange} onChange={this.onQueryBarChange} @@ -386,70 +459,30 @@ class SearchBarUI extends Component { customSubmitButton={ this.props.customSubmitButton ? this.props.customSubmitButton : undefined } + showSubmitButton={this.props.showSubmitButton} dataTestSubj={this.props.dataTestSubj} indicateNoData={this.props.indicateNoData} placeholder={this.props.placeholder} isClearable={this.props.isClearable} iconType={this.props.iconType} nonKqlMode={this.props.nonKqlMode} - nonKqlModeHelpText={this.props.nonKqlModeHelpText} timeRangeForSuggestionsOverride={timeRangeForSuggestionsOverride} + filters={this.props.filters!} + onFiltersUpdated={this.props.onFiltersUpdated} + dataViewPickerComponentProps={this.props.dataViewPickerComponentProps} + showDatePickerAsBadge={this.shouldShowDatePickerAsBadge()} + filterBar={filterBar} /> - ); - } - - let filterBar; - if (this.shouldRenderFilterBar()) { - const filterGroupClasses = classNames('globalFilterGroup__wrapper', { - 'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible, - }); - - filterBar = ( -
- -
- ); - } - - const globalQueryBarClasses = classNames('globalQueryBar', { - 'globalQueryBar--inPage': this.props.displayStyle === 'inPage', - }); - - return ( -
- {queryBar} - {filterBar} - - {this.state.showSaveQueryModal ? ( - this.setState({ showSaveQueryModal: false })} - showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} - /> - ) : null} - {this.state.showSaveNewQueryModal ? ( - this.onSave(savedQueryMeta, true)} - onClose={() => this.setState({ showSaveNewQueryModal: false })} - showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} - /> - ) : null}
); } + private hasFiltersOrQuery() { + const hasFilters = Boolean(this.props.filters && this.props.filters.length > 0); + const hasQuery = Boolean(this.state.query && this.state.query.query); + return hasFilters || hasQuery; + } + private renderSavedQueryManagement = memoizeOne( ( onClearSavedQuery: SearchBarOwnProps['onClearSavedQuery'], @@ -457,14 +490,14 @@ class SearchBarUI extends Component { savedQuery: SearchBarOwnProps['savedQuery'] ) => { const savedQueryManagement = onClearSavedQuery && ( - this.setState({ openQueryBarMenu: false })} + hasFiltersOrQuery={this.hasFiltersOrQuery()} /> ); @@ -475,4 +508,4 @@ class SearchBarUI extends Component { // Needed for React.lazy // eslint-disable-next-line import/no-default-export -export default injectI18n(withKibana(SearchBarUI)); +export default injectI18n(withEuiTheme(withKibana(SearchBarUI))); diff --git a/src/plugins/unified_search/public/typeahead/_suggestion.scss b/src/plugins/unified_search/public/typeahead/_suggestion.scss index e466a52e7fc108..a59e53a102d6c1 100644 --- a/src/plugins/unified_search/public/typeahead/_suggestion.scss +++ b/src/plugins/unified_search/public/typeahead/_suggestion.scss @@ -15,12 +15,16 @@ $kbnTypeaheadTypes: ( @include euiBottomShadowFlat; border-top-left-radius: $euiBorderRadius; border-top-right-radius: $euiBorderRadius; + // Clips the shadow so it doesn't show above the input (below) + clip-path: polygon(-50px -50px, calc(100% + 50px) -50px, calc(100% + 50px) 100%, -50px 100%); } .kbnTypeahead__popover--bottom { @include euiBottomShadow; border-bottom-left-radius: $euiBorderRadius; border-bottom-right-radius: $euiBorderRadius; + // Clips the shadow so it doesn't show above the input (top) + clip-path: polygon(-50px 1px, calc(100% + 50px) 1px, calc(100% + 50px) calc(100% + 50px), -50px calc(100% + 50px)); } .kbnTypeahead { @@ -59,7 +63,6 @@ $kbnTypeaheadTypes: ( .kbnTypeahead__item:first-child { border-bottom: none; - border-radius: $euiBorderRadius $euiBorderRadius 0 0; } .kbnTypeahead__item.active { diff --git a/src/plugins/unified_search/public/typeahead/suggestions_component.tsx b/src/plugins/unified_search/public/typeahead/suggestions_component.tsx index 75e446cf2d6e8b..ebeddfaaff81fb 100644 --- a/src/plugins/unified_search/public/typeahead/suggestions_component.tsx +++ b/src/plugins/unified_search/public/typeahead/suggestions_component.tsx @@ -9,8 +9,7 @@ import React, { PureComponent, ReactNode } from 'react'; import { isEmpty } from 'lodash'; import classNames from 'classnames'; - -import styled from 'styled-components'; +import { css } from '@emotion/react'; import useRafState from 'react-use/lib/useRafState'; import { QuerySuggestion } from '../autocomplete'; @@ -146,15 +145,6 @@ export default class SuggestionsComponent extends PureComponent ` - position: absolute; - z-index: 4001; - left: ${props.left}px; - width: ${props.width}px; - ${props.verticalListPosition}`} -`; - const ResizableSuggestionsListDiv: React.FC<{ inputContainer: HTMLElement; suggestionsSize?: SuggestionsListSize; @@ -174,12 +164,16 @@ const ResizableSuggestionsListDiv: React.FC<{ ? `top: ${pageYOffset + containerRect.bottom - SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET}px;` : `bottom: ${documentHeight - (pageYOffset + containerRect.top)}px;`; + const divPosition = css` + position: absolute; + z-index: 4001; + left: ${containerRect.left}px; + width: ${containerRect.width}px; + ${verticalListPosition} + `; + return ( - +
- +
); }); diff --git a/src/plugins/vis_type_markdown/public/markdown_vis.ts b/src/plugins/vis_type_markdown/public/markdown_vis.ts index 7fcf9fb6311e67..a4b4010064e78d 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/plugins/vis_type_markdown/public/markdown_vis.ts @@ -58,6 +58,8 @@ export const markdownVisDefinition: VisTypeDefinition = { options: { showTimePicker: false, showFilterBar: false, + showQueryBar: true, + showQueryInput: false, }, inspectorAdapters: {}, }; diff --git a/src/plugins/vis_types/timelion/public/timelion_vis_type.tsx b/src/plugins/vis_types/timelion/public/timelion_vis_type.tsx index f32a485ac2565d..f8d7415f6aefef 100644 --- a/src/plugins/vis_types/timelion/public/timelion_vis_type.tsx +++ b/src/plugins/vis_types/timelion/public/timelion_vis_type.tsx @@ -66,8 +66,9 @@ export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) }, options: { showIndexSelection: false, - showQueryBar: false, + showQueryBar: true, showFilterBar: false, + showQueryInput: false, }, requiresSearch: true, }; diff --git a/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index 80295e5af2e401..bb197e219f439c 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -18,6 +18,7 @@ const defaultOptions: VisTypeOptions = { showQueryBar: true, showFilterBar: true, showIndexSelection: true, + showQueryInput: true, hierarchicalData: false, // we should get rid of this i guess ? }; diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 0e7e44b6ea38e4..383a238621e1e7 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -20,6 +20,7 @@ export interface VisTypeOptions { showQueryBar: boolean; showFilterBar: boolean; showIndexSelection: boolean; + showQueryInput: boolean; hierarchicalData: boolean; } diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx index a6c1710afbed8d..e42ee1d0cd6c03 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx @@ -161,7 +161,8 @@ const TopNav = ({ return vis.type.options.showTimePicker && hasTimeField; }; const showFilterBar = vis.type.options.showFilterBar; - const showQueryInput = vis.type.requiresSearch && vis.type.options.showQueryBar; + const showQueryInput = + vis.type.requiresSearch && vis.type.options.showQueryBar && vis.type.options.showQueryInput; useEffect(() => { return () => { diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 867e146e64ca33..5a3e881b86471c 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -11,6 +11,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'discover', 'header', 'share', 'timePicker']); const a11y = getService('a11y'); + const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const inspector = getService('inspector'); const testSubjects = getService('testSubjects'); const TEST_COLUMN_NAMES = ['dayOfWeek', 'DestWeather']; @@ -93,11 +94,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('a11y test on saved queries list panel', async () => { + await savedQueryManagementComponent.loadSavedQuery('test'); await PageObjects.discover.clickSavedQueriesPopOver(); - await testSubjects.moveMouseTo( - 'saved-query-list-item load-saved-query-test-button saved-query-list-item-selected saved-query-list-item-selected' - ); - await testSubjects.find('delete-saved-query-test-button'); + await testSubjects.click('saved-query-management-load-button'); + await savedQueryManagementComponent.deleteSavedQuery('test'); await a11y.testAppSnapshot(); }); }); diff --git a/test/accessibility/apps/filter_panel.ts b/test/accessibility/apps/filter_panel.ts index deb1e9512cd816..b479c62f489757 100644 --- a/test/accessibility/apps/filter_panel.ts +++ b/test/accessibility/apps/filter_panel.ts @@ -43,38 +43,47 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // the following tests filter panel options which changes UI it('a11y test on filter panel options panel', async () => { await filterBar.addFilter('DestCountry', 'is', 'AU'); - await testSubjects.click('showFilterActions'); + await testSubjects.click('showQueryBarMenu'); await a11y.testAppSnapshot(); }); it('a11y test on disable all filter options view', async () => { - await testSubjects.click('disableAllFilters'); + await testSubjects.click('filter-sets-applyToAllFilters'); + await testSubjects.click('filter-sets-disableAllFilters'); await a11y.testAppSnapshot(); }); - it('a11y test on pin filters view', async () => { - await testSubjects.click('showFilterActions'); - await testSubjects.click('enableAllFilters'); - await testSubjects.click('showFilterActions'); - await testSubjects.click('pinAllFilters'); + it('a11y test on enable all filters view', async () => { + await testSubjects.click('showQueryBarMenu'); + await testSubjects.click('filter-sets-applyToAllFilters'); + await testSubjects.click('filter-sets-enableAllFilters'); + await a11y.testAppSnapshot(); + }); + + it('a11y test on pin all filters view', async () => { + await testSubjects.click('showQueryBarMenu'); + await testSubjects.click('filter-sets-applyToAllFilters'); + await testSubjects.click('filter-sets-pinAllFilters'); await a11y.testAppSnapshot(); }); it('a11y test on unpin all filters view', async () => { - await testSubjects.click('showFilterActions'); - await testSubjects.click('unpinAllFilters'); + await testSubjects.click('showQueryBarMenu'); + await testSubjects.click('filter-sets-applyToAllFilters'); + await testSubjects.click('filter-sets-unpinAllFilters'); await a11y.testAppSnapshot(); }); it('a11y test on invert inclusion of all filters view', async () => { - await testSubjects.click('showFilterActions'); - await testSubjects.click('invertInclusionAllFilters'); + await testSubjects.click('showQueryBarMenu'); + await testSubjects.click('filter-sets-applyToAllFilters'); + await testSubjects.click('filter-sets-invertAllFilters'); await a11y.testAppSnapshot(); }); it('a11y test on remove all filtes view', async () => { - await testSubjects.click('showFilterActions'); - await testSubjects.click('removeAllFilters'); + await testSubjects.click('showQueryBarMenu'); + await testSubjects.click('filter-sets-removeAllFilters'); await a11y.testAppSnapshot(); }); }); diff --git a/test/functional/apps/dashboard/group1/embed_mode.ts b/test/functional/apps/dashboard/group1/embed_mode.ts index 25f48236ab7d58..482c976d98689a 100644 --- a/test/functional/apps/dashboard/group1/embed_mode.ts +++ b/test/functional/apps/dashboard/group1/embed_mode.ts @@ -59,7 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.missingOrFail('top-nav'); await testSubjects.missingOrFail('queryInput'); await testSubjects.missingOrFail('superDatePickerToggleQuickMenuButton'); - await testSubjects.existOrFail('showFilterActions'); + await testSubjects.existOrFail('showQueryBarMenu'); const currentUrl = await browser.getCurrentUrl(); const newUrl = [currentUrl].concat(urlParamExtensions).join('&'); @@ -70,7 +70,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('top-nav'); await testSubjects.existOrFail('queryInput'); await testSubjects.existOrFail('superDatePickerToggleQuickMenuButton'); - await testSubjects.missingOrFail('showFilterActions'); }); after(async function () { diff --git a/test/functional/apps/dashboard/group2/dashboard_saved_query.ts b/test/functional/apps/dashboard/group2/dashboard_saved_query.ts index ac9613f4bf400b..1dad54234e8a3c 100644 --- a/test/functional/apps/dashboard/group2/dashboard_saved_query.ts +++ b/test/functional/apps/dashboard/group2/dashboard_saved_query.ts @@ -40,22 +40,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickNewDashboard(); }); - it('should show the saved query management component when there are no saved queries', async () => { - await savedQueryManagementComponent.openSavedQueryManagementComponent(); - const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover'); - expect(descriptionText).to.eql( - 'Saved Queries\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' - ); + it('should show the saved query management load button as disabled when there are no saved queries', async () => { + await testSubjects.click('showQueryBarMenu'); + const loadFilterSetBtn = await testSubjects.find('saved-query-management-load-button'); + const isDisabled = await loadFilterSetBtn.getAttribute('disabled'); + expect(isDisabled).to.equal('true'); }); it('should allow a query to be saved via the saved objects management component', async () => { await queryBar.setQuery('response:200'); + await queryBar.clickQuerySubmitButton(); + await testSubjects.click('showQueryBarMenu'); await savedQueryManagementComponent.saveNewQuery( 'OkResponse', '200 responses for .jpg over 24 hours', true, true ); + const contextMenuPanelTitleButton = await testSubjects.exists( + 'contextMenuPanelTitleButton' + ); + if (contextMenuPanelTitleButton) { + await testSubjects.click('contextMenuPanelTitleButton'); + } + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); await savedQueryManagementComponent.savedQueryTextExist('response:200'); }); @@ -81,6 +89,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await queryBar.setQuery('response:404'); await savedQueryManagementComponent.updateCurrentlyLoadedQuery('OkResponse', false, false); await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + const contextMenuPanelTitleButton = await testSubjects.exists( + 'contextMenuPanelTitleButton' + ); + if (contextMenuPanelTitleButton) { + await testSubjects.click('contextMenuPanelTitleButton'); + } await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); expect(await queryBar.getQueryString()).to.eql(''); await savedQueryManagementComponent.loadSavedQuery('OkResponse'); @@ -88,9 +102,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('allows saving the currently loaded query as a new query', async () => { + await queryBar.setQuery('response:400'); await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( 'OkResponseCopy', - '200 responses', + '400 responses', false, false ); diff --git a/test/functional/apps/dashboard/group3/dashboard_state.ts b/test/functional/apps/dashboard/group3/dashboard_state.ts index 48fb9233682ad0..c5306f4ab4ff31 100644 --- a/test/functional/apps/dashboard/group3/dashboard_state.ts +++ b/test/functional/apps/dashboard/group3/dashboard_state.ts @@ -48,13 +48,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { isNewChartsLibraryEnabled = await PageObjects.visChart.isNewChartsLibraryEnabled(); await PageObjects.dashboard.initTests(); await PageObjects.dashboard.preserveCrossAppState(); + await browser.setLocalStorageItem('data.newDataViewMenu', 'true'); if (isNewChartsLibraryEnabled) { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyPieChartsLibrary': false, }); - await browser.refresh(); } + await browser.refresh(); }); after(async function () { diff --git a/test/functional/apps/dashboard_elements/controls/options_list.ts b/test/functional/apps/dashboard_elements/controls/options_list.ts index a4da1c217f92b1..de3144d98beabe 100644 --- a/test/functional/apps/dashboard_elements/controls/options_list.ts +++ b/test/functional/apps/dashboard_elements/controls/options_list.ts @@ -286,7 +286,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await header.waitUntilLoadingHasFinished(); await ensureAvailableOptionsEql(allAvailableOptions); - await filterBar.removeAllFilters(); + await filterBar.removeFilter('sound.keyword'); }); it('Does not apply time range to options list control', async () => { @@ -406,6 +406,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Options List dashboard no validation', async () => { before(async () => { + await filterBar.removeAllFilters(); + await queryBar.clickQuerySubmitButton(); await dashboardControls.optionsListOpenPopover(controlId); await dashboardControls.optionsListPopoverSelectOption('meow'); await dashboardControls.optionsListPopoverSelectOption('bark'); @@ -431,6 +433,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await filterBar.removeAllFilters(); + await queryBar.clickQuerySubmitButton(); await dashboardControls.clearAllControls(); }); }); diff --git a/test/functional/apps/discover/_indexpattern_without_timefield.ts b/test/functional/apps/discover/_indexpattern_without_timefield.ts index 2d5892fa6e6cac..6c936f63e999d4 100644 --- a/test/functional/apps/discover/_indexpattern_without_timefield.ts +++ b/test/functional/apps/discover/_indexpattern_without_timefield.ts @@ -91,7 +91,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.goBack(); await PageObjects.discover.waitForDocTableLoadingComplete(); return ( - (await testSubjects.getVisibleText('indexPattern-switch-link')) === 'without-timefield' + (await testSubjects.getVisibleText('discover-dataView-switch-link')) === + 'without-timefield' ); } ); diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 79d49131df1387..d56b5032a430b8 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -144,12 +144,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('saved query management component functionality', function () { before(async () => await setUpQueriesWithFilters()); - it('should show the saved query management component when there are no saved queries', async () => { + it('should show the saved query management load button as disabled when there are no saved queries', async () => { await savedQueryManagementComponent.openSavedQueryManagementComponent(); - const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover'); - expect(descriptionText).to.eql( - 'Saved Queries\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' - ); + const loadFilterSetBtn = await testSubjects.find('saved-query-management-load-button'); + const isDisabled = await loadFilterSetBtn.getAttribute('disabled'); + expect(isDisabled).to.equal('true'); }); it('should allow a query to be saved via the saved objects management component', async () => { @@ -189,9 +188,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('allows saving changes to a currently loaded query via the saved query management component', async () => { + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); await queryBar.setQuery('response:404'); await savedQueryManagementComponent.updateCurrentlyLoadedQuery('OkResponse', false, false); await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + const contextMenuPanelTitleButton = await testSubjects.exists( + 'contextMenuPanelTitleButton' + ); + if (contextMenuPanelTitleButton) { + await testSubjects.click('contextMenuPanelTitleButton'); + } await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); expect(await queryBar.getQueryString()).to.eql(''); await savedQueryManagementComponent.loadSavedQuery('OkResponse'); @@ -199,9 +205,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('allows saving the currently loaded query as a new query', async () => { + await queryBar.setQuery('response:400'); await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( 'OkResponseCopy', - '200 responses', + '400 responses', false, false ); @@ -215,6 +222,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('does not allow saving a query with a non-unique name', async () => { + // this check allows this test to run stand alone, also should fix occacional flakiness + const savedQueryExists = await savedQueryManagementComponent.savedQueryExist('OkResponse'); + if (!savedQueryExists) { + await savedQueryManagementComponent.saveNewQuery( + 'OkResponse', + '200 responses for .jpg over 24 hours', + true, + true + ); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + } + await queryBar.setQuery('response:400'); await savedQueryManagementComponent.saveNewQueryWithNameError('OkResponse'); }); @@ -232,17 +251,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('allows clearing if non default language was remembered in localstorage', async () => { + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.switchQueryLanguage('lucene'); await PageObjects.common.navigateToApp('discover'); // makes sure discovered is reloaded without any state in url + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.expectQueryLanguageOrFail('lucene'); // make sure lucene is remembered after refresh (comes from localstorage) await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.expectQueryLanguageOrFail('kql'); await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.expectQueryLanguageOrFail('lucene'); }); it('changing language removes saved query', async () => { await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.switchQueryLanguage('lucene'); expect(await queryBar.getQueryString()).to.eql(''); }); diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index 016cead53f0c43..1d9d02d5e94b58 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); - const PageObjects = getPageObjects(['common', 'header', 'home', 'timePicker']); + const PageObjects = getPageObjects(['common', 'header', 'home', 'timePicker', 'unifiedSearch']); const appsMenu = getService('appsMenu'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); @@ -37,6 +37,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Navigate to discover app await appsMenu.clickLink('Discover'); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); const discoverUrl = await browser.getCurrentUrl(); await PageObjects.timePicker.setDefaultAbsoluteRange(); const modifiedTimeDiscoverUrl = await browser.getCurrentUrl(); diff --git a/test/functional/apps/visualize/group2/_gauge_chart.ts b/test/functional/apps/visualize/group2/_gauge_chart.ts index 2c20c913b4d16d..08425fcd78b5f9 100644 --- a/test/functional/apps/visualize/group2/_gauge_chart.ts +++ b/test/functional/apps/visualize/group2/_gauge_chart.ts @@ -102,6 +102,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show correct values for fields with fieldFormatters', async () => { + await filterBar.removeAllFilters(); const expectedTexts = ['2,904', 'win 8: Count', '0B', 'win 8: Min bytes']; await PageObjects.visEditor.selectAggregation('Terms'); @@ -117,8 +118,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(expectedTexts).to.eql(metricValue); }); }); - - afterEach(async () => await filterBar.removeAllFilters()); }); }); } diff --git a/test/functional/apps/visualize/group6/_vega_chart.ts b/test/functional/apps/visualize/group6/_vega_chart.ts index 78a370523071bb..1d802065ad1378 100644 --- a/test/functional/apps/visualize/group6/_vega_chart.ts +++ b/test/functional/apps/visualize/group6/_vega_chart.ts @@ -220,7 +220,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Vega extension functions', () => { beforeEach(async () => { - await filterBar.removeAllFilters(); + const filtersCount = await filterBar.getFilterCount(); + if (filtersCount > 0) { + await filterBar.removeAllFilters(); + } }); const fillSpecAndGo = async (newSpec: string) => { diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 28ac88674b4a67..206cc82912c368 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -281,6 +281,7 @@ export class CommonPageObject extends FtrService { } if (appName === 'discover') { await this.browser.setLocalStorageItem('data.autocompleteFtuePopover', 'true'); + await this.browser.setLocalStorageItem('data.newDataViewMenu', 'true'); } return currentUrl; }); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index ce25370493823c..5691b4f5609c79 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -22,6 +22,9 @@ export class DiscoverPageObject extends FtrService { private readonly config = this.ctx.getService('config'); private readonly dataGrid = this.ctx.getService('dataGrid'); private readonly kibanaServer = this.ctx.getService('kibanaServer'); + private readonly queryBar = this.ctx.getService('queryBar'); + + private readonly unifiedSearch = this.ctx.getPageObject('unifiedSearch'); private readonly defaultFindTimeout = this.config.get('timeouts.find'); @@ -365,8 +368,7 @@ export class DiscoverPageObject extends FtrService { public async clickIndexPatternActions() { await this.retry.try(async () => { - await this.testSubjects.click('discoverIndexPatternActions'); - await this.testSubjects.existOrFail('discover-addRuntimeField-popover'); + await this.testSubjects.click('discover-dataView-switch-link'); }); } @@ -494,7 +496,7 @@ export class DiscoverPageObject extends FtrService { } public async selectIndexPattern(indexPattern: string) { - await this.testSubjects.click('indexPattern-switch-link'); + await this.testSubjects.click('discover-dataView-switch-link'); await this.find.setValue('[data-test-subj="indexPattern-switcher"] input', indexPattern); await this.find.clickByCssSelector( `[data-test-subj="indexPattern-switcher"] [title="${indexPattern}"]` @@ -557,6 +559,7 @@ export class DiscoverPageObject extends FtrService { await this.retry.waitFor('Discover app on screen', async () => { return await this.isDiscoverAppOnScreen(); }); + await this.unifiedSearch.closeTourPopoverByLocalStorage(); } public async showAllFilterActions() { @@ -564,10 +567,13 @@ export class DiscoverPageObject extends FtrService { } public async clickSavedQueriesPopOver() { - await this.testSubjects.click('saved-query-management-popover-button'); + await this.testSubjects.click('showQueryBarMenu'); } public async clickCurrentSavedQuery() { + await this.queryBar.setQuery('Cancelled : true'); + await this.queryBar.clickQuerySubmitButton(); + await this.testSubjects.click('showQueryBarMenu'); await this.testSubjects.click('saved-query-management-save-button'); } @@ -630,7 +636,7 @@ export class DiscoverPageObject extends FtrService { public async getCurrentlySelectedDataView() { await this.testSubjects.existOrFail('discover-sidebar'); - const button = await this.testSubjects.find('indexPattern-switch-link'); + const button = await this.testSubjects.find('discover-dataView-switch-link'); return button.getAttribute('title'); } } diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 826c4b78d1d0f1..bdfe91efef9007 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -31,6 +31,7 @@ import { SavedObjectsPageObject } from './management/saved_objects_page'; import { LegacyDataTableVisPageObject } from './legacy/data_table_vis'; import { IndexPatternFieldEditorPageObject } from './management/indexpattern_field_editor_page'; import { DashboardPageControls } from './dashboard_page_controls'; +import { UnifiedSearchPageObject } from './unified_search_page'; export const pageObjects = { common: CommonPageObject, @@ -58,4 +59,5 @@ export const pageObjects = { vegaChart: VegaChartPageObject, savedObjects: SavedObjectsPageObject, indexPatternFieldEditorObjects: IndexPatternFieldEditorPageObject, + unifiedSearch: UnifiedSearchPageObject, }; diff --git a/test/functional/page_objects/unified_search_page.ts b/test/functional/page_objects/unified_search_page.ts new file mode 100644 index 00000000000000..b1bcd0662f77e0 --- /dev/null +++ b/test/functional/page_objects/unified_search_page.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrService } from '../ftr_provider_context'; + +export class UnifiedSearchPageObject extends FtrService { + private readonly browser = this.ctx.getService('browser'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + + public async closeTour() { + const tourPopoverIsOpen = await this.testSubjects.exists('dataViewPickerTourLink'); + if (tourPopoverIsOpen) { + await this.testSubjects.click('dataViewPickerTourLink'); + } + } + + public async closeTourPopoverByLocalStorage() { + await this.browser.setLocalStorageItem('data.newDataViewMenu', 'true'); + await this.browser.refresh(); + } +} diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 20aec8ba5d9842..e087d50f21003a 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -39,6 +39,7 @@ export class VisualizePageObject extends FtrService { private readonly elasticChart = this.ctx.getService('elasticChart'); private readonly common = this.ctx.getPageObject('common'); private readonly header = this.ctx.getPageObject('header'); + private readonly unifiedSearch = this.ctx.getPageObject('unifiedSearch'); private readonly visEditor = this.ctx.getPageObject('visEditor'); private readonly visChart = this.ctx.getPageObject('visChart'); @@ -154,6 +155,10 @@ export class VisualizePageObject extends FtrService { public async clickVisType(type: string) { await this.testSubjects.click(`visType-${type}`); await this.header.waitUntilLoadingHasFinished(); + + if (type === 'lens') { + await this.unifiedSearch.closeTour(); + } } public async clickAreaChart() { diff --git a/test/functional/services/dashboard/visualizations.ts b/test/functional/services/dashboard/visualizations.ts index 8688d375f7a7b9..48828798a4efa3 100644 --- a/test/functional/services/dashboard/visualizations.ts +++ b/test/functional/services/dashboard/visualizations.ts @@ -17,6 +17,7 @@ export class DashboardVisualizationsService extends FtrService { private readonly visualize = this.ctx.getPageObject('visualize'); private readonly visEditor = this.ctx.getPageObject('visEditor'); private readonly header = this.ctx.getPageObject('header'); + private readonly unifiedSearch = this.ctx.getPageObject('unifiedSearch'); private readonly discover = this.ctx.getPageObject('discover'); private readonly timePicker = this.ctx.getPageObject('timePicker'); @@ -43,6 +44,7 @@ export class DashboardVisualizationsService extends FtrService { }) { this.log.debug(`createSavedSearch(${name})`); await this.header.clickDiscover(true); + await this.unifiedSearch.closeTourPopoverByLocalStorage(); await this.timePicker.setHistoricalDataRange(); if (query) { diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index eee1a1027f5419..7178013d5b9fd6 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -64,8 +64,8 @@ export class FilterBarService extends FtrService { * Removes all filters */ public async removeAllFilters(): Promise { - await this.testSubjects.click('showFilterActions'); - await this.testSubjects.click('removeAllFilters'); + await this.testSubjects.click('showQueryBarMenu'); + await this.testSubjects.click('filter-sets-removeAllFilters'); await this.header.waitUntilLoadingHasFinished(); await this.common.waitUntilUrlIncludes('filters:!()'); } diff --git a/test/functional/services/query_bar.ts b/test/functional/services/query_bar.ts index ec5fc039101a5c..ca6c161accc396 100644 --- a/test/functional/services/query_bar.ts +++ b/test/functional/services/query_bar.ts @@ -16,7 +16,6 @@ export class QueryBarService extends FtrService { private readonly common = this.ctx.getPageObject('common'); private readonly header = this.ctx.getPageObject('header'); private readonly find = this.ctx.getService('find'); - private readonly browser = this.ctx.getService('browser'); async getQueryString(): Promise { return await this.testSubjects.getAttribute('queryInput', 'value'); @@ -60,20 +59,19 @@ export class QueryBarService extends FtrService { public async switchQueryLanguage(lang: 'kql' | 'lucene'): Promise { await this.testSubjects.click('switchQueryLanguageButton'); - const kqlToggle = await this.testSubjects.find('languageToggle'); - const currentLang = - (await kqlToggle.getAttribute('aria-checked')) === 'true' ? 'kql' : 'lucene'; - if (lang !== currentLang) { - await kqlToggle.click(); + await this.testSubjects.click(`${lang}LanguageMenuItem`); + const contextMenuPanelTitleButton = await this.testSubjects.exists( + 'contextMenuPanelTitleButton' + ); + if (contextMenuPanelTitleButton) { + await this.testSubjects.click('contextMenuPanelTitleButton'); } - - await this.browser.pressKeys(this.browser.keys.ESCAPE); // close popover await this.expectQueryLanguageOrFail(lang); // make sure lang is switched } public async expectQueryLanguageOrFail(lang: 'kql' | 'lucene'): Promise { const queryLanguageButton = await this.testSubjects.find('switchQueryLanguageButton'); - expect((await queryLanguageButton.getVisibleText()).toLowerCase()).to.eql(lang); + expect((await queryLanguageButton.getVisibleText()).toLowerCase()).to.eql(`language: ${lang}`); } /** diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index a216f8cb0469e5..7822ed8f77a897 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -19,7 +19,7 @@ export class SavedQueryManagementComponentService extends FtrService { public async getCurrentlyLoadedQueryID() { await this.openSavedQueryManagementComponent(); try { - return await this.testSubjects.getVisibleText('~saved-query-list-item-selected'); + return await this.testSubjects.getVisibleText('savedQueryTitle'); } catch { return undefined; } @@ -53,7 +53,12 @@ export class SavedQueryManagementComponentService extends FtrService { return saveQueryFormSaveButtonStatus === false; }); - await this.testSubjects.click('savedQueryFormCancelButton'); + const contextMenuPanelTitleButton = await this.testSubjects.exists( + 'contextMenuPanelTitleButton' + ); + if (contextMenuPanelTitleButton) { + await this.testSubjects.click('contextMenuPanelTitleButton'); + } } public async saveCurrentlyLoadedAsNewQuery( @@ -63,7 +68,7 @@ export class SavedQueryManagementComponentService extends FtrService { includeTimeFilter: boolean ) { await this.openSavedQueryManagementComponent(); - await this.testSubjects.click('saved-query-management-save-as-new-button'); + await this.testSubjects.click('saved-query-management-save-button'); await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter); } @@ -79,12 +84,12 @@ export class SavedQueryManagementComponentService extends FtrService { public async loadSavedQuery(title: string) { await this.openSavedQueryManagementComponent(); + await this.testSubjects.click('saved-query-management-load-button'); await this.testSubjects.click(`~load-saved-query-${title}-button`); + await this.testSubjects.click('saved-query-management-apply-changes-button'); await this.retry.try(async () => { await this.openSavedQueryManagementComponent(); - const selectedSavedQueryText = await this.testSubjects.getVisibleText( - '~saved-query-list-item-selected' - ); + const selectedSavedQueryText = await this.testSubjects.getVisibleText('savedQueryTitle'); expect(selectedSavedQueryText).to.eql(title); }); await this.closeSavedQueryManagementComponent(); @@ -92,13 +97,24 @@ export class SavedQueryManagementComponentService extends FtrService { public async deleteSavedQuery(title: string) { await this.openSavedQueryManagementComponent(); - await this.testSubjects.click(`~delete-saved-query-${title}-button`); + const shouldClickLoadMenu = await this.testSubjects.exists( + 'saved-query-management-load-button' + ); + if (shouldClickLoadMenu) { + await this.testSubjects.click('saved-query-management-load-button'); + } + await this.testSubjects.click(`~load-saved-query-${title}-button`); + await this.retry.waitFor('delete saved query', async () => { + await this.testSubjects.click(`delete-saved-query-${title}-button`); + const exists = await this.testSubjects.exists('confirmModalTitleText'); + return exists === true; + }); await this.common.clickConfirmOnModal(); } async clearCurrentlyLoadedQuery() { await this.openSavedQueryManagementComponent(); - await this.testSubjects.click('saved-query-management-clear-button'); + await this.testSubjects.click('filter-sets-removeAllFilters'); await this.closeSavedQueryManagementComponent(); const queryString = await this.queryBar.getQueryString(); expect(queryString).to.be.empty(); @@ -113,7 +129,6 @@ export class SavedQueryManagementComponentService extends FtrService { if (title) { await this.testSubjects.setValue('saveQueryFormTitle', title); } - await this.testSubjects.setValue('saveQueryFormDescription', description); const currentIncludeFiltersValue = (await this.testSubjects.getAttribute( @@ -138,6 +153,7 @@ export class SavedQueryManagementComponentService extends FtrService { async savedQueryExist(title: string) { await this.openSavedQueryManagementComponent(); + await this.testSubjects.click('saved-query-management-load-button'); const exists = await this.testSubjects.exists(`~load-saved-query-${title}-button`); await this.closeSavedQueryManagementComponent(); return exists; @@ -145,6 +161,13 @@ export class SavedQueryManagementComponentService extends FtrService { async savedQueryExistOrFail(title: string) { await this.openSavedQueryManagementComponent(); + await this.retry.waitFor('load saved query', async () => { + const shouldClickLoadMenu = await this.testSubjects.exists( + 'saved-query-management-load-button' + ); + return shouldClickLoadMenu === true; + }); + await this.testSubjects.click('saved-query-management-load-button'); await this.testSubjects.existOrFail(`~load-saved-query-${title}-button`); } @@ -163,24 +186,19 @@ export class SavedQueryManagementComponentService extends FtrService { } async openSavedQueryManagementComponent() { - const isOpenAlready = await this.testSubjects.exists('saved-query-management-popover'); + const isOpenAlready = await this.testSubjects.exists('queryBarMenuPanel'); if (isOpenAlready) return; - await this.testSubjects.click('saved-query-management-popover-button'); - - await this.retry.waitFor('saved query management popover to have any text', async () => { - const queryText = await this.testSubjects.getVisibleText('saved-query-management-popover'); - return queryText.length > 0; - }); + await this.testSubjects.click('showQueryBarMenu'); } async closeSavedQueryManagementComponent() { - const isOpenAlready = await this.testSubjects.exists('saved-query-management-popover'); + const isOpenAlready = await this.testSubjects.exists('queryBarMenuPanel'); if (!isOpenAlready) return; await this.retry.try(async () => { - await this.testSubjects.click('saved-query-management-popover-button'); - await this.testSubjects.missingOrFail('saved-query-management-popover'); + await this.testSubjects.click('showQueryBarMenu'); + await this.testSubjects.missingOrFail('queryBarMenuPanel'); }); } @@ -197,7 +215,9 @@ export class SavedQueryManagementComponentService extends FtrService { async saveNewQueryMissingOrFail() { await this.openSavedQueryManagementComponent(); - await this.testSubjects.missingOrFail('saved-query-management-save-button'); + const saveFilterSetBtn = await this.testSubjects.find('saved-query-management-save-button'); + const isDisabled = await saveFilterSetBtn.getAttribute('disabled'); + expect(isDisabled).to.equal('true'); } async updateCurrentlyLoadedQueryMissingOrFail() { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.scss b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.scss index 6f274921d5ebf2..6b0624fae27576 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.scss +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.scss @@ -3,6 +3,10 @@ padding: $euiSizeS; } +.dvSearchPanel__container { + align-items: baseline; +} + @include euiBreakpoint('xs', 's', 'm', 'l') { .dvSearchPanel__container { flex-direction: column; @@ -13,8 +17,4 @@ .dvSearchPanel__controls { padding: 0; } - // prevent margin -16 which scrunches the filter bar - .globalFilterGroup__wrapper-isVisible { - margin: 0 !important; - } } diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx index e7ac50c9066606..7d218d98afa39a 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx @@ -120,7 +120,6 @@ export const SearchPanel: FC = ({ return ( { // pick the button component out of the tree because // it's part of a popover and thus not covered by enzyme - ( - instance.find(QueryStringInput).prop('prepend') as ReactElement - ).props.children.props.onClick(); + instance.find('[data-test-subj="graphDatasourceButton"]').first().simulate('click'); expect(openSourceModal).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/graph/public/components/search_bar.tsx b/x-pack/plugins/graph/public/components/search_bar.tsx index 762a2e87d2a5a8..046ed05977c798 100644 --- a/x-pack/plugins/graph/public/components/search_bar.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiToolTip } from '@elastic/eui'; import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; @@ -110,6 +110,47 @@ export function SearchBarComponent(props: SearchBarStateProps & SearchBarProps) }} > + + + { + confirmWipeWorkspace( + () => + openSourceModal({ overlays, savedObjects, uiSettings }, onIndexPatternSelected), + i18n.translate('xpack.graph.clearWorkspace.confirmText', { + defaultMessage: + 'If you change data sources, your current fields and vertices will be reset.', + }), + { + confirmButtonText: i18n.translate( + 'xpack.graph.clearWorkspace.confirmButtonLabel', + { + defaultMessage: 'Change data source', + } + ), + title: i18n.translate('xpack.graph.clearWorkspace.modalTitle', { + defaultMessage: 'Unsaved changes', + }), + } + ); + }} + > + {currentIndexPattern + ? currentIndexPattern.title + : // This branch will be shown if the user exits the + // initial picker modal + i18n.translate('xpack.graph.bar.pickSourceLabel', { + defaultMessage: 'Select a data source', + })} + + + - { - confirmWipeWorkspace( - () => - openSourceModal( - { overlays, savedObjects, uiSettings }, - onIndexPatternSelected - ), - i18n.translate('xpack.graph.clearWorkspace.confirmText', { - defaultMessage: - 'If you change data sources, your current fields and vertices will be reset.', - }), - { - confirmButtonText: i18n.translate( - 'xpack.graph.clearWorkspace.confirmButtonLabel', - { - defaultMessage: 'Change data source', - } - ), - title: i18n.translate('xpack.graph.clearWorkspace.modalTitle', { - defaultMessage: 'Unsaved changes', - }), - } - ); - }} - > - {currentIndexPattern - ? currentIndexPattern.title - : // This branch will be shown if the user exits the - // initial picker modal - i18n.translate('xpack.graph.bar.pickSourceLabel', { - defaultMessage: 'Select a data source', - })} - - - } onChange={setQuery} /> diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 8ed33fb3045258..e5a55322a2f105 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -20,6 +20,7 @@ "share", "presentationUtil", "dataViewFieldEditor", + "dataViewEditor", "expressionGauge", "expressionHeatmap", "eventAnnotation", diff --git a/x-pack/plugins/lens/public/app_plugin/app.scss b/x-pack/plugins/lens/public/app_plugin/app.scss index 99684a8b983c7a..58ecce55929370 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.scss +++ b/x-pack/plugins/lens/public/app_plugin/app.scss @@ -10,10 +10,6 @@ flex-direction: column; height: 100%; overflow: hidden; - - > .kbnTopNavMenu__wrapper { - border-bottom: $euiBorderThin; - } } .lnsApp__frame { diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index bfad8dcd3f0ee8..6e8cc4315ad8bf 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -380,6 +380,75 @@ describe('Lens App', () => { }); }); + describe('TopNavMenu#dataViewPickerProps', () => { + it('calls the nav component with the correct dataview picker props if no permissions are given', async () => { + const { instance, lensStore } = await mountWith({ preloadedState: {} }); + const document = { + savedObjectId: defaultSavedObjectId, + state: { + query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], + }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], + } as unknown as Document; + + act(() => { + lensStore.dispatch( + setState({ + query: 'fake query' as unknown as Query, + persistedDoc: document, + }) + ); + }); + instance.update(); + const props = instance + .find('[data-test-subj="lnsApp_topNav"]') + .prop('dataViewPickerComponentProps') as TopNavMenuData[]; + expect(props).toEqual( + expect.objectContaining({ + currentDataViewId: 'mockip', + onChangeDataView: expect.any(Function), + onDataViewCreated: expect.any(Function), + onAddField: undefined, + }) + ); + }); + + it('calls the nav component with the correct dataview picker props if permissions are given', async () => { + const { instance, lensStore, services } = await mountWith({ preloadedState: {} }); + services.dataViewFieldEditor.userPermissions.editIndexPattern = () => true; + const document = { + savedObjectId: defaultSavedObjectId, + state: { + query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], + }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], + } as unknown as Document; + + act(() => { + lensStore.dispatch( + setState({ + query: 'fake query' as unknown as Query, + persistedDoc: document, + }) + ); + }); + instance.update(); + const props = instance + .find('[data-test-subj="lnsApp_topNav"]') + .prop('dataViewPickerComponentProps') as TopNavMenuData[]; + expect(props).toEqual( + expect.objectContaining({ + currentDataViewId: 'mockip', + onChangeDataView: expect.any(Function), + onDataViewCreated: expect.any(Function), + onAddField: expect.any(Function), + }) + ); + }); + }); + describe('persistence', () => { it('passes query and indexPatterns to TopNavMenu', async () => { const { instance, lensStore, services } = await mountWith({ preloadedState: {} }); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index e532c82b7b3be9..4ae1b8860c8782 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -7,7 +7,7 @@ import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { useStore } from 'react-redux'; import { TopNavMenuData } from '@kbn/navigation-plugin/public'; import { downloadMultipleAs } from '@kbn/share-plugin/public'; @@ -16,6 +16,7 @@ import { exporters } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { trackUiEvent } from '../lens_ui_telemetry'; +import type { StateSetter } from '../types'; import { LensAppServices, LensTopNavActions, @@ -29,8 +30,15 @@ import { useLensDispatch, LensAppState, DispatchSetState, + updateDatasourceState, } from '../state_management'; -import { getIndexPatternsObjects, getIndexPatternsIds, getResolvedDateRange } from '../utils'; +import { + getIndexPatternsObjects, + getIndexPatternsIds, + getResolvedDateRange, + handleIndexPatternChange, + refreshIndexPatternsList, +} from '../utils'; import { combineQueryAndFilters, getLayerMetaInfo } from './show_underlying_data'; function getLensTopNavConfig(options: { @@ -222,6 +230,8 @@ export const LensTopNavMenu = ({ attributeService, discover, dashboardFeatureFlag, + dataViewFieldEditor, + dataViewEditor, dataViews, } = useKibana().services; @@ -232,7 +242,11 @@ export const LensTopNavMenu = ({ ); const [indexPatterns, setIndexPatterns] = useState([]); + const [currentIndexPattern, setCurrentIndexPattern] = useState(); const [rejectedIndexPatterns, setRejectedIndexPatterns] = useState([]); + const editPermission = dataViewFieldEditor.userPermissions.editIndexPattern(); + const closeFieldEditor = useRef<() => void | undefined>(); + const closeDataViewEditor = useRef<() => void | undefined>(); const { isSaveable, @@ -293,6 +307,20 @@ export const LensTopNavMenu = ({ dataViews, ]); + useEffect(() => { + if (indexPatterns.length > 0) { + setCurrentIndexPattern(indexPatterns[0]); + } + }, [indexPatterns]); + + useEffect(() => { + return () => { + // Make sure to close the editors when unmounting + closeFieldEditor.current?.(); + closeDataViewEditor.current?.(); + }; + }, []); + const { TopNavMenu } = navigation.ui; const { from, to } = data.query.timefilter.timefilter.getTime(); @@ -576,6 +604,123 @@ export const LensTopNavMenu = ({ }); }, [data.query.filterManager, data.query.queryString, dispatchSetState]); + const setDatasourceState: StateSetter = useMemo(() => { + return (updater) => { + dispatch( + updateDatasourceState({ + updater, + datasourceId: activeDatasourceId!, + clearStagedPreview: true, + }) + ); + }; + }, [activeDatasourceId, dispatch]); + + const refreshFieldList = useCallback(async () => { + if (currentIndexPattern && currentIndexPattern.id) { + refreshIndexPatternsList({ + activeDatasources: Object.keys(datasourceStates).reduce( + (acc, datasourceId) => ({ + ...acc, + [datasourceId]: datasourceMap[datasourceId], + }), + {} + ), + indexPatternId: currentIndexPattern.id, + setDatasourceState, + }); + } + // start a new session so all charts are refreshed + data.search.session.start(); + }, [ + currentIndexPattern, + data.search.session, + datasourceMap, + datasourceStates, + setDatasourceState, + ]); + + const editField = useMemo( + () => + editPermission + ? async (fieldName?: string, uiAction: 'edit' | 'add' = 'edit') => { + if (currentIndexPattern?.id) { + const indexPatternInstance = await data.dataViews.get(currentIndexPattern?.id); + closeFieldEditor.current = dataViewFieldEditor.openEditor({ + ctx: { + dataView: indexPatternInstance, + }, + fieldName, + onSave: async () => { + refreshFieldList(); + }, + }); + } + } + : undefined, + [editPermission, currentIndexPattern?.id, data.dataViews, dataViewFieldEditor, refreshFieldList] + ); + + const addField = useMemo( + () => (editPermission && editField ? () => editField(undefined, 'add') : undefined), + [editField, editPermission] + ); + + const createNewDataView = useCallback(() => { + const dataViewEditPermission = dataViewEditor.userPermissions.editDataView; + if (!dataViewEditPermission) { + return; + } + closeDataViewEditor.current = dataViewEditor.openEditor({ + onSave: async (dataView) => { + if (dataView.id) { + handleIndexPatternChange({ + activeDatasources: Object.keys(datasourceStates).reduce( + (acc, datasourceId) => ({ + ...acc, + [datasourceId]: datasourceMap[datasourceId], + }), + {} + ), + datasourceStates, + indexPatternId: dataView.id, + setDatasourceState, + }); + refreshFieldList(); + } + }, + }); + }, [dataViewEditor, datasourceMap, datasourceStates, refreshFieldList, setDatasourceState]); + + const dataViewPickerProps = { + trigger: { + label: currentIndexPattern?.title || '', + 'data-test-subj': 'lns-dataView-switch-link', + title: currentIndexPattern?.title || '', + }, + currentDataViewId: currentIndexPattern?.id, + onAddField: addField, + onDataViewCreated: createNewDataView, + onChangeDataView: (newIndexPatternId: string) => { + const currentDataView = indexPatterns.find( + (indexPattern) => indexPattern.id === newIndexPatternId + ); + setCurrentIndexPattern(currentDataView); + handleIndexPatternChange({ + activeDatasources: Object.keys(datasourceStates).reduce( + (acc, datasourceId) => ({ + ...acc, + [datasourceId]: datasourceMap[datasourceId], + }), + {} + ), + datasourceStates, + indexPatternId: newIndexPatternId, + setDatasourceState, + }); + }, + }; + return ( ip.isTimeBased()) || Boolean( @@ -607,6 +753,7 @@ export const LensTopNavMenu = ({ data-test-subj="lnsApp_topNav" screenTitle={'lens'} appName={'lens'} + displayStyle="detached" /> ); }; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index f7d865e92853e7..6ddd49a7e5df03 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -84,6 +84,8 @@ export async function getLensServices( notifications: coreStart.notifications, savedObjectsClient: coreStart.savedObjects.client, presentationUtil: startDependencies.presentationUtil, + dataViewEditor: startDependencies.dataViewEditor, + dataViewFieldEditor: startDependencies.dataViewFieldEditor, dashboard: startDependencies.dashboard, getOriginatingAppName: () => { return embeddableEditorIncomingState?.originatingApp diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 94507754b893f4..abb6cfa6a06a6e 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -30,6 +30,8 @@ import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public' import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { DashboardFeatureFlagConfig } from '@kbn/dashboard-plugin/public'; import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; +import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; +import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import { VisualizeFieldContext, ACTION_VISUALIZE_LENS_FIELD } from '@kbn/ui-actions-plugin/public'; import { ACTION_CONVERT_TO_LENS } from '@kbn/visualizations-plugin/public'; import type { EmbeddableEditorState, EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; @@ -140,6 +142,8 @@ export interface LensAppServices { // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag: DashboardFeatureFlagConfig; + dataViewEditor: DataViewEditorStart; + dataViewFieldEditor: IndexPatternFieldEditorStart; } export interface LensTopNavTooltips { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx index 1baad07b2198c3..ae087221fd49ae 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx @@ -7,8 +7,9 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; -import { EuiPopover, EuiPopoverTitle, EuiSelectable, EuiSelectableProps } from '@elastic/eui'; +import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui'; import { ToolbarButton, ToolbarButtonProps } from '@kbn/kibana-react-plugin/public'; +import { DataViewsList } from '@kbn/unified-search-plugin/public'; import { IndexPatternRef } from './types'; import { trackUiEvent } from '../lens_ui_telemetry'; @@ -67,50 +68,26 @@ export function ChangeIndexPattern({ isOpen={isPopoverOpen} closePopover={() => setPopoverIsOpen(false)} display="block" - panelPaddingSize="s" + panelPaddingSize="none" ownFocus >
- + {i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', { defaultMessage: 'Data view', })} - - {...selectableProps} - searchable - singleSelection="always" - options={indexPatternRefs.map(({ title, id }) => ({ - key: id, - label: title, - value: id, - checked: id === indexPatternId ? 'on' : undefined, - }))} - onChange={(choices) => { - const choice = choices.find(({ checked }) => checked) as unknown as { - value: string; - }; + + { trackUiEvent('indexpattern_changed'); - onChangeIndexPattern(choice.value); + onChangeIndexPattern(newId); setPopoverIsOpen(false); }} - searchProps={{ - compressed: true, - ...(selectableProps ? selectableProps.searchProps : undefined), - }} - > - {(list, search) => ( - <> - {search} - {list} - - )} - + currentDataViewId={indexPatternId} + selectableProps={selectableProps} + />
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 512ef627c9116f..9aaaf9c128a111 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; -import { waitFor } from '@testing-library/react'; import ReactDOM from 'react-dom'; import { createMockedDragDropContext } from './mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; @@ -19,7 +18,6 @@ import { act } from 'react-dom/test-utils'; import { coreMock } from '@kbn/core/public/mocks'; import { IndexPatternPrivateState } from './types'; import { mountWithIntl, shallowWithIntl } from '@kbn/test-jest-helpers'; -import { ChangeIndexPattern } from './change_indexpattern'; import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui'; import { documentField } from './document_field'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; @@ -328,14 +326,6 @@ describe('IndexPattern Data Panel', () => { expect(wrapper.find('[data-test-subj="indexPattern-no-indexpatterns"]')).toHaveLength(1); }); - it('should call setState when the index pattern is switched', async () => { - const wrapper = shallowWithIntl(); - - wrapper.find(ChangeIndexPattern).prop('onChangeIndexPattern')('2'); - - expect(defaultProps.onChangeIndexPattern).toHaveBeenCalledWith('2'); - }); - describe('loading existence data', () => { function testProps() { const setState = jest.fn(); @@ -853,90 +843,5 @@ describe('IndexPattern Data Panel', () => { 'memory', ]); }); - describe('edit field list', () => { - beforeEach(() => { - props.indexPatternFieldEditor.userPermissions.editIndexPattern = () => true; - }); - it('should call field editor plugin on clicking add button', async () => { - const mockIndexPattern = {}; - (props.dataViews.get as jest.Mock).mockImplementation(() => - Promise.resolve(mockIndexPattern) - ); - const wrapper = mountWithIntl(); - act(() => { - const popoverTrigger = wrapper.find( - '[data-test-subj="lnsIndexPatternActions-popover"] button' - ); - popoverTrigger.simulate('click'); - }); - - wrapper.update(); - act(() => { - wrapper.find('[data-test-subj="indexPattern-add-field"]').first().simulate('click'); - }); - // wait for indx pattern to be loaded - await waitFor(() => { - expect(props.indexPatternFieldEditor.openEditor).toHaveBeenCalledWith( - expect.objectContaining({ - ctx: expect.objectContaining({ - dataView: mockIndexPattern, - }), - }) - ); - }); - }); - - it('should reload index pattern if callback gets called', async () => { - const mockIndexPattern = { - id: '1', - fields: [ - { - name: 'fieldOne', - aggregatable: true, - }, - ], - metaFields: [], - }; - (props.dataViews.get as jest.Mock).mockImplementation(() => - Promise.resolve(mockIndexPattern) - ); - const wrapper = mountWithIntl(); - - act(() => { - const popoverTrigger = wrapper.find( - '[data-test-subj="lnsIndexPatternActions-popover"] button' - ); - popoverTrigger.simulate('click'); - }); - - wrapper.update(); - act(() => { - wrapper.find('[data-test-subj="indexPattern-add-field"]').first().simulate('click'); - }); - - // wait for indx pattern to be loaded - await act(async () => await new Promise((r) => setTimeout(r, 0))); - - await (props.indexPatternFieldEditor.openEditor as jest.Mock).mock.calls[0][0].onSave(); - // wait for indx pattern to be loaded - await act(async () => await new Promise((r) => setTimeout(r, 0))); - expect(props.onUpdateIndexPattern).toHaveBeenCalledWith( - expect.objectContaining({ - fields: [ - expect.objectContaining({ - name: 'fieldOne', - }), - expect.anything(), - ], - }) - ); - }); - - it('should not render add button without permissions', () => { - props.indexPatternFieldEditor.userPermissions.editIndexPattern = () => false; - const wrapper = mountWithIntl(); - expect(wrapper.find('[data-test-subj="indexPattern-add-field"]').exists()).toBe(false); - }); - }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index ab437b9328e7e0..d4cdca9a4c7fa2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -20,7 +20,6 @@ import { EuiFilterGroup, EuiFilterButton, EuiScreenReaderOnly, - EuiButtonIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { EsQueryConfig, Query, Filter } from '@kbn/es-query'; @@ -47,6 +46,8 @@ import { trackUiEvent } from '../lens_ui_telemetry'; import { loadIndexPatterns, syncExistingFields } from './loader'; import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; +import { LensFieldIcon } from './lens_field_icon'; +import { FieldGroups, FieldList } from './field_list'; export type Props = Omit, 'core'> & { data: DataPublicPluginStart; @@ -61,9 +62,6 @@ export type Props = Omit, 'co core: CoreStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; }; -import { LensFieldIcon } from './lens_field_icon'; -import { ChangeIndexPattern } from './change_indexpattern'; -import { FieldGroups, FieldList } from './field_list'; function sortFields(fieldA: IndexPatternField, fieldB: IndexPatternField) { return fieldA.displayName.localeCompare(fieldB.displayName, undefined, { sensitivity: 'base' }); @@ -573,11 +571,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ [currentIndexPattern.id, dataViews, editPermission, indexPatternFieldEditor, refreshFieldList] ); - const addField = useMemo( - () => (editPermission && editField ? () => editField(undefined, 'add') : undefined), - [editField, editPermission] - ); - const fieldProps = useMemo( () => ({ core, @@ -603,8 +596,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ ] ); - const [popoverOpen, setPopoverOpen] = useState(false); - return ( - - - - { - onChangeIndexPattern(newId); - clearLocalState(); - }} - /> - - {addField && ( - - { - setPopoverOpen(false); - }} - ownFocus - data-test-subj="lnsIndexPatternActions-popover" - button={ - { - setPopoverOpen(!popoverOpen); - }} - /> - } - > - { - setPopoverOpen(false); - addField(); - }} - > - {i18n.translate('xpack.lens.indexPatterns.addFieldButton', { - defaultMessage: 'Add field to data view', - })} - , - { - setPopoverOpen(false); - core.application.navigateToApp('management', { - path: `/kibana/indexPatterns/patterns/${currentIndexPattern.id}`, - }); - }} - > - {i18n.translate('xpack.lens.indexPatterns.manageFieldButton', { - defaultMessage: 'Manage data view fields', - })} - , - ]} - /> - - - )} - - { + handleChangeIndexPattern(indexPatternId, state, setState); + }, + + refreshIndexPatternsList: async ({ indexPatternId, setState }) => { + const newlyMappedIndexPattern = await loadIndexPatterns({ + indexPatternsService: dataViews, + cache: {}, + patterns: [indexPatternId], + }); + const indexPatternRefs = await dataViews.getIdsWithTitle(); + const indexPattern = newlyMappedIndexPattern[indexPatternId]; + setState((s) => { + return { + ...s, + indexPatterns: { + ...s.indexPatterns, + [indexPattern.id]: indexPattern, + }, + indexPatternRefs, + }; + }); + }, + // Reset the temporary invalid state when closing the editor, but don't // update the state if it's not needed updateStateOnCloseDimension: ({ state, layerId }) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 91b9de58bdaa15..dba57f2fcb03ec 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -11,6 +11,7 @@ import { IndexPatternLayerPanelProps, LayerPanel } from './layerpanel'; import { shallowWithIntl as shallow } from '@kbn/test-jest-helpers'; import { ShallowWrapper } from 'enzyme'; import { EuiSelectable } from '@elastic/eui'; +import { DataViewsList } from '@kbn/unified-search-plugin/public'; import { ChangeIndexPattern } from './change_indexpattern'; import { getFieldByNameFactory } from './pure_helpers'; import { TermsIndexPatternColumn } from './operations'; @@ -212,7 +213,14 @@ describe('Layer Data Panel', () => { }); function getIndexPatternPickerList(instance: ShallowWrapper) { - return instance.find(ChangeIndexPattern).first().dive().find(EuiSelectable); + return instance + .find(ChangeIndexPattern) + .first() + .dive() + .find(DataViewsList) + .first() + .dive() + .find(EuiSelectable); } function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx index f8548321e49bd3..efa1ef509b12d7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx @@ -22,7 +22,6 @@ export function LayerPanel({ state, layerId, onChangeIndexPattern }: IndexPatter const layer = state.layers[layerId]; const indexPattern = state.indexPatterns[layer.indexPatternId]; - const notFoundTitleLabel = i18n.translate('xpack.lens.layerPanel.missingDataView', { defaultMessage: 'Data view not found', }); diff --git a/x-pack/plugins/lens/public/mocks/datasource_mock.ts b/x-pack/plugins/lens/public/mocks/datasource_mock.ts index c30b39476b1ab4..cf25828d3322ca 100644 --- a/x-pack/plugins/lens/public/mocks/datasource_mock.ts +++ b/x-pack/plugins/lens/public/mocks/datasource_mock.ts @@ -36,6 +36,7 @@ export function createMockDatasource(id: string): DatasourceMock { initialize: jest.fn((_state?) => Promise.resolve()), renderDataPanel: jest.fn(), renderLayerPanel: jest.fn(), + getCurrentIndexPatternId: jest.fn(), toExpression: jest.fn((_frame, _state) => null), insertLayer: jest.fn((_state, _newLayerId) => ({})), removeLayer: jest.fn((_state, _layerId) => {}), diff --git a/x-pack/plugins/lens/public/mocks/services_mock.tsx b/x-pack/plugins/lens/public/mocks/services_mock.tsx index f5e94d374481ae..800ec3dee25b1d 100644 --- a/x-pack/plugins/lens/public/mocks/services_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/services_mock.tsx @@ -10,6 +10,8 @@ import { Subject } from 'rxjs'; import { coreMock } from '@kbn/core/public/mocks'; import { navigationPluginMock } from '@kbn/navigation-plugin/public/mocks'; import { UI_SETTINGS } from '@kbn/data-plugin/public'; +import { indexPatternFieldEditorPluginMock } from '@kbn/data-view-field-editor-plugin/public/mocks'; +import { indexPatternEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks'; import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks'; import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks'; import { dashboardPluginMock } from '@kbn/dashboard-plugin/public/mocks'; @@ -155,5 +157,7 @@ export function makeDefaultServices( clear: jest.fn(), }, spaces: spacesPluginMock.createStartContract(), + dataViewFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), + dataViewEditor: indexPatternEditorPluginMock.createStartContract(), }; } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index b39c14cd82454a..876cb63b0333d0 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -32,6 +32,7 @@ import type { EventAnnotationPluginSetup } from '@kbn/event-annotation-plugin/pu import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; +import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; import { AppNavLinkStatus } from '@kbn/core/public'; import { @@ -123,6 +124,7 @@ export interface LensPluginStartDependencies { savedObjectsTagging?: SavedObjectTaggingPluginStart; presentationUtil: PresentationUtilPluginStart; dataViewFieldEditor: IndexPatternFieldEditorStart; + dataViewEditor: DataViewEditorStart; inspector: InspectorStartContract; spaces: SpacesPluginStart; usageCollection?: UsageCollectionStart; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 8c6c6d9af22dcc..a91240e7e6a3e8 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -223,6 +223,7 @@ export interface Datasource { // Given the current state, which parts should be saved? getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; + getCurrentIndexPatternId: (state: T) => string; insertLayer: (state: T, newLayerId: string) => T; removeLayer: (state: T, layerId: string) => T; @@ -274,6 +275,14 @@ export interface Datasource { state: T; }) => T | undefined; + updateCurrentIndexPatternId?: (props: { + indexPatternId: string; + state: T; + setState: StateSetter; + }) => void; + + refreshIndexPatternsList?: (props: { indexPatternId: string; setState: StateSetter }) => void; + toExpression: (state: T, layerId: string) => ExpressionAstExpression | string | null; getDatasourceSuggestionsForField: ( diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index b5ada350b2aaa4..2a2bd0a35efa19 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -19,6 +19,7 @@ import type { LensBrushEvent, LensFilterEvent, Visualization, + StateSetter, } from './types'; import type { DatasourceStates, VisualizationState } from './state_management'; @@ -63,6 +64,43 @@ export const getInitialDatasourceId = (datasourceMap: DatasourceMap, doc?: Docum return (doc && getActiveDatasourceIdFromDoc(doc)) || Object.keys(datasourceMap)[0] || null; }; +export function handleIndexPatternChange({ + activeDatasources, + datasourceStates, + indexPatternId, + setDatasourceState, +}: { + activeDatasources: Record; + datasourceStates: DatasourceStates; + indexPatternId: string; + setDatasourceState: StateSetter; +}): void { + Object.entries(activeDatasources).forEach(([id, datasource]) => { + datasource?.updateCurrentIndexPatternId?.({ + state: datasourceStates[id].state, + indexPatternId, + setState: setDatasourceState, + }); + }); +} + +export function refreshIndexPatternsList({ + activeDatasources, + indexPatternId, + setDatasourceState, +}: { + activeDatasources: Record; + indexPatternId: string; + setDatasourceState: StateSetter; +}): void { + Object.entries(activeDatasources).forEach(([id, datasource]) => { + datasource?.refreshIndexPatternsList?.({ + indexPatternId, + setState: setDatasourceState, + }); + }); +} + export function getIndexPatternsIds({ activeDatasources, datasourceStates, @@ -70,17 +108,21 @@ export function getIndexPatternsIds({ activeDatasources: Record; datasourceStates: DatasourceStates; }): string[] { + let currentIndexPatternId: string | undefined; const references: SavedObjectReference[] = []; Object.entries(activeDatasources).forEach(([id, datasource]) => { const { savedObjectReferences } = datasource.getPersistableState(datasourceStates[id].state); + const indexPatternId = datasource.getCurrentIndexPatternId(datasourceStates[id].state); + currentIndexPatternId = indexPatternId; references.push(...savedObjectReferences); }); - - const uniqueFilterableIndexPatternIds = uniq( - references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) - ); - - return uniqueFilterableIndexPatternIds; + const referencesIds = references + .filter(({ type }) => type === 'index-pattern') + .map(({ id }) => id); + if (currentIndexPatternId) { + referencesIds.unshift(currentIndexPatternId); + } + return uniq(referencesIds); } export async function getIndexPatternsObjects( diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 380d387249e178..e00581833f621c 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -28,13 +28,14 @@ { "path": "../../../src/plugins/saved_objects/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../../../src/plugins/embeddable/tsconfig.json" }, - { "path": "../../../src/plugins/presentation_util/tsconfig.json" }, - { "path": "../../../src/plugins/field_formats/tsconfig.json" }, - { "path": "../../../src/plugins/chart_expressions/expression_xy/tsconfig.json" }, - { "path": "../../../src/plugins/chart_expressions/expression_heatmap/tsconfig.json" }, - { "path": "../../../src/plugins/chart_expressions/expression_gauge/tsconfig.json" }, - { "path": "../../../src/plugins/event_annotation/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json"}, + { "path": "../../../src/plugins/presentation_util/tsconfig.json"}, + { "path": "../../../src/plugins/field_formats/tsconfig.json"}, + { "path": "../../../src/plugins/chart_expressions/expression_heatmap/tsconfig.json"}, + { "path": "../../../src/plugins/chart_expressions/expression_gauge/tsconfig.json"}, + { "path": "../../../src/plugins/data_view_editor/tsconfig.json"}, + { "path": "../../../src/plugins/event_annotation/tsconfig.json"}, + { "path": "../../../src/plugins/chart_expressions/expression_xy/tsconfig.json"}, { "path": "../../../src/plugins/unified_search/tsconfig.json" } ] } diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index 3c4d434b1ec3f3..287281619cb08f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -94,8 +94,8 @@ export const setEnrichmentDates = (from?: string, to?: string) => { export const goToClosedAlerts = () => { cy.get(CLOSED_ALERTS_FILTER_BTN).click(); - cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); - cy.get(REFRESH_BUTTON).should('have.text', 'Refresh'); + cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); + cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query'); cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist'); }; @@ -105,13 +105,13 @@ export const goToManageAlertsDetectionRules = () => { export const goToOpenedAlerts = () => { cy.get(OPENED_ALERTS_FILTER_BTN).click({ force: true }); - cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); - cy.get(REFRESH_BUTTON).should('have.text', 'Refresh'); + cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); + cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query'); }; export const refreshAlerts = () => { // ensure we've refetched fields the first time index is defined - cy.get(REFRESH_BUTTON).should('have.text', 'Refresh'); + cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query'); cy.get(REFRESH_BUTTON).first().click({ force: true }); }; @@ -127,8 +127,8 @@ export const openAlerts = () => { export const goToAcknowledgedAlerts = () => { cy.get(ACKNOWLEDGED_ALERTS_FILTER_BTN).click(); - cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); - cy.get(REFRESH_BUTTON).should('have.text', 'Refresh'); + cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); + cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query'); cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist'); }; @@ -154,7 +154,7 @@ export const investigateFirstAlertInTimeline = () => { }; export const waitForAlerts = () => { - cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); + cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); }; export const waitForAlertsPanelToBeLoaded = () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts index 508e76851f7ff8..cf6c6ae4670923 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts @@ -10,5 +10,5 @@ import { REFRESH_BUTTON } from '../../screens/security_header'; export const waitForAuthenticationsToBeLoaded = () => { cy.get(AUTHENTICATIONS_TABLE).should('exist'); - cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); + cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts index 66f7f0cb9f3b8f..67bec8904a8497 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts @@ -10,5 +10,5 @@ import { REFRESH_BUTTON } from '../../screens/security_header'; export const waitForUncommonProcessesToBeLoaded = () => { cy.get(UNCOMMON_PROCESSES_TABLE).should('exist'); - cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); + cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_header.ts b/x-pack/plugins/security_solution/cypress/tasks/security_header.ts index 57cf72ed85a6e8..a50851fa87c77d 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/security_header.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_header.ts @@ -21,5 +21,7 @@ export const navigateFromHeaderTo = (page: string) => { }; export const refreshPage = () => { - cy.get(REFRESH_BUTTON).click({ force: true }).should('not.have.text', 'Updating'); + cy.get(REFRESH_BUTTON) + .click({ force: true }) + .should('not.have.attr', 'aria-label', 'Needs updating'); }; diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap index 51326d54a61611..4dd14f56997ebf 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap @@ -7,7 +7,7 @@ exports[`rendering renders correctly 1`] = ` (({ children, show = return ( - + {children} diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index 2d38f72b338eeb..1620a142c15cba 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -87,6 +87,7 @@ describe('QueryBar ', () => { dataTestSubj: undefined, dateRangeFrom: 'now/d', dateRangeTo: 'now/d', + displayStyle: undefined, filters: [], indexPatterns: [ { @@ -205,6 +206,7 @@ describe('QueryBar ', () => { showQueryBar: true, showQueryInput: true, showSaveQuery: true, + showSubmitButton: false, }); }); @@ -304,7 +306,7 @@ describe('QueryBar ', () => { }); describe('SavedQueryManagementComponent state', () => { - test('popover should hidden when "Save current query" button was clicked', async () => { + test('popover should remain open when "Save current query" button was clicked', async () => { const wrapper = await getWrapper( { /> ); const isSavedQueryPopoverOpen = () => - wrapper.find('EuiPopover[id="savedQueryPopover"]').prop('isOpen'); + wrapper.find('EuiPopover[data-test-subj="queryBarMenuPopover"]').prop('isOpen'); expect(isSavedQueryPopoverOpen()).toBeFalsy(); - wrapper - .find('button[data-test-subj="saved-query-management-popover-button"]') - .simulate('click'); + wrapper.find('button[data-test-subj="showQueryBarMenu"]').simulate('click'); await waitFor(() => { expect(isSavedQueryPopoverOpen()).toBeTruthy(); @@ -338,7 +338,7 @@ describe('QueryBar ', () => { wrapper.find('button[data-test-subj="saved-query-management-save-button"]').simulate('click'); await waitFor(() => { - expect(isSavedQueryPopoverOpen()).toBeFalsy(); + expect(isSavedQueryPopoverOpen()).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx index 15fd5927b7f754..fe8d50d6fab2ea 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx @@ -17,7 +17,7 @@ import { SavedQueryTimeFilter, } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; -import { SearchBar } from '@kbn/unified-search-plugin/public'; +import { SearchBar, SearchBarProps } from '@kbn/unified-search-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; export interface QueryBarComponentProps { @@ -36,6 +36,7 @@ export interface QueryBarComponentProps { refreshInterval?: number; savedQuery?: SavedQuery; onSavedQuery: (savedQuery: SavedQuery | undefined) => void; + displayStyle?: SearchBarProps['displayStyle']; } export const QueryBar = memo( @@ -55,6 +56,7 @@ export const QueryBar = memo( savedQuery, onSavedQuery, dataTestSubj, + displayStyle, }) => { const onQuerySubmit = useCallback( (payload: { dateRange: TimeRange; query?: Query }) => { @@ -102,12 +104,11 @@ export const QueryBar = memo( [filterManager] ); - const CustomButton = <>{null}; const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); return ( ( timeHistory={new TimeHistory(new Storage(localStorage))} dataTestSubj={dataTestSubj} savedQuery={savedQuery} + displayStyle={displayStyle} /> ); } diff --git a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx index d1d5c2fcebc124..d5e9fba36361ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx @@ -11,7 +11,6 @@ import React, { memo, useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps, useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import { Subscription } from 'rxjs'; -import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import type { DataViewBase, Filter, Query } from '@kbn/es-query'; @@ -53,12 +52,6 @@ interface SiemSearchBarProps { hideQueryInput?: boolean; } -const SearchBarContainer = styled.div` - .globalQueryBar { - padding: 0px; - } -`; - export const SearchBarComponent = memo( ({ end, @@ -322,7 +315,7 @@ export const SearchBarComponent = memo( const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); return ( - +
( showSaveQuery={true} dataTestSubj={dataTestSubj} /> - +
); }, (prevProps, nextProps) => diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx index 2ce403a832906b..5c8d2643eb445c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx @@ -46,23 +46,7 @@ interface QueryBarDefineRuleProps { const actionTimelineToHide: ActionTimelineToShow[] = ['duplicate', 'createFrom']; -const StyledEuiFormRow = styled(EuiFormRow)` - .kbnTypeahead__items { - max-height: 45vh !important; - } - .globalQueryBar { - padding: 4px 0px 0px 0px; - .kbnQueryBar { - & > div:first-child { - margin: 0px 0px 0px 4px; - } - &__wrap, - &__textarea { - z-index: 0; - } - } - } -`; +const StyledEuiFormRow = styled(EuiFormRow)``; // TODO need to add disabled in the SearchBar @@ -283,6 +267,7 @@ export const QueryBarDefineRule = ({ savedQuery={savedQuery} onSavedQuery={onSavedQuery} hideSavedQuery={false} + displayStyle="inPage" />
)} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx index 889c5005ec193d..24da8b3b86a35c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx @@ -8,7 +8,6 @@ import React, { memo, useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import { encode, RisonValue } from 'rison-node'; -import styled from 'styled-components'; import type { Query } from '@kbn/es-query'; import { TimeHistory } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; @@ -19,12 +18,6 @@ import { useEndpointSelector } from '../hooks'; import * as selectors from '../../store/selectors'; import { clone } from '../../models/index_pattern'; -const AdminQueryBar = styled.div` - .globalQueryBar { - padding: 0; - } -`; - export const AdminSearchBar = memo(() => { const history = useHistory(); const { admin_query: _, ...queryParams } = useEndpointSelector(selectors.uiQueryParams); @@ -57,7 +50,7 @@ export const AdminSearchBar = memo(() => { return (
{searchBarIndexPatterns && searchBarIndexPatterns.length > 0 && ( - +
{ showQueryBar={true} showQueryInput={true} /> - +
)}
); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index 6412567174c731..c62869c0f0746c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -275,6 +275,7 @@ export const QueryBarTimeline = memo( savedQuery={savedQuery} onSavedQuery={onSavedQuery} dataTestSubj={'timelineQueryInput'} + displayStyle="inPage" /> ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 17dd6491f23267..c5cc33c18c1c4f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -62,24 +62,13 @@ interface Props { const SearchOrFilterContainer = styled.div` ${({ theme }) => `margin-top: ${theme.eui.euiSizeXS};`} - user-select: none; - .globalQueryBar { - padding: 0px; - .kbnQueryBar { - div:first-child { - margin-right: 0px; - } - } - .globalFilterGroup__wrapper.globalFilterGroup__wrapper-isVisible { - height: auto !important; - } - } + user-select: none; // This should not be here, it makes the entire page inaccessible `; SearchOrFilterContainer.displayName = 'SearchOrFilterContainer'; const ModeFlexItem = styled(EuiFlexItem)` - user-select: none; + user-select: none; // Again, why? `; ModeFlexItem.displayName = 'ModeFlexItem'; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx index e753fee71d44b6..6747c60bb840cc 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { getDisplayValueFromFilter } from '@kbn/data-plugin/public'; @@ -30,7 +31,12 @@ export const ReadOnlyFilterItems = ({ filters, indexPatterns }: ReadOnlyFilterIt const filterList = filters.map((filter, index) => { const filterValue = getDisplayValueFromFilter(filter, indexPatterns); return ( - + lns-empty-dimension', diff --git a/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_panel_action.ts index c302d9a195397a..1dafddbb8567b8 100644 --- a/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_panel_action.ts +++ b/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_panel_action.ts @@ -67,7 +67,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); await discover.waitForDiscoverAppOnScreen(); - const el = await testSubjects.find('indexPattern-switch-link'); + const el = await testSubjects.find('discover-dataView-switch-link'); const text = await el.getVisibleText(); expect(text).to.be('logstash-*'); diff --git a/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts index 47a895472d9925..6b08a9455b644a 100644 --- a/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts @@ -228,6 +228,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { true, false ); + const contextMenuPanelTitleButton = await testSubjects.exists( + 'contextMenuPanelTitleButton' + ); + if (contextMenuPanelTitleButton) { + await testSubjects.click('contextMenuPanelTitleButton'); + } await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); const queryString = await queryBar.getQueryString(); @@ -244,6 +250,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('allow saving currently loaded query as a copy', async () => { await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); + await queryBar.setQuery('response:404'); await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( 'ok2', 'description', diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 0a12de3fb44d64..1f4cfa15fa892e 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -20,6 +20,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'security', 'share', 'spaceSelector', + 'header', ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -152,13 +153,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('allow saving currently loaded query as a copy', async () => { await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); + await queryBar.setQuery('response:404'); await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( 'ok2', 'description', true, false ); + await PageObjects.header.waitUntilLoadingHasFinished(); await savedQueryManagementComponent.savedQueryExistOrFail('ok2'); + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); + await testSubjects.click('showQueryBarMenu'); await savedQueryManagementComponent.deleteSavedQuery('ok2'); }); }); diff --git a/x-pack/test/functional/apps/lens/group2/dashboard.ts b/x-pack/test/functional/apps/lens/group2/dashboard.ts index 9a8cc99b243150..787a0a6a6d99ab 100644 --- a/x-pack/test/functional/apps/lens/group2/dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/dashboard.ts @@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'timePicker', 'lens', 'discover', + 'unifiedSearch', ]); const find = getService('find'); @@ -163,6 +164,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickNewDashboard(); await dashboardAddPanel.clickCreateNewLink(); await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); await PageObjects.lens.goToTimeRange(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', diff --git a/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts index 910a4a68806441..bd8f02c723102e 100644 --- a/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts @@ -87,7 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.enableFilter(); // turn off the KQL switch to change the language to lucene await testSubjects.click('indexPattern-filter-by-input > switchQueryLanguageButton'); - await testSubjects.click('languageToggle'); + await testSubjects.click('luceneLanguageMenuItem'); await testSubjects.click('indexPattern-filter-by-input > switchQueryLanguageButton'); // apparently setting a filter requires some time before and after typing to work properly await PageObjects.common.sleep(1000); diff --git a/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts index 9446b28c1e3ca8..92e9b6fcdb58ed 100644 --- a/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts @@ -23,6 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const filterBarService = getService('filterBar'); const queryBar = getService('queryBar'); + const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const browser = getService('browser'); const retry = getService('retry'); @@ -58,8 +59,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should bring both dashboard context and visualization context to discover', async () => { await PageObjects.dashboard.switchToEditMode(); await dashboardPanelActions.clickEdit(); - + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.switchQueryLanguage('lucene'); + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); await queryBar.setQuery('host.keyword www.elastic.co'); await queryBar.submitQuery(); await filterBarService.addFilter('geo.src', 'is', 'AF'); @@ -67,8 +69,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.sleep(1000); await PageObjects.lens.saveAndReturn(); - + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.switchQueryLanguage('kql'); + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); await queryBar.setQuery('request.keyword : "/apm"'); await queryBar.submitQuery(); await filterBarService.addFilter( diff --git a/x-pack/test/functional/apps/lens/group3/disable_auto_apply.ts b/x-pack/test/functional/apps/lens/group3/disable_auto_apply.ts index e52b1cccda8a39..4ccc642dd9929e 100644 --- a/x-pack/test/functional/apps/lens/group3/disable_auto_apply.ts +++ b/x-pack/test/functional/apps/lens/group3/disable_auto_apply.ts @@ -50,6 +50,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.goToTimeRange(); await PageObjects.lens.disableAutoApply(); + await PageObjects.lens.closeSettingsMenu(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', diff --git a/x-pack/test/functional/apps/lens/group3/lens_tagging.ts b/x-pack/test/functional/apps/lens/group3/lens_tagging.ts index d69b49403fc315..b246f84bb43ce6 100644 --- a/x-pack/test/functional/apps/lens/group3/lens_tagging.ts +++ b/x-pack/test/functional/apps/lens/group3/lens_tagging.ts @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visualize', 'lens', 'timePicker', + 'unifiedSearch', ]); const lensTag = 'extreme-lens-tag'; @@ -36,6 +37,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); }); after(async () => { diff --git a/x-pack/test/functional/apps/maps/group1/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/group1/feature_controls/maps_security.ts index ad4a2acd475a0c..94f46763acd315 100644 --- a/x-pack/test/functional/apps/maps/group1/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/group1/feature_controls/maps_security.ts @@ -113,18 +113,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { false ); }); - - it('allow saving currently loaded query as a copy', async () => { - await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); - await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( - 'ok2', - 'description', - true, - false - ); - await savedQueryManagementComponent.savedQueryExistOrFail('ok2'); - await savedQueryManagementComponent.deleteSavedQuery('ok2'); - }); }); describe('global maps read-only privileges', () => { diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index 85b9a31e2d3619..0bf6f6fad2a754 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -138,10 +138,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('allows saving via the saved query management component popover with no saved query loaded', async () => { await queryBar.setQuery('response:200'); + await queryBar.clickQuerySubmitButton(); + await testSubjects.click('showQueryBarMenu'); await savedQueryManagementComponent.saveNewQuery('foo', 'bar', true, false); + await PageObjects.header.waitUntilLoadingHasFinished(); await savedQueryManagementComponent.savedQueryExistOrFail('foo'); await savedQueryManagementComponent.closeSavedQueryManagementComponent(); - + await testSubjects.click('showQueryBarMenu'); await savedQueryManagementComponent.deleteSavedQuery('foo'); await savedQueryManagementComponent.savedQueryMissingOrFail('foo'); }); @@ -170,13 +173,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('allow saving currently loaded query as a copy', async () => { await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); + await queryBar.setQuery('response:404'); await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( 'ok2', 'description', true, false ); + await PageObjects.header.waitUntilLoadingHasFinished(); await savedQueryManagementComponent.savedQueryExistOrFail('ok2'); + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); + await testSubjects.click('showQueryBarMenu'); await savedQueryManagementComponent.deleteSavedQuery('ok2'); }); }); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index d38264150cfa57..7432c5e066a3d7 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -28,6 +28,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont 'visualize', 'dashboard', 'timeToVisualize', + 'unifiedSearch', ]); return logWrapper('lensPage', log, { @@ -56,6 +57,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont fromTime = fromTime || PageObjects.timePicker.defaultStartTime; toTime = toTime || PageObjects.timePicker.defaultEndTime; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + // give some time for the update button tooltip to close + await PageObjects.common.sleep(500); }, /** @@ -96,6 +99,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont return retry.try(async () => { await testSubjects.click(`visListingTitleLink-${title}`); await this.isLensPageOrFail(); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); }); }, @@ -566,10 +570,11 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont // pressing Enter at this point may lead to auto-complete the queryInput with random stuff from the // dropdown which was not intended originally. // To close the Filter popover we need to move to the label input and then press Enter: - // solution is to press Tab 2 twice (first Tab will close the dropdown) instead of Enter to avoid + // solution is to press Tab 3 tims (first Tab will close the dropdown) instead of Enter to avoid // race condition with the dropdown await PageObjects.common.pressTabKey(); await PageObjects.common.pressTabKey(); + await PageObjects.common.pressTabKey(); // Now it is safe to press Enter as we're in the label input await PageObjects.common.pressEnterKey(); await PageObjects.common.sleep(1000); // give time for debounced components to rerender @@ -837,7 +842,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * Changes the index pattern in the data panel */ async switchDataPanelIndexPattern(name: string) { - await testSubjects.click('indexPattern-switch-link'); + await testSubjects.click('lns-dataView-switch-link'); await find.clickByCssSelector(`[title="${name}"]`); await PageObjects.header.waitUntilLoadingHasFinished(); }, @@ -855,7 +860,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * Returns the current index pattern of the data panel */ async getDataPanelIndexPattern() { - return await (await testSubjects.find('indexPattern-switch-link')).getAttribute('title'); + return await (await testSubjects.find('lns-dataView-switch-link')).getAttribute('title'); }, /** @@ -1128,6 +1133,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await PageObjects.dashboard.switchToEditMode(); } await dashboardAddPanel.clickCreateNewLink(); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); await this.goToTimeRange(); await this.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', @@ -1200,7 +1206,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async clickAddField() { - await testSubjects.click('lnsIndexPatternActions'); + await testSubjects.click('lns-dataView-switch-link'); await testSubjects.existOrFail('indexPattern-add-field'); await testSubjects.click('indexPattern-add-field'); }, @@ -1371,9 +1377,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async closeSettingsMenu() { - if (!(await this.settingsMenuOpen())) return; - - await testSubjects.click('lnsApp_settingsButton'); + if (await this.settingsMenuOpen()) { + await testSubjects.click('lnsApp_settingsButton'); + } }, async enableAutoApply() { diff --git a/x-pack/test/functional/services/ml/dashboard_embeddables.ts b/x-pack/test/functional/services/ml/dashboard_embeddables.ts index 1eb4e25a4edd57..74268f74d19a20 100644 --- a/x-pack/test/functional/services/ml/dashboard_embeddables.ts +++ b/x-pack/test/functional/services/ml/dashboard_embeddables.ts @@ -117,7 +117,9 @@ export function MachineLearningDashboardEmbeddablesProvider( async selectDiscoverIndexPattern(indexPattern: string) { await retry.tryForTime(2 * 1000, async () => { await PageObjects.discover.selectIndexPattern(indexPattern); - const indexPatternTitle = await testSubjects.getVisibleText('indexPattern-switch-link'); + const indexPatternTitle = await testSubjects.getVisibleText( + 'discover-dataView-switch-link' + ); expect(indexPatternTitle).to.be(indexPattern); }); }, diff --git a/x-pack/test/functional/services/transform/discover.ts b/x-pack/test/functional/services/transform/discover.ts index a98f7e5ae98905..d96ab079043a0a 100644 --- a/x-pack/test/functional/services/transform/discover.ts +++ b/x-pack/test/functional/services/transform/discover.ts @@ -28,7 +28,7 @@ export function TransformDiscoverProvider({ getService }: FtrProviderContext) { async assertNoResults(expectedDestinationIndex: string) { // Discover should use the destination index pattern const actualIndexPatternSwitchLinkText = await ( - await testSubjects.find('indexPattern-switch-link') + await testSubjects.find('discover-dataView-switch-link') ).getVisibleText(); expect(actualIndexPatternSwitchLinkText).to.eql( expectedDestinationIndex, diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index 3c75db1c4c3666..bc281204008958 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -25,7 +25,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi const comboBox = getService('comboBox'); const retry = getService('retry'); const ml = getService('ml'); - const PageObjects = getPageObjects(['discover', 'timePicker']); + const PageObjects = getPageObjects(['discover', 'timePicker', 'unifiedSearch']); return { async clickNextButton() { @@ -911,6 +911,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi await testSubjects.click('transformWizardCardDiscover'); await PageObjects.discover.isDiscoverAppOnScreen(); }); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); }, async setDiscoverTimeRange(fromTime: string, toTime: string) { From 2c091706c051edc8d4b83c41576a62d76ef8468c Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Fri, 6 May 2022 12:15:37 +0500 Subject: [PATCH 45/83] [Discover] Migrate es query legacy rule params (#129179) * [Alerting] migrate es query legacy rule params * [Discover] add closing bracket * [Discover] apply suggestions * [Discover] resolve comments * [Discover] change version * [Discover] apply suggestions * [Discover] apply suggestions --- .../data/common/search/search_source/types.ts | 10 + .../saved_objects/search_migrations.test.ts | 24 ++ .../server/saved_objects/search_migrations.ts | 40 +- ...ualization_saved_object_migrations.test.ts | 25 ++ .../visualization_saved_object_migrations.ts | 46 ++- x-pack/plugins/alerting/server/plugin.test.ts | 5 + x-pack/plugins/alerting/server/plugin.ts | 8 +- .../alerting/server/saved_objects/index.ts | 6 +- .../server/saved_objects/migrations.test.ts | 366 ++++++++++++++---- .../server/saved_objects/migrations.ts | 112 +++++- .../spaces_only/tests/alerting/migrations.ts | 12 + .../functional/es_archives/alerts/data.json | 53 ++- 12 files changed, 572 insertions(+), 135 deletions(-) diff --git a/src/plugins/data/common/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts index 9ac9c4a057ee99..a3cd83f6ba67a2 100644 --- a/src/plugins/data/common/search/search_source/types.ts +++ b/src/plugins/data/common/search/search_source/types.ts @@ -217,3 +217,13 @@ export interface ShardFailure { }; shard: number; } + +export function isSerializedSearchSource( + maybeSerializedSearchSource: unknown +): maybeSerializedSearchSource is SerializedSearchSourceFields { + return ( + typeof maybeSerializedSearchSource === 'object' && + maybeSerializedSearchSource !== null && + !Array.isArray(maybeSerializedSearchSource) + ); +} diff --git a/src/plugins/discover/server/saved_objects/search_migrations.test.ts b/src/plugins/discover/server/saved_objects/search_migrations.test.ts index 9563bd6dc86c3b..fcce5d41fe90b0 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.test.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.test.ts @@ -350,6 +350,7 @@ Object { testMigrateMatchAllQuery(migrationFn); }); }); + it('should apply search source migrations within saved search', () => { const savedSearch = { attributes: { @@ -379,4 +380,27 @@ Object { }, }); }); + + it('should not apply search source migrations within saved search when searchSourceJSON is not an object', () => { + const savedSearch = { + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '5', + }, + }, + } as SavedObjectUnsanitizedDoc; + + const versionToTest = '9.1.2'; + const migrations = getAllMigrations({ + [versionToTest]: (state) => ({ ...state, migrated: true }), + }); + + expect(migrations[versionToTest](savedSearch, {} as SavedObjectMigrationContext)).toEqual({ + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '5', + }, + }, + }); + }); }); diff --git a/src/plugins/discover/server/saved_objects/search_migrations.ts b/src/plugins/discover/server/saved_objects/search_migrations.ts index 95da82fa38acfa..2fb49628f53bcc 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.ts @@ -17,7 +17,7 @@ import type { import { mergeSavedObjectMigrationMaps } from '@kbn/core/server'; import { DEFAULT_QUERY_LANGUAGE } from '@kbn/data-plugin/server'; import { MigrateFunctionsObject, MigrateFunction } from '@kbn/kibana-utils-plugin/common'; -import type { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; +import { isSerializedSearchSource, SerializedSearchSourceFields } from '@kbn/data-plugin/common'; export interface SavedSearchMigrationAttributes extends SavedObjectAttributes { kibanaSavedObjectMeta: { @@ -135,27 +135,31 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = (doc) = /** * This creates a migration map that applies search source migrations */ -const getSearchSourceMigrations = (searchSourceMigrations: MigrateFunctionsObject) => +const getSearchSourceMigrations = ( + searchSourceMigrations: MigrateFunctionsObject +): MigrateFunctionsObject => mapValues( searchSourceMigrations, (migrate: MigrateFunction): MigrateFunction => (state) => { - const _state = state as unknown as { attributes: SavedSearchMigrationAttributes }; - - const parsedSearchSourceJSON = _state.attributes.kibanaSavedObjectMeta.searchSourceJSON; - - if (!parsedSearchSourceJSON) return _state; - - return { - ..._state, - attributes: { - ..._state.attributes, - kibanaSavedObjectMeta: { - ..._state.attributes.kibanaSavedObjectMeta, - searchSourceJSON: JSON.stringify(migrate(JSON.parse(parsedSearchSourceJSON))), + const _state = state as { attributes: SavedSearchMigrationAttributes }; + + const parsedSearchSourceJSON = JSON.parse( + _state.attributes.kibanaSavedObjectMeta.searchSourceJSON + ); + if (isSerializedSearchSource(parsedSearchSourceJSON)) { + return { + ..._state, + attributes: { + ..._state.attributes, + kibanaSavedObjectMeta: { + ..._state.attributes.kibanaSavedObjectMeta, + searchSourceJSON: JSON.stringify(migrate(parsedSearchSourceJSON)), + }, }, - }, - }; + }; + } + return _state; } ); @@ -171,6 +175,6 @@ export const getAllMigrations = ( ): SavedObjectMigrationMap => { return mergeSavedObjectMigrationMaps( searchMigrations, - getSearchSourceMigrations(searchSourceMigrations) as unknown as SavedObjectMigrationMap + getSearchSourceMigrations(searchSourceMigrations) as SavedObjectMigrationMap ); }; diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts index 99dbf548e6f444..19f117ec18cc8f 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts @@ -2470,6 +2470,31 @@ describe('migration visualization', () => { }); }); + it('should not apply search source migrations within visualization when searchSourceJSON is not an object', () => { + const visualizationDoc = { + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '5', + }, + }, + } as SavedObjectUnsanitizedDoc; + + const versionToTest = '1.2.4'; + const visMigrations = getAllMigrations({ + [versionToTest]: (state) => ({ ...state, migrated: true }), + }); + + expect( + visMigrations[versionToTest](visualizationDoc, {} as SavedObjectMigrationContext) + ).toEqual({ + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '5', + }, + }, + }); + }); + describe('8.1.0 pie - labels and addLegend migration', () => { const getDoc = (addLegend: boolean, lastLevel: boolean = false) => ({ attributes: { diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts index 4b729afa62307c..d236ad83c853ac 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts @@ -11,7 +11,11 @@ import type { SavedObjectMigrationFn, SavedObjectMigrationMap } from '@kbn/core/ import { mergeSavedObjectMigrationMaps } from '@kbn/core/server'; import { MigrateFunctionsObject, MigrateFunction } from '@kbn/kibana-utils-plugin/common'; -import { DEFAULT_QUERY_LANGUAGE, SerializedSearchSourceFields } from '@kbn/data-plugin/common'; +import { + DEFAULT_QUERY_LANGUAGE, + isSerializedSearchSource, + SerializedSearchSourceFields, +} from '@kbn/data-plugin/common'; import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; import { commonAddSupportOfDualIndexSelectionModeInTSVB, @@ -1215,27 +1219,31 @@ const visualizationSavedObjectTypeMigrations = { /** * This creates a migration map that applies search source migrations to legacy visualization SOs */ -const getVisualizationSearchSourceMigrations = (searchSourceMigrations: MigrateFunctionsObject) => +const getVisualizationSearchSourceMigrations = ( + searchSourceMigrations: MigrateFunctionsObject +): MigrateFunctionsObject => mapValues( searchSourceMigrations, (migrate: MigrateFunction): MigrateFunction => (state) => { - const _state = state as unknown as { attributes: VisualizationSavedObjectAttributes }; - - const parsedSearchSourceJSON = _state.attributes.kibanaSavedObjectMeta.searchSourceJSON; - - if (!parsedSearchSourceJSON) return _state; - - return { - ..._state, - attributes: { - ..._state.attributes, - kibanaSavedObjectMeta: { - ..._state.attributes.kibanaSavedObjectMeta, - searchSourceJSON: JSON.stringify(migrate(JSON.parse(parsedSearchSourceJSON))), + const _state = state as { attributes: VisualizationSavedObjectAttributes }; + + const parsedSearchSourceJSON = JSON.parse( + _state.attributes.kibanaSavedObjectMeta.searchSourceJSON + ); + if (isSerializedSearchSource(parsedSearchSourceJSON)) { + return { + ..._state, + attributes: { + ..._state.attributes, + kibanaSavedObjectMeta: { + ..._state.attributes.kibanaSavedObjectMeta, + searchSourceJSON: JSON.stringify(migrate(parsedSearchSourceJSON)), + }, }, - }, - }; + }; + } + return _state; } ); @@ -1244,7 +1252,5 @@ export const getAllMigrations = ( ): SavedObjectMigrationMap => mergeSavedObjectMigrationMaps( visualizationSavedObjectTypeMigrations, - getVisualizationSearchSourceMigrations( - searchSourceMigrations - ) as unknown as SavedObjectMigrationMap + getVisualizationSearchSourceMigrations(searchSourceMigrations) as SavedObjectMigrationMap ); diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index b342eddaa0c1b6..5eba1353df2162 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -21,6 +21,7 @@ import { eventLogMock } from '@kbn/event-log-plugin/server/mocks'; import { actionsMock } from '@kbn/actions-plugin/server/mocks'; import { dataPluginMock } from '@kbn/data-plugin/server/mocks'; import { monitoringCollectionMock } from '@kbn/monitoring-collection-plugin/server/mocks'; +import { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server'; import { spacesMock } from '@kbn/spaces-plugin/server/mocks'; const generateAlertingConfig = (): AlertingConfig => ({ @@ -66,6 +67,7 @@ describe('Alerting Plugin', () => { actions: actionsMock.createSetup(), statusService: statusServiceMock.createSetupContract(), monitoringCollection: monitoringCollectionMock.createSetup(), + data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, }; let plugin: AlertingPlugin; @@ -207,6 +209,7 @@ describe('Alerting Plugin', () => { actions: actionsMock.createSetup(), statusService: statusServiceMock.createSetupContract(), monitoringCollection: monitoringCollectionMock.createSetup(), + data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, }); const startContract = plugin.start(coreMock.createStart(), { @@ -246,6 +249,7 @@ describe('Alerting Plugin', () => { actions: actionsMock.createSetup(), statusService: statusServiceMock.createSetupContract(), monitoringCollection: monitoringCollectionMock.createSetup(), + data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, }); const startContract = plugin.start(coreMock.createStart(), { @@ -296,6 +300,7 @@ describe('Alerting Plugin', () => { actions: actionsMock.createSetup(), statusService: statusServiceMock.createSetupContract(), monitoringCollection: monitoringCollectionMock.createSetup(), + data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, }); const startContract = plugin.start(coreMock.createStart(), { diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 6589b1537f766c..063c221ea98db5 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -10,6 +10,7 @@ import { BehaviorSubject } from 'rxjs'; import { pick } from 'lodash'; import { UsageCollectionSetup, UsageCounter } from '@kbn/usage-collection-plugin/server'; import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; +import { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server'; import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart, @@ -140,6 +141,7 @@ export interface AlertingPluginsSetup { eventLog: IEventLogService; statusService: StatusServiceSetup; monitoringCollection: MonitoringCollectionSetup; + data: DataPluginSetup; } export interface AlertingPluginsStart { @@ -247,12 +249,16 @@ export class AlertingPlugin { // Usage counter for telemetry this.usageCounter = plugins.usageCollection?.createUsageCounter(ALERTS_FEATURE_ID); + const getSearchSourceMigrations = plugins.data.search.searchSource.getAllMigrations.bind( + plugins.data.search.searchSource + ); setupSavedObjects( core.savedObjects, plugins.encryptedSavedObjects, this.ruleTypeRegistry, this.logger, - plugins.actions.isPreconfiguredConnector + plugins.actions.isPreconfiguredConnector, + getSearchSourceMigrations ); initializeApiKeyInvalidator( diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index 85e4dc5a8e05bb..6566fee15d4a86 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -12,6 +12,7 @@ import type { SavedObjectsServiceSetup, } from '@kbn/core/server'; import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; +import { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common'; import { alertMappings } from './mappings'; import { getMigrations } from './migrations'; import { transformRulesForExport } from './transform_rule_for_export'; @@ -51,14 +52,15 @@ export function setupSavedObjects( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, ruleTypeRegistry: RuleTypeRegistry, logger: Logger, - isPreconfigured: (connectorId: string) => boolean + isPreconfigured: (connectorId: string) => boolean, + getSearchSourceMigrations: () => MigrateFunctionsObject ) { savedObjects.registerType({ name: 'alert', hidden: true, namespaceType: 'multiple-isolated', convertToMultiNamespaceTypeVersion: '8.0.0', - migrations: getMigrations(encryptedSavedObjects, isPreconfigured), + migrations: getMigrations(encryptedSavedObjects, getSearchSourceMigrations(), isPreconfigured), mappings: alertMappings, management: { displayName: 'rule', diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index 921412d4e79e85..c83d0a95dfdcb8 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -8,7 +8,7 @@ import uuid from 'uuid'; import { getMigrations, isAnyActionSupportIncidents } from './migrations'; import { RawRule } from '../types'; -import { SavedObjectUnsanitizedDoc } from '@kbn/core/server'; +import { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from '@kbn/core/server'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { migrationMocks } from '@kbn/core/server/mocks'; import { RuleType, ruleTypeMappings } from '@kbn/securitysolution-rules'; @@ -25,7 +25,7 @@ describe('successful migrations', () => { }); describe('7.10.0', () => { test('marks alerts as legacy', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({}); expect(migration710(alert, migrationContext)).toMatchObject({ ...alert, @@ -39,7 +39,7 @@ describe('successful migrations', () => { }); test('migrates the consumer for metrics', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ consumer: 'metrics', }); @@ -56,7 +56,7 @@ describe('successful migrations', () => { }); test('migrates the consumer for siem', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ consumer: 'securitySolution', }); @@ -73,7 +73,7 @@ describe('successful migrations', () => { }); test('migrates the consumer for alerting', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ consumer: 'alerting', }); @@ -90,7 +90,7 @@ describe('successful migrations', () => { }); test('migrates PagerDuty actions to set a default dedupkey of the AlertId', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ actions: [ { @@ -127,7 +127,7 @@ describe('successful migrations', () => { }); test('skips PagerDuty actions with a specified dedupkey', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ actions: [ { @@ -165,7 +165,7 @@ describe('successful migrations', () => { }); test('skips PagerDuty actions with an eventAction of "trigger"', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ actions: [ { @@ -204,7 +204,7 @@ describe('successful migrations', () => { }); test('creates execution status', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData(); const dateStart = Date.now(); const migratedAlert = migration710(alert, migrationContext); @@ -232,7 +232,7 @@ describe('successful migrations', () => { describe('7.11.0', () => { test('add updatedAt field to alert - set to SavedObject updated_at attribute', () => { - const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0']; + const migration711 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.11.0']; const alert = getMockData({}, true); expect(migration711(alert, migrationContext)).toEqual({ ...alert, @@ -245,7 +245,7 @@ describe('successful migrations', () => { }); test('add updatedAt field to alert - set to createdAt when SavedObject updated_at is not defined', () => { - const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0']; + const migration711 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.11.0']; const alert = getMockData({}); expect(migration711(alert, migrationContext)).toEqual({ ...alert, @@ -258,7 +258,7 @@ describe('successful migrations', () => { }); test('add notifyWhen=onActiveAlert when throttle is null', () => { - const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0']; + const migration711 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.11.0']; const alert = getMockData({}); expect(migration711(alert, migrationContext)).toEqual({ ...alert, @@ -271,7 +271,7 @@ describe('successful migrations', () => { }); test('add notifyWhen=onActiveAlert when throttle is set', () => { - const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0']; + const migration711 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.11.0']; const alert = getMockData({ throttle: '5m' }); expect(migration711(alert, migrationContext)).toEqual({ ...alert, @@ -286,7 +286,9 @@ describe('successful migrations', () => { describe('7.11.2', () => { test('transforms connectors that support incident correctly', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ actions: [ { @@ -428,7 +430,9 @@ describe('successful migrations', () => { }); test('it transforms only subAction=pushToService', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ actions: [ { @@ -447,7 +451,9 @@ describe('successful migrations', () => { }); test('it does not transforms other connectors', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ actions: [ { @@ -526,7 +532,9 @@ describe('successful migrations', () => { }); test('it does not transforms alerts when the right structure connectors is already applied', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ actions: [ { @@ -563,7 +571,9 @@ describe('successful migrations', () => { }); test('if incident attribute is an empty object, copy back the related attributes from subActionParams back to incident', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ actions: [ { @@ -625,7 +635,9 @@ describe('successful migrations', () => { }); test('custom action does not get migrated/loss', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ actions: [ { @@ -654,7 +666,7 @@ describe('successful migrations', () => { describe('7.13.0', () => { test('security solution alerts get migrated and remove null values', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -748,7 +760,7 @@ describe('successful migrations', () => { }); test('non-null values in security solution alerts are not modified', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -815,7 +827,7 @@ describe('successful migrations', () => { }); test('security solution threshold alert with string in threshold.field is migrated to array', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -846,7 +858,7 @@ describe('successful migrations', () => { }); test('security solution threshold alert with empty string in threshold.field is migrated to empty array', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -877,7 +889,7 @@ describe('successful migrations', () => { }); test('security solution threshold alert with array in threshold.field and cardinality is left alone', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -919,7 +931,7 @@ describe('successful migrations', () => { }); test('security solution ML alert with string in machineLearningJobId is converted to an array', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -945,7 +957,7 @@ describe('successful migrations', () => { }); test('security solution ML alert with an array in machineLearningJobId is preserved', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -973,7 +985,9 @@ describe('successful migrations', () => { describe('7.14.1', () => { test('security solution author field is migrated to array if it is undefined', () => { - const migration7141 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.14.1']; + const migration7141 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.14.1' + ]; const alert = getMockData({ alertTypeId: 'siem.signals', params: {}, @@ -991,7 +1005,9 @@ describe('successful migrations', () => { }); test('security solution author field does not override existing values if they exist', () => { - const migration7141 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.14.1']; + const migration7141 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.14.1' + ]; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -1015,7 +1031,9 @@ describe('successful migrations', () => { describe('7.15.0', () => { test('security solution is migrated to saved object references if it has 1 exceptionsList', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -1044,7 +1062,9 @@ describe('successful migrations', () => { }); test('security solution is migrated to saved object references if it has 2 exceptionsLists', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -1084,7 +1104,9 @@ describe('successful migrations', () => { }); test('security solution is migrated to saved object references if it has 3 exceptionsLists', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -1135,7 +1157,9 @@ describe('successful migrations', () => { }); test('security solution does not change anything if exceptionsList is missing', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -1147,7 +1171,9 @@ describe('successful migrations', () => { }); test('security solution will keep existing references if we do not have an exceptionsList but we do already have references', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1177,7 +1203,9 @@ describe('successful migrations', () => { }); test('security solution keep any foreign references if they exist but still migrate other references', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1242,7 +1270,9 @@ describe('successful migrations', () => { }); test('security solution is idempotent and if re-run on the same migrated data will keep the same items', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1282,7 +1312,9 @@ describe('successful migrations', () => { }); test('security solution will migrate with only missing data if we have partially migrated data', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1331,7 +1363,9 @@ describe('successful migrations', () => { }); test('security solution will not migrate if exception list if it is invalid data', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1345,7 +1379,9 @@ describe('successful migrations', () => { }); test('security solution will migrate valid data if it is mixed with invalid data', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1387,7 +1423,9 @@ describe('successful migrations', () => { }); test('security solution will not migrate if exception list is invalid data but will keep existing references', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1419,7 +1457,7 @@ describe('successful migrations', () => { describe('7.16.0', () => { test('add legacyId field to alert - set to SavedObject id attribute', () => { - const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration716 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.16.0']; const alert = getMockData({}, true); expect(migration716(alert, migrationContext)).toEqual({ ...alert, @@ -1434,7 +1472,7 @@ describe('successful migrations', () => { isPreconfigured.mockReset(); isPreconfigured.mockReturnValueOnce(true); isPreconfigured.mockReturnValueOnce(false); - const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration716 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.16.0']; const rule = { ...getMockData({ actions: [ @@ -1510,7 +1548,7 @@ describe('successful migrations', () => { isPreconfigured.mockReturnValueOnce(true); isPreconfigured.mockReturnValueOnce(false); isPreconfigured.mockReturnValueOnce(false); - const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration716 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.16.0']; const rule = { ...getMockData({ actions: [ @@ -1593,7 +1631,7 @@ describe('successful migrations', () => { test('does nothing to rules with no references', () => { isPreconfigured.mockReset(); - const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration716 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.16.0']; const rule = { ...getMockData({ actions: [ @@ -1629,7 +1667,7 @@ describe('successful migrations', () => { test('does nothing to rules with no action references', () => { isPreconfigured.mockReset(); - const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration716 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.16.0']; const rule = { ...getMockData({ actions: [ @@ -1671,7 +1709,7 @@ describe('successful migrations', () => { test('does nothing to rules with references but no actions', () => { isPreconfigured.mockReset(); - const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration716 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.16.0']; const rule = { ...getMockData({ actions: [], @@ -1699,7 +1737,9 @@ describe('successful migrations', () => { }); test('security solution is migrated to saved object references if it has a "ruleAlertId"', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = getMockData({ alertTypeId: 'siem.notifications', params: { @@ -1724,7 +1764,9 @@ describe('successful migrations', () => { }); test('security solution does not migrate anything if its type is not siem.notifications', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = getMockData({ alertTypeId: 'other-type', params: { @@ -1741,7 +1783,9 @@ describe('successful migrations', () => { }); }); test('security solution does not change anything if "ruleAlertId" is missing', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = getMockData({ alertTypeId: 'siem.notifications', params: {}, @@ -1757,7 +1801,9 @@ describe('successful migrations', () => { }); test('security solution will keep existing references if we do not have a "ruleAlertId" but we do already have references', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.notifications', @@ -1789,7 +1835,9 @@ describe('successful migrations', () => { }); test('security solution will keep any foreign references if they exist but still migrate other "ruleAlertId" references', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.notifications', @@ -1828,7 +1876,9 @@ describe('successful migrations', () => { }); test('security solution is idempotent and if re-run on the same migrated data will keep the same items "ruleAlertId" references', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.notifications', @@ -1862,7 +1912,9 @@ describe('successful migrations', () => { }); test('security solution will not migrate "ruleAlertId" if it is invalid data', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.notifications', @@ -1882,7 +1934,9 @@ describe('successful migrations', () => { }); test('security solution will not migrate "ruleAlertId" if it is invalid data but will keep existing references', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.notifications', @@ -1916,7 +1970,9 @@ describe('successful migrations', () => { }); test('geo-containment alert migration extracts boundary and index references', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: '.geo-containment', @@ -1944,7 +2000,9 @@ describe('successful migrations', () => { }); test('geo-containment alert migration should preserve foreign references', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: '.geo-containment', @@ -1984,7 +2042,9 @@ describe('successful migrations', () => { }); test('geo-containment alert migration ignores other alert-types', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: '.foo', @@ -2008,13 +2068,13 @@ describe('successful migrations', () => { describe('8.0.0', () => { test('no op migration for rules SO', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['8.0.0']; const alert = getMockData({}, true); expect(migration800(alert, migrationContext)).toEqual(alert); }); test('add threatIndicatorPath default value to threat match rules if missing', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['8.0.0']; const alert = getMockData( { params: { type: 'threat_match' }, alertTypeId: 'siem.signals' }, true @@ -2025,7 +2085,7 @@ describe('successful migrations', () => { }); test('doesnt change threatIndicatorPath value in threat match rules if value is present', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['8.0.0']; const alert = getMockData( { params: { type: 'threat_match', threatIndicatorPath: 'custom.indicator.path' }, @@ -2039,7 +2099,7 @@ describe('successful migrations', () => { }); test('doesnt change threatIndicatorPath value in other rules', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['8.0.0']; const alert = getMockData({ params: { type: 'eql' }, alertTypeId: 'siem.signals' }, true); expect(migration800(alert, migrationContext).attributes.params.threatIndicatorPath).toEqual( undefined @@ -2047,7 +2107,7 @@ describe('successful migrations', () => { }); test('doesnt change threatIndicatorPath value if not a siem.signals rule', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['8.0.0']; const alert = getMockData( { params: { type: 'threat_match' }, alertTypeId: 'not.siem.signals' }, true @@ -2058,7 +2118,7 @@ describe('successful migrations', () => { }); test('doesnt change AAD rule params if not a siem.signals rule', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['8.0.0']; const alert = getMockData( { params: { outputIndex: 'output-index', type: 'query' }, alertTypeId: 'not.siem.signals' }, true @@ -2073,7 +2133,9 @@ describe('successful migrations', () => { test.each(Object.keys(ruleTypeMappings) as RuleType[])( 'changes AAD rule params accordingly if rule is a siem.signals %p rule', (ruleType) => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.0.0' + ]; const alert = getMockData( { params: { outputIndex: 'output-index', type: ruleType }, alertTypeId: 'siem.signals' }, true @@ -2118,7 +2180,7 @@ describe('successful migrations', () => { ); test('Does not update rule tags if rule has already been enabled', () => { - const migrations = getMigrations(encryptedSavedObjectsSetup, isPreconfigured); + const migrations = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured); const migration800 = migrations['8.0.0']; const migration801 = migrations['8.0.1']; @@ -2141,7 +2203,7 @@ describe('successful migrations', () => { }); test('Does not update rule tags if rule was already disabled before upgrading to 8.0', () => { - const migrations = getMigrations(encryptedSavedObjectsSetup, isPreconfigured); + const migrations = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured); const migration800 = migrations['8.0.0']; const migration801 = migrations['8.0.1']; @@ -2161,7 +2223,7 @@ describe('successful migrations', () => { }); test('Updates rule tags if rule was auto-disabled in 8.0 upgrade and not reenabled', () => { - const migrations = getMigrations(encryptedSavedObjectsSetup, isPreconfigured); + const migrations = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured); const migration800 = migrations['8.0.0']; const migration801 = migrations['8.0.1']; @@ -2181,7 +2243,7 @@ describe('successful migrations', () => { }); test('Updates rule tags correctly if tags are undefined', () => { - const migrations = getMigrations(encryptedSavedObjectsSetup, isPreconfigured); + const migrations = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured); const migration801 = migrations['8.0.1']; const alert = { @@ -2204,7 +2266,7 @@ describe('successful migrations', () => { }); test('Updates rule tags correctly if tags are null', () => { - const migrations = getMigrations(encryptedSavedObjectsSetup, isPreconfigured); + const migrations = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured); const migration801 = migrations['8.0.1']; const alert = { @@ -2231,7 +2293,9 @@ describe('successful migrations', () => { describe('8.2.0', () => { test('migrates params to mapped_params', () => { - const migration820 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.2.0']; + const migration820 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.2.0' + ]; const alert = getMockData( { params: { @@ -2254,8 +2318,29 @@ describe('successful migrations', () => { }); describe('8.3.0', () => { + test('migrates es_query alert params', () => { + const migration830 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.3.0' + ]; + const alert = getMockData( + { + params: { esQuery: '{ "query": "test-query" }' }, + alertTypeId: '.es-query', + }, + true + ); + const migratedAlert820 = migration830(alert, migrationContext); + + expect(migratedAlert820.attributes.params).toEqual({ + esQuery: '{ "query": "test-query" }', + searchType: 'esQuery', + }); + }); + test('removes internal tags', () => { - const migration830 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.3.0']; + const migration830 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.3.0' + ]; const alert = getMockData( { tags: [ @@ -2274,7 +2359,9 @@ describe('successful migrations', () => { }); test('do not remove internal tags if rule is not Security solution rule', () => { - const migration830 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.3.0']; + const migration830 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.3.0' + ]; const alert = getMockData( { tags: ['__internal_immutable:false', 'tag-1'], @@ -2290,7 +2377,9 @@ describe('successful migrations', () => { describe('Metrics Inventory Threshold rule', () => { test('Migrates incorrect action group spelling', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.0.0' + ]; const actions = [ { @@ -2317,7 +2406,9 @@ describe('successful migrations', () => { }); test('Works with the correct action group spelling', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.0.0' + ]; const actions = [ { @@ -2346,6 +2437,72 @@ describe('successful migrations', () => { }); }); +describe('search source migration', () => { + it('should apply migration within es query alert rule', () => { + const esQueryRuleSavedObject = { + attributes: { + params: { + searchConfiguration: { + some: 'prop', + migrated: false, + }, + }, + }, + } as SavedObjectUnsanitizedDoc; + + const versionToTest = '9.1.3'; + const migrations = getMigrations( + encryptedSavedObjectsSetup, + { + [versionToTest]: (state) => ({ ...state, migrated: true }), + }, + isPreconfigured + ); + + expect( + migrations[versionToTest](esQueryRuleSavedObject, {} as SavedObjectMigrationContext) + ).toEqual({ + attributes: { + params: { + searchConfiguration: { + some: 'prop', + migrated: true, + }, + }, + }, + }); + }); + + it('should not apply migration within es query alert rule when searchConfiguration not an object', () => { + const esQueryRuleSavedObject = { + attributes: { + params: { + searchConfiguration: 5, + }, + }, + } as SavedObjectUnsanitizedDoc; + + const versionToTest = '9.1.4'; + const migrations = getMigrations( + encryptedSavedObjectsSetup, + { + [versionToTest]: (state) => ({ ...state, migrated: true }), + }, + isPreconfigured + ); + + expect( + migrations[versionToTest](esQueryRuleSavedObject, {} as SavedObjectMigrationContext) + ).toEqual({ + attributes: { + params: { + searchConfiguration: 5, + }, + }, + }); + }); +}); + describe('handles errors during migrations', () => { beforeEach(() => { jest.resetAllMocks(); @@ -2355,7 +2512,7 @@ describe('handles errors during migrations', () => { }); describe('7.10.0 throws if migration fails', () => { test('should show the proper exception', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ consumer: 'alerting', }); @@ -2380,7 +2537,7 @@ describe('handles errors during migrations', () => { describe('7.11.0 throws if migration fails', () => { test('should show the proper exception', () => { - const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0']; + const migration711 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.11.0']; const alert = getMockData({ consumer: 'alerting', }); @@ -2405,7 +2562,9 @@ describe('handles errors during migrations', () => { describe('7.11.2 throws if migration fails', () => { test('should show the proper exception', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ consumer: 'alerting', }); @@ -2430,7 +2589,9 @@ describe('handles errors during migrations', () => { describe('7.13.0 throws if migration fails', () => { test('should show the proper exception', () => { - const migration7130 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration7130 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.13.0' + ]; const alert = getMockData({ consumer: 'alerting', }); @@ -2455,7 +2616,9 @@ describe('handles errors during migrations', () => { describe('7.16.0 throws if migration fails', () => { test('should show the proper exception', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const rule = getMockData(); expect(() => { migration7160(rule, migrationContext); @@ -2475,6 +2638,53 @@ describe('handles errors during migrations', () => { ); }); }); + + describe('8.3.0 throws if migration fails', () => { + test('should show the proper exception on search source migration', () => { + encryptedSavedObjectsSetup.createMigration.mockImplementation(({ migration }) => migration); + const mockRule = getMockData(); + const rule = { + ...mockRule, + attributes: { + ...mockRule.attributes, + params: { + searchConfiguration: { + some: 'prop', + migrated: false, + }, + }, + }, + }; + + const versionToTest = '8.3.0'; + const migration830 = getMigrations( + encryptedSavedObjectsSetup, + { + [versionToTest]: () => { + throw new Error(`Can't migrate search source!`); + }, + }, + isPreconfigured + )[versionToTest]; + + expect(() => { + migration830(rule, migrationContext); + }).toThrowError(`Can't migrate search source!`); + expect(migrationContext.log.error).toHaveBeenCalledWith( + `encryptedSavedObject ${versionToTest} migration failed for alert ${rule.id} with error: Can't migrate search source!`, + { + migrations: { + alertDocument: { + ...rule, + attributes: { + ...rule.attributes, + }, + }, + }, + } + ); + }); + }); }); function getUpdatedAt(): string { diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index 69d88e196dcfda..b3f8d873d8ef03 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -7,6 +7,7 @@ import { isRuleType, ruleTypeMappings } from '@kbn/securitysolution-rules'; import { isString } from 'lodash/fp'; +import { gte } from 'semver'; import { LogMeta, SavedObjectMigrationMap, @@ -19,12 +20,16 @@ import { } from '@kbn/core/server'; import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; import type { IsMigrationNeededPredicate } from '@kbn/encrypted-saved-objects-plugin/server'; -import { RawRule, RawRuleAction, RawRuleExecutionStatus } from '../types'; +import { MigrateFunctionsObject, MigrateFunction } from '@kbn/kibana-utils-plugin/common'; +import { mergeSavedObjectMigrationMaps } from '@kbn/core/server'; +import { isSerializedSearchSource, SerializedSearchSourceFields } from '@kbn/data-plugin/common'; import { extractRefsFromGeoContainmentAlert } from './geo_containment/migrations'; +import { RawRule, RawRuleAction, RawRuleExecutionStatus } from '../types'; import { getMappedParams } from '../rules_client/lib/mapped_params_utils'; const SIEM_APP_ID = 'securitySolution'; const SIEM_SERVER_APP_ID = 'siem'; +const MINIMUM_SS_MIGRATION_VERSION = '8.3.0'; export const LEGACY_LAST_MODIFIED_VERSION = 'pre-7.10.0'; export const FILEBEAT_7X_INDICATOR_PATH = 'threatintel.indicator'; @@ -59,6 +64,9 @@ export const isAnyActionSupportIncidents = (doc: SavedObjectUnsanitizedDoc): boolean => doc.attributes.alertTypeId === 'siem.signals'; +export const isEsQueryRuleType = (doc: SavedObjectUnsanitizedDoc) => + doc.attributes.alertTypeId === '.es-query'; + export const isDetectionEngineAADRuleType = (doc: SavedObjectUnsanitizedDoc): boolean => (Object.values(ruleTypeMappings) as string[]).includes(doc.attributes.alertTypeId); @@ -75,6 +83,7 @@ export const isSecuritySolutionLegacyNotification = ( export function getMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, + searchSourceMigrations: MigrateFunctionsObject, isPreconfigured: (connectorId: string) => boolean ): SavedObjectMigrationMap { const migrationWhenRBACWasIntroduced = createEsoMigration( @@ -155,22 +164,25 @@ export function getMigrations( const migrationRules830 = createEsoMigration( encryptedSavedObjects, (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => true, - pipeMigrations(removeInternalTags) + pipeMigrations(addSearchType, removeInternalTags) ); - return { - '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), - '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), - '7.11.2': executeMigrationWithErrorHandling(migrationActions7112, '7.11.2'), - '7.13.0': executeMigrationWithErrorHandling(migrationSecurityRules713, '7.13.0'), - '7.14.1': executeMigrationWithErrorHandling(migrationSecurityRules714, '7.14.1'), - '7.15.0': executeMigrationWithErrorHandling(migrationSecurityRules715, '7.15.0'), - '7.16.0': executeMigrationWithErrorHandling(migrateRules716, '7.16.0'), - '8.0.0': executeMigrationWithErrorHandling(migrationRules800, '8.0.0'), - '8.0.1': executeMigrationWithErrorHandling(migrationRules801, '8.0.1'), - '8.2.0': executeMigrationWithErrorHandling(migrationRules820, '8.2.0'), - '8.3.0': executeMigrationWithErrorHandling(migrationRules830, '8.3.0'), - }; + return mergeSavedObjectMigrationMaps( + { + '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), + '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), + '7.11.2': executeMigrationWithErrorHandling(migrationActions7112, '7.11.2'), + '7.13.0': executeMigrationWithErrorHandling(migrationSecurityRules713, '7.13.0'), + '7.14.1': executeMigrationWithErrorHandling(migrationSecurityRules714, '7.14.1'), + '7.15.0': executeMigrationWithErrorHandling(migrationSecurityRules715, '7.15.0'), + '7.16.0': executeMigrationWithErrorHandling(migrateRules716, '7.16.0'), + '8.0.0': executeMigrationWithErrorHandling(migrationRules800, '8.0.0'), + '8.0.1': executeMigrationWithErrorHandling(migrationRules801, '8.0.1'), + '8.2.0': executeMigrationWithErrorHandling(migrationRules820, '8.2.0'), + '8.3.0': executeMigrationWithErrorHandling(migrationRules830, '8.3.0'), + }, + getSearchSourceMigrations(encryptedSavedObjects, searchSourceMigrations) + ); } function executeMigrationWithErrorHandling( @@ -697,6 +709,23 @@ function addSecuritySolutionAADRuleTypes( : doc; } +function addSearchType(doc: SavedObjectUnsanitizedDoc) { + const searchType = doc.attributes.params.searchType; + + return isEsQueryRuleType(doc) && !searchType + ? { + ...doc, + attributes: { + ...doc.attributes, + params: { + ...doc.attributes.params, + searchType: 'esQuery', + }, + }, + } + : doc; +} + function addSecuritySolutionAADRuleTypeTags( doc: SavedObjectUnsanitizedDoc ): SavedObjectUnsanitizedDoc { @@ -902,3 +931,56 @@ function pipeMigrations(...migrations: AlertMigration[]): AlertMigration { return (doc: SavedObjectUnsanitizedDoc) => migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); } + +function mapSearchSourceMigrationFunc( + migrateSerializedSearchSourceFields: MigrateFunction +): MigrateFunction { + return (doc) => { + const _doc = doc as { attributes: RawRule }; + + const serializedSearchSource = _doc.attributes.params.searchConfiguration; + + if (isSerializedSearchSource(serializedSearchSource)) { + return { + ..._doc, + attributes: { + ..._doc.attributes, + params: { + ..._doc.attributes.params, + searchConfiguration: migrateSerializedSearchSourceFields(serializedSearchSource), + }, + }, + }; + } + return _doc; + }; +} + +/** + * This creates a migration map that applies search source migrations to legacy es query rules. + * It doesn't modify existing migrations. The following migrations will occur at minimum version of 8.3+. + */ +function getSearchSourceMigrations( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, + searchSourceMigrations: MigrateFunctionsObject +) { + const filteredMigrations: SavedObjectMigrationMap = {}; + for (const versionKey in searchSourceMigrations) { + if (gte(versionKey, MINIMUM_SS_MIGRATION_VERSION)) { + const migrateSearchSource = mapSearchSourceMigrationFunc( + searchSourceMigrations[versionKey] + ) as unknown as AlertMigration; + + filteredMigrations[versionKey] = executeMigrationWithErrorHandling( + createEsoMigration( + encryptedSavedObjects, + (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => + isEsQueryRuleType(doc), + pipeMigrations(migrateSearchSource) + ), + versionKey + ); + } + } + return filteredMigrations; +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index 4622e84081506e..999b0a5f4c4f50 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -419,6 +419,18 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); }); + it('8.2.0 migrates existing esQuery alerts to contain searchType param', async () => { + const response = await es.get<{ alert: RawRule }>( + { + index: '.kibana', + id: 'alert:776cb5c0-ad1e-11ec-ab9e-5f5932f4fad8', + }, + { meta: true } + ); + expect(response.statusCode).to.equal(200); + expect(response.body._source?.alert?.params.searchType).to.eql('esQuery'); + }); + it('8.3.0 removes internal tags in Security Solution rule', async () => { const response = await es.get<{ alert: RawRule }>( { diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json index 1c096d9df9930f..3ce8cddcb284d4 100644 --- a/x-pack/test/functional/es_archives/alerts/data.json +++ b/x-pack/test/functional/es_archives/alerts/data.json @@ -891,6 +891,57 @@ } } +{ + "type": "doc", + "value": { + "id": "alert:776cb5c0-ad1e-11ec-ab9e-5f5932f4fad8", + "index": ".kibana_1", + "source": { + "alert": { + "name": "123", + "alertTypeId": ".es-query", + "consumer": "alerts", + "params": { + "esQuery": "{\n \"query\":{\n \"match_all\" : {}\n }\n}", + "size": 100, + "timeWindowSize": 5, + "timeWindowUnit": "m", + "threshold": [ + 1000 + ], + "thresholdComparator": ">", + "index": [ + "kibana_sample_data_ecommerce" + ], + "timeField": "order_date" + }, + "schedule": { + "interval": "1m" + }, + "enabled": true, + "actions": [ + ], + "throttle": null, + "apiKeyOwner": null, + "createdBy" : "elastic", + "updatedBy" : "elastic", + "createdAt": "2022-03-26T16:04:50.698Z", + "muteAll": false, + "mutedInstanceIds": [], + "scheduledTaskId": "776cb5c0-ad1e-11ec-ab9e-5f5932f4fad8", + "tags": [] + }, + "type": "alert", + "updated_at": "2022-03-26T16:05:55.957Z", + "migrationVersion": { + "alert": "8.0.1" + }, + "references": [ + ] + } + } +} + { "type":"doc", "value":{ @@ -989,4 +1040,4 @@ ] } } -} \ No newline at end of file +} From 8539a912b52a02b372bd4d4756f260948d9ea4ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 6 May 2022 10:42:05 +0200 Subject: [PATCH 46/83] [EBT] Core Context Providers (#130785) --- .../analytics/client/src/schema/types.test.ts | 42 +++ packages/analytics/client/src/schema/types.ts | 2 +- .../analytics/analytics_service.test.mocks.ts | 25 ++ .../analytics/analytics_service.test.ts | 93 ++++++ .../public/analytics/analytics_service.ts | 132 +++++++- .../public/analytics/get_session_id.test.ts | 22 ++ src/core/public/analytics/get_session_id.ts | 20 ++ src/core/public/analytics/logger.test.ts | 80 +++++ src/core/public/core_system.test.mocks.ts | 14 + src/core/public/core_system.test.ts | 41 +++ src/core/public/core_system.ts | 36 ++- .../execution_context_service.test.ts | 26 +- .../execution_context_service.ts | 54 +++- .../public/fetch_optional_memory_info.test.ts | 35 +++ src/core/public/fetch_optional_memory_info.ts | 42 +++ .../injected_metadata_service.mock.ts | 1 + .../injected_metadata_service.test.ts | 30 ++ .../injected_metadata_service.ts | 12 + src/core/public/public.api.md | 2 +- .../server/analytics/analytics_service.ts | 40 +++ .../elasticsearch_service.mock.ts | 6 + .../elasticsearch_service.test.ts | 2 + .../elasticsearch/elasticsearch_service.ts | 8 + .../elasticsearch/get_cluster_info.test.ts | 82 +++++ .../server/elasticsearch/get_cluster_info.ts | 35 +++ ...egister_analytics_context_provider.test.ts | 35 +++ .../register_analytics_context_provider.ts | 32 ++ src/core/server/elasticsearch/types.ts | 2 + .../environment/environment_service.test.ts | 22 +- .../server/environment/environment_service.ts | 33 +- src/core/server/http/http_service.mock.ts | 3 +- src/core/server/rendering/__mocks__/params.ts | 3 + .../rendering_service.test.ts.snap | 132 ++++++++ .../rendering/rendering_service.test.ts | 18 ++ .../server/rendering/rendering_service.tsx | 36 ++- src/core/server/rendering/types.ts | 7 + ...luster_routing_allocation_disabled.test.ts | 4 +- src/core/server/server.ts | 126 +++++++- src/core/server/status/status_service.test.ts | 61 +++- src/core/server/status/status_service.ts | 60 +++- src/core/types/execution_context.ts | 2 +- .../public/custom_shipper.ts | 9 +- .../server/custom_shipper.ts | 9 +- test/analytics/config.ts | 2 +- test/analytics/services/kibana_ebt.ts | 24 +- .../tests/analytics_from_the_browser.ts | 7 +- .../tests/analytics_from_the_server.ts | 16 +- .../core_context_providers.ts | 93 ++++++ .../from_the_browser/index.ts | 9 +- .../from_the_browser/loaded_kibana.ts | 38 +++ .../from_the_server/core_context_providers.ts | 80 +++++ .../core_overall_status_changed.ts | 51 ++++ .../from_the_server/index.ts | 12 +- .../from_the_server/kibana_started.ts | 29 ++ ...ud_deployment_id_analytics_context.test.ts | 30 ++ ...r_cloud_deployment_id_analytics_context.ts | 28 ++ x-pack/plugins/cloud/public/plugin.test.ts | 287 +++++------------- x-pack/plugins/cloud/public/plugin.tsx | 193 ++---------- x-pack/plugins/cloud/server/plugin.ts | 4 +- ...egister_analytics_context_provider.test.ts | 51 ++++ .../register_analytics_context_provider.ts | 45 +++ x-pack/plugins/licensing/public/plugin.ts | 3 + x-pack/plugins/licensing/server/plugin.ts | 3 + 63 files changed, 2033 insertions(+), 448 deletions(-) create mode 100644 src/core/public/analytics/analytics_service.test.mocks.ts create mode 100644 src/core/public/analytics/analytics_service.test.ts create mode 100644 src/core/public/analytics/get_session_id.test.ts create mode 100644 src/core/public/analytics/get_session_id.ts create mode 100644 src/core/public/analytics/logger.test.ts create mode 100644 src/core/public/fetch_optional_memory_info.test.ts create mode 100644 src/core/public/fetch_optional_memory_info.ts create mode 100644 src/core/server/elasticsearch/get_cluster_info.test.ts create mode 100644 src/core/server/elasticsearch/get_cluster_info.ts create mode 100644 src/core/server/elasticsearch/register_analytics_context_provider.test.ts create mode 100644 src/core/server/elasticsearch/register_analytics_context_provider.ts create mode 100644 test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts create mode 100644 test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts create mode 100644 test/analytics/tests/instrumented_events/from_the_server/core_context_providers.ts create mode 100644 test/analytics/tests/instrumented_events/from_the_server/core_overall_status_changed.ts create mode 100644 test/analytics/tests/instrumented_events/from_the_server/kibana_started.ts create mode 100644 x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.test.ts create mode 100644 x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts create mode 100644 x-pack/plugins/licensing/common/register_analytics_context_provider.test.ts create mode 100644 x-pack/plugins/licensing/common/register_analytics_context_provider.ts diff --git a/packages/analytics/client/src/schema/types.test.ts b/packages/analytics/client/src/schema/types.test.ts index 3ed25e46d6dc9d..05eccf2bb19c79 100644 --- a/packages/analytics/client/src/schema/types.test.ts +++ b/packages/analytics/client/src/schema/types.test.ts @@ -421,6 +421,48 @@ describe('schema types', () => { }; expect(valueType).not.toBeUndefined(); // <-- Only to stop the var-not-used complain }); + + test('it should expect support readonly arrays', () => { + let valueType: SchemaValue> = { + type: 'array', + items: { + properties: { + a_value: { + type: 'keyword', + _meta: { + description: 'Some description', + }, + }, + }, + }, + }; + + valueType = { + type: 'array', + items: { + properties: { + a_value: { + type: 'keyword', + _meta: { + description: 'Some description', + optional: false, + }, + }, + }, + _meta: { + description: 'Description at the object level', + }, + }, + }; + + // @ts-expect-error because it's missing the items definition + valueType = { type: 'array' }; + // @ts-expect-error because it's missing the items definition + valueType = { type: 'array', items: {} }; + // @ts-expect-error because it's missing the items' properties definition + valueType = { type: 'array', items: { properties: {} } }; + expect(valueType).not.toBeUndefined(); // <-- Only to stop the var-not-used complain + }); }); }); diff --git a/packages/analytics/client/src/schema/types.ts b/packages/analytics/client/src/schema/types.ts index 8bac1ceaad6204..5043c46e73fd45 100644 --- a/packages/analytics/client/src/schema/types.ts +++ b/packages/analytics/client/src/schema/types.ts @@ -64,7 +64,7 @@ export type SchemaValue = ? // If the Value is unknown (TS can't infer the type), allow any type of schema SchemaArray | SchemaObject | SchemaChildValue : // Otherwise, try to infer the type and enforce the schema - NonNullable extends Array + NonNullable extends Array | ReadonlyArray ? SchemaArray : NonNullable extends object ? SchemaObject diff --git a/src/core/public/analytics/analytics_service.test.mocks.ts b/src/core/public/analytics/analytics_service.test.mocks.ts new file mode 100644 index 00000000000000..3d98cf43929262 --- /dev/null +++ b/src/core/public/analytics/analytics_service.test.mocks.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AnalyticsClient } from '@kbn/analytics-client'; +import { Subject } from 'rxjs'; + +export const analyticsClientMock: jest.Mocked = { + optIn: jest.fn(), + reportEvent: jest.fn(), + registerEventType: jest.fn(), + registerContextProvider: jest.fn(), + removeContextProvider: jest.fn(), + registerShipper: jest.fn(), + telemetryCounter$: new Subject(), + shutdown: jest.fn(), +}; + +jest.doMock('@kbn/analytics-client', () => ({ + createAnalytics: () => analyticsClientMock, +})); diff --git a/src/core/public/analytics/analytics_service.test.ts b/src/core/public/analytics/analytics_service.test.ts new file mode 100644 index 00000000000000..e2298a79ff134b --- /dev/null +++ b/src/core/public/analytics/analytics_service.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { firstValueFrom, Observable } from 'rxjs'; +import { analyticsClientMock } from './analytics_service.test.mocks'; +import { coreMock, injectedMetadataServiceMock } from '../mocks'; +import { AnalyticsService } from './analytics_service'; + +describe('AnalyticsService', () => { + let analyticsService: AnalyticsService; + beforeEach(() => { + jest.clearAllMocks(); + analyticsService = new AnalyticsService(coreMock.createCoreContext()); + }); + test('should register some context providers on creation', async () => { + expect(analyticsClientMock.registerContextProvider).toHaveBeenCalledTimes(3); + await expect( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[0][0].context$) + ).resolves.toMatchInlineSnapshot(` + Object { + "branch": "branch", + "buildNum": 100, + "buildSha": "buildSha", + "isDev": true, + "isDistributable": false, + "version": "version", + } + `); + await expect( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[1][0].context$) + ).resolves.toEqual({ session_id: expect.any(String) }); + await expect( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[2][0].context$) + ).resolves.toEqual({ + preferred_language: 'en-US', + preferred_languages: ['en-US', 'en'], + user_agent: expect.any(String), + }); + }); + + test('setup should expose all the register APIs, reportEvent and opt-in', () => { + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + expect(analyticsService.setup({ injectedMetadata })).toStrictEqual({ + registerShipper: expect.any(Function), + registerContextProvider: expect.any(Function), + removeContextProvider: expect.any(Function), + registerEventType: expect.any(Function), + reportEvent: expect.any(Function), + optIn: expect.any(Function), + telemetryCounter$: expect.any(Observable), + }); + }); + + test('setup should register the elasticsearch info context provider (undefined)', async () => { + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + analyticsService.setup({ injectedMetadata }); + await expect( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[3][0].context$) + ).resolves.toMatchInlineSnapshot(`undefined`); + }); + + test('setup should register the elasticsearch info context provider (with info)', async () => { + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + injectedMetadata.getElasticsearchInfo.mockReturnValue({ + cluster_name: 'cluster_name', + cluster_uuid: 'cluster_uuid', + cluster_version: 'version', + }); + analyticsService.setup({ injectedMetadata }); + await expect( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[3][0].context$) + ).resolves.toMatchInlineSnapshot(` + Object { + "cluster_name": "cluster_name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "version", + } + `); + }); + + test('setup should expose only the APIs report and opt-in', () => { + expect(analyticsService.start()).toStrictEqual({ + reportEvent: expect.any(Function), + optIn: expect.any(Function), + telemetryCounter$: expect.any(Observable), + }); + }); +}); diff --git a/src/core/public/analytics/analytics_service.ts b/src/core/public/analytics/analytics_service.ts index 86b0977faa0c06..723122ffbaef26 100644 --- a/src/core/public/analytics/analytics_service.ts +++ b/src/core/public/analytics/analytics_service.ts @@ -8,7 +8,10 @@ import type { AnalyticsClient } from '@kbn/analytics-client'; import { createAnalytics } from '@kbn/analytics-client'; +import { of } from 'rxjs'; +import { InjectedMetadataSetup } from '../injected_metadata'; import { CoreContext } from '../core_system'; +import { getSessionId } from './get_session_id'; import { createLogger } from './logger'; /** @@ -27,6 +30,11 @@ export type AnalyticsServiceStart = Pick< 'optIn' | 'reportEvent' | 'telemetryCounter$' >; +/** @internal */ +export interface AnalyticsServiceSetupDeps { + injectedMetadata: InjectedMetadataSetup; +} + export class AnalyticsService { private readonly analyticsClient: AnalyticsClient; @@ -38,9 +46,18 @@ export class AnalyticsService { // For now, we are relying on whether it's a distributable or running from source. sendTo: core.env.packageInfo.dist ? 'production' : 'staging', }); + + this.registerBuildInfoAnalyticsContext(core); + + // We may eventually move the following to the client's package since they are not Kibana-specific + // and can benefit other consumers of the client. + this.registerSessionIdContext(); + this.registerBrowserInfoAnalyticsContext(); } - public setup(): AnalyticsServiceSetup { + public setup({ injectedMetadata }: AnalyticsServiceSetupDeps): AnalyticsServiceSetup { + this.registerElasticsearchInfoContext(injectedMetadata); + return { optIn: this.analyticsClient.optIn, registerContextProvider: this.analyticsClient.registerContextProvider, @@ -51,6 +68,7 @@ export class AnalyticsService { telemetryCounter$: this.analyticsClient.telemetryCounter$, }; } + public start(): AnalyticsServiceStart { return { optIn: this.analyticsClient.optIn, @@ -58,7 +76,119 @@ export class AnalyticsService { telemetryCounter$: this.analyticsClient.telemetryCounter$, }; } + public stop() { this.analyticsClient.shutdown(); } + + /** + * Enriches the events with a session_id, so we can correlate them and understand funnels. + * @private + */ + private registerSessionIdContext() { + this.analyticsClient.registerContextProvider({ + name: 'session-id', + context$: of({ session_id: getSessionId() }), + schema: { + session_id: { + type: 'keyword', + _meta: { description: 'Unique session ID for every browser session' }, + }, + }, + }); + } + + /** + * Enriches the event with the build information. + * @param core The core context. + * @private + */ + private registerBuildInfoAnalyticsContext(core: CoreContext) { + this.analyticsClient.registerContextProvider({ + name: 'build info', + context$: of({ + isDev: core.env.mode.dev, + isDistributable: core.env.packageInfo.dist, + version: core.env.packageInfo.version, + branch: core.env.packageInfo.branch, + buildNum: core.env.packageInfo.buildNum, + buildSha: core.env.packageInfo.buildSha, + }), + schema: { + isDev: { + type: 'boolean', + _meta: { description: 'Is it running in development mode?' }, + }, + isDistributable: { + type: 'boolean', + _meta: { description: 'Is it running from a distributable?' }, + }, + version: { type: 'keyword', _meta: { description: 'Version of the Kibana instance.' } }, + branch: { + type: 'keyword', + _meta: { description: 'Branch of source running Kibana from.' }, + }, + buildNum: { type: 'long', _meta: { description: 'Build number of the Kibana instance.' } }, + buildSha: { type: 'keyword', _meta: { description: 'Build SHA of the Kibana instance.' } }, + }, + }); + } + + /** + * Enriches events with the current Browser's information + * @private + */ + private registerBrowserInfoAnalyticsContext() { + this.analyticsClient.registerContextProvider({ + name: 'browser info', + context$: of({ + user_agent: navigator.userAgent, + preferred_language: navigator.language, + preferred_languages: navigator.languages, + }), + schema: { + user_agent: { + type: 'keyword', + _meta: { description: 'User agent of the browser.' }, + }, + preferred_language: { + type: 'keyword', + _meta: { description: 'Preferred language of the browser.' }, + }, + preferred_languages: { + type: 'array', + items: { + type: 'keyword', + _meta: { description: 'List of the preferred languages of the browser.' }, + }, + }, + }, + }); + } + + /** + * Enriches the events with the Elasticsearch info (cluster name, uuid and version). + * @param injectedMetadata The injected metadata service. + * @private + */ + private registerElasticsearchInfoContext(injectedMetadata: InjectedMetadataSetup) { + this.analyticsClient.registerContextProvider({ + name: 'elasticsearch info', + context$: of(injectedMetadata.getElasticsearchInfo()), + schema: { + cluster_name: { + type: 'keyword', + _meta: { description: 'The Cluster Name', optional: true }, + }, + cluster_uuid: { + type: 'keyword', + _meta: { description: 'The Cluster UUID', optional: true }, + }, + cluster_version: { + type: 'keyword', + _meta: { description: 'The Cluster version', optional: true }, + }, + }, + }); + } } diff --git a/src/core/public/analytics/get_session_id.test.ts b/src/core/public/analytics/get_session_id.test.ts new file mode 100644 index 00000000000000..85ac515e29f68e --- /dev/null +++ b/src/core/public/analytics/get_session_id.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getSessionId } from './get_session_id'; + +describe('getSessionId', () => { + test('should return a session id', () => { + const sessionId = getSessionId(); + expect(sessionId).toStrictEqual(expect.any(String)); + }); + + test('calling it twice should return the same value', () => { + const sessionId1 = getSessionId(); + const sessionId2 = getSessionId(); + expect(sessionId2).toStrictEqual(sessionId1); + }); +}); diff --git a/src/core/public/analytics/get_session_id.ts b/src/core/public/analytics/get_session_id.ts new file mode 100644 index 00000000000000..62bb3a4a1c3368 --- /dev/null +++ b/src/core/public/analytics/get_session_id.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { v4 } from 'uuid'; + +/** + * Returns a session ID for the current user. + * We are storing it to the sessionStorage. This means it remains the same through refreshes, + * but it is not persisted when closing the browser/tab or manually navigating to another URL. + */ +export function getSessionId(): string { + const sessionId = sessionStorage.getItem('sessionId') ?? v4(); + sessionStorage.setItem('sessionId', sessionId); + return sessionId; +} diff --git a/src/core/public/analytics/logger.test.ts b/src/core/public/analytics/logger.test.ts new file mode 100644 index 00000000000000..2fbe17e3f7d220 --- /dev/null +++ b/src/core/public/analytics/logger.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { LogRecord } from '@kbn/logging'; +import { createLogger } from './logger'; + +describe('createLogger', () => { + // Calling `.mockImplementation` on all of them to avoid jest logging the console usage + const logErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + const logWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const logInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + const logDebugSpy = jest.spyOn(console, 'debug').mockImplementation(); + const logTraceSpy = jest.spyOn(console, 'trace').mockImplementation(); + const logLogSpy = jest.spyOn(console, 'log').mockImplementation(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should create a logger', () => { + const logger = createLogger(false); + expect(logger).toStrictEqual( + expect.objectContaining({ + fatal: expect.any(Function), + error: expect.any(Function), + warn: expect.any(Function), + info: expect.any(Function), + debug: expect.any(Function), + trace: expect.any(Function), + log: expect.any(Function), + get: expect.any(Function), + }) + ); + }); + + test('when isDev === false, it should not log anything', () => { + const logger = createLogger(false); + logger.fatal('fatal'); + expect(logErrorSpy).not.toHaveBeenCalled(); + logger.error('error'); + expect(logErrorSpy).not.toHaveBeenCalled(); + logger.warn('warn'); + expect(logWarnSpy).not.toHaveBeenCalled(); + logger.info('info'); + expect(logInfoSpy).not.toHaveBeenCalled(); + logger.debug('debug'); + expect(logDebugSpy).not.toHaveBeenCalled(); + logger.trace('trace'); + expect(logTraceSpy).not.toHaveBeenCalled(); + logger.log({} as LogRecord); + expect(logLogSpy).not.toHaveBeenCalled(); + logger.get().warn('warn'); + expect(logWarnSpy).not.toHaveBeenCalled(); + }); + + test('when isDev === true, it should log everything', () => { + const logger = createLogger(true); + logger.fatal('fatal'); + expect(logErrorSpy).toHaveBeenCalledTimes(1); + logger.error('error'); + expect(logErrorSpy).toHaveBeenCalledTimes(2); // fatal + error + logger.warn('warn'); + expect(logWarnSpy).toHaveBeenCalledTimes(1); + logger.info('info'); + expect(logInfoSpy).toHaveBeenCalledTimes(1); + logger.debug('debug'); + expect(logDebugSpy).toHaveBeenCalledTimes(1); + logger.trace('trace'); + expect(logTraceSpy).toHaveBeenCalledTimes(1); + logger.log({} as LogRecord); + expect(logLogSpy).toHaveBeenCalledTimes(1); + logger.get().warn('warn'); + expect(logWarnSpy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index 6eddf08cd2ae13..ff24cc88397942 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -21,6 +21,20 @@ import { renderingServiceMock } from './rendering/rendering_service.mock'; import { integrationsServiceMock } from './integrations/integrations_service.mock'; import { coreAppMock } from './core_app/core_app.mock'; import { themeServiceMock } from './theme/theme_service.mock'; +import { analyticsServiceMock } from './analytics/analytics_service.mock'; + +export const analyticsServiceStartMock = analyticsServiceMock.createAnalyticsServiceStart(); +export const MockAnalyticsService = analyticsServiceMock.create(); +MockAnalyticsService.start.mockReturnValue(analyticsServiceStartMock); +export const AnalyticsServiceConstructor = jest.fn().mockReturnValue(MockAnalyticsService); +jest.doMock('./analytics', () => ({ + AnalyticsService: AnalyticsServiceConstructor, +})); + +export const fetchOptionalMemoryInfoMock = jest.fn(); +jest.doMock('./fetch_optional_memory_info', () => ({ + fetchOptionalMemoryInfo: fetchOptionalMemoryInfoMock, +})); export const MockInjectedMetadataService = injectedMetadataServiceMock.create(); export const InjectedMetadataServiceConstructor = jest diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 553c1668951e82..2a57364c9f93ff 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -34,6 +34,10 @@ import { MockCoreApp, MockThemeService, ThemeServiceConstructor, + AnalyticsServiceConstructor, + MockAnalyticsService, + analyticsServiceStartMock, + fetchOptionalMemoryInfoMock, } from './core_system.test.mocks'; import { CoreSystem } from './core_system'; @@ -56,6 +60,7 @@ const defaultCoreSystemParams = { }, packageInfo: { dist: false, + version: '1.2.3', }, }, version: 'version', @@ -90,6 +95,7 @@ describe('constructor', () => { expect(IntegrationsServiceConstructor).toHaveBeenCalledTimes(1); expect(CoreAppConstructor).toHaveBeenCalledTimes(1); expect(ThemeServiceConstructor).toHaveBeenCalledTimes(1); + expect(AnalyticsServiceConstructor).toHaveBeenCalledTimes(1); }); it('passes injectedMetadata param to InjectedMetadataService', () => { @@ -146,6 +152,11 @@ describe('#setup()', () => { return core.setup(); } + it('calls analytics#setup()', async () => { + await setupCore(); + expect(MockAnalyticsService.setup).toHaveBeenCalledTimes(1); + }); + it('calls application#setup()', async () => { await setupCore(); expect(MockApplicationService.setup).toHaveBeenCalledTimes(1); @@ -222,6 +233,36 @@ describe('#start()', () => { ); }); + it('reports the event Loaded Kibana', async () => { + await startCore(); + expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(1); + expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledWith('Loaded Kibana', { + kibana_version: '1.2.3', + }); + }); + + it('reports the event Loaded Kibana (with memory)', async () => { + fetchOptionalMemoryInfoMock.mockReturnValue({ + memory_js_heap_size_limit: 3, + memory_js_heap_size_total: 2, + memory_js_heap_size_used: 1, + }); + + await startCore(); + expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(1); + expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledWith('Loaded Kibana', { + kibana_version: '1.2.3', + memory_js_heap_size_limit: 3, + memory_js_heap_size_total: 2, + memory_js_heap_size_used: 1, + }); + }); + + it('calls analytics#start()', async () => { + await startCore(); + expect(MockAnalyticsService.start).toHaveBeenCalledTimes(1); + }); + it('calls application#start()', async () => { await startCore(); expect(MockApplicationService.start).toHaveBeenCalledTimes(1); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 9334dd579f0f39..9ea1f16f7f2263 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -32,7 +32,9 @@ import { ThemeService } from './theme'; import { CoreApp } from './core_app'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; import { ExecutionContextService } from './execution_context'; +import type { AnalyticsServiceSetup } from './analytics'; import { AnalyticsService } from './analytics'; +import { fetchOptionalMemoryInfo } from './fetch_optional_memory_info'; interface Params { rootDomElement: HTMLElement; @@ -148,9 +150,10 @@ export class CoreSystem { await this.integrations.setup(); this.docLinks.setup(); - const analytics = this.analytics.setup(); + const analytics = this.analytics.setup({ injectedMetadata }); + this.registerLoadedKibanaEventType(analytics); - const executionContext = this.executionContext.setup(); + const executionContext = this.executionContext.setup({ analytics }); const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup, @@ -273,6 +276,11 @@ export class CoreSystem { targetDomElement: coreUiTargetDomElement, }); + analytics.reportEvent('Loaded Kibana', { + kibana_version: this.coreContext.env.packageInfo.version, + ...fetchOptionalMemoryInfo(), + }); + return { application, executionContext, @@ -303,4 +311,28 @@ export class CoreSystem { this.analytics.stop(); this.rootDomElement.textContent = ''; } + + private registerLoadedKibanaEventType(analytics: AnalyticsServiceSetup) { + analytics.registerEventType({ + eventType: 'Loaded Kibana', + schema: { + kibana_version: { + type: 'keyword', + _meta: { description: 'The version of Kibana' }, + }, + memory_js_heap_size_limit: { + type: 'long', + _meta: { description: 'The maximum size of the heap', optional: true }, + }, + memory_js_heap_size_total: { + type: 'long', + _meta: { description: 'The total size of the heap', optional: true }, + }, + memory_js_heap_size_used: { + type: 'long', + _meta: { description: 'The used size of the heap', optional: true }, + }, + }, + }); + } } diff --git a/src/core/public/execution_context/execution_context_service.test.ts b/src/core/public/execution_context/execution_context_service.test.ts index 70e57b8993bb1a..5c8f8bfae89f8c 100644 --- a/src/core/public/execution_context/execution_context_service.test.ts +++ b/src/core/public/execution_context/execution_context_service.test.ts @@ -5,23 +5,45 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { ExecutionContextService, ExecutionContextSetup } from './execution_context_service'; +import type { AnalyticsServiceSetup } from '../analytics'; +import { analyticsServiceMock } from '../analytics/analytics_service.mock'; describe('ExecutionContextService', () => { let execContext: ExecutionContextSetup; let curApp$: BehaviorSubject; let execService: ExecutionContextService; + let analytics: jest.Mocked; beforeEach(() => { + analytics = analyticsServiceMock.createAnalyticsServiceSetup(); execService = new ExecutionContextService(); - execContext = execService.setup(); + execContext = execService.setup({ analytics }); curApp$ = new BehaviorSubject('app1'); execContext = execService.start({ curApp$, }); }); + it('should extend the analytics context', async () => { + expect(analytics.registerContextProvider).toHaveBeenCalledTimes(1); + const context$ = analytics.registerContextProvider.mock.calls[0][0].context$; + execContext.set({ + type: 'ghf', + description: 'first set', + }); + + await expect(firstValueFrom(context$)).resolves.toMatchInlineSnapshot(` + Object { + "applicationId": "app1", + "entityId": undefined, + "page": undefined, + "pageName": "ghf:app1", + } + `); + }); + it('app name updates automatically and clears everything else', () => { execContext.set({ type: 'ghf', diff --git a/src/core/public/execution_context/execution_context_service.ts b/src/core/public/execution_context/execution_context_service.ts index a14d876c9643c8..c8d198b9c84f8d 100644 --- a/src/core/public/execution_context/execution_context_service.ts +++ b/src/core/public/execution_context/execution_context_service.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { isEqual, isUndefined, omitBy } from 'lodash'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { compact, isEqual, isUndefined, omitBy } from 'lodash'; +import { BehaviorSubject, Observable, Subscription, map } from 'rxjs'; +import { AnalyticsServiceSetup } from '../analytics'; import { CoreService, KibanaExecutionContext } from '../../types'; // Should be exported from elastic/apm-rum @@ -55,6 +56,10 @@ export interface ExecutionContextSetup { */ export type ExecutionContextStart = ExecutionContextSetup; +export interface SetupDeps { + analytics: AnalyticsServiceSetup; +} + export interface StartDeps { curApp$: Observable; } @@ -68,7 +73,9 @@ export class ExecutionContextService private subscription: Subscription = new Subscription(); private contract?: ExecutionContextSetup; - public setup() { + public setup({ analytics }: SetupDeps) { + this.enrichAnalyticsContext(analytics); + this.contract = { context$: this.context$.asObservable(), clear: () => { @@ -134,4 +141,45 @@ export class ExecutionContextService ...context, }; } + + /** + * Sets the analytics context provider based on the execution context details. + * @param analytics The analytics service + * @private + */ + private enrichAnalyticsContext(analytics: AnalyticsServiceSetup) { + analytics.registerContextProvider({ + name: 'execution_context', + context$: this.context$.pipe( + map(({ type, name, page, id }) => ({ + pageName: `${compact([type, name, page]).join(':')}`, + applicationId: name ?? type ?? 'unknown', + page, + entityId: id, + })) + ), + schema: { + pageName: { + type: 'keyword', + _meta: { description: 'The name of the current page' }, + }, + page: { + type: 'keyword', + _meta: { description: 'The current page', optional: true }, + }, + applicationId: { + type: 'keyword', + _meta: { description: 'The id of the current application' }, + }, + entityId: { + type: 'keyword', + _meta: { + description: + 'The id of the current entity (dashboard, visualization, canvas, lens, etc)', + optional: true, + }, + }, + }, + }); + } } diff --git a/src/core/public/fetch_optional_memory_info.test.ts b/src/core/public/fetch_optional_memory_info.test.ts new file mode 100644 index 00000000000000..f92fad9c14d634 --- /dev/null +++ b/src/core/public/fetch_optional_memory_info.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { fetchOptionalMemoryInfo } from './fetch_optional_memory_info'; + +describe('fetchOptionalMemoryInfo', () => { + test('should return undefined if no memory info is available', () => { + expect(fetchOptionalMemoryInfo()).toBeUndefined(); + }); + + test('should return the memory info when available', () => { + // @ts-expect-error 2339 + window.performance.memory = { + get jsHeapSizeLimit() { + return 3; + }, + get totalJSHeapSize() { + return 2; + }, + get usedJSHeapSize() { + return 1; + }, + }; + expect(fetchOptionalMemoryInfo()).toEqual({ + memory_js_heap_size_limit: 3, + memory_js_heap_size_total: 2, + memory_js_heap_size_used: 1, + }); + }); +}); diff --git a/src/core/public/fetch_optional_memory_info.ts b/src/core/public/fetch_optional_memory_info.ts new file mode 100644 index 00000000000000..b18f3ca2698da3 --- /dev/null +++ b/src/core/public/fetch_optional_memory_info.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * `Performance.memory` output. + * https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory + */ +export interface BrowserPerformanceMemoryInfo { + /** + * The maximum size of the heap, in bytes, that is available to the context. + */ + memory_js_heap_size_limit: number; + /** + * The total allocated heap size, in bytes. + */ + memory_js_heap_size_total: number; + /** + * The currently active segment of JS heap, in bytes. + */ + memory_js_heap_size_used: number; +} + +/** + * Get performance information from the browser (non-standard property). + * @remarks Only available in Google Chrome and MS Edge for now. + */ +export function fetchOptionalMemoryInfo(): BrowserPerformanceMemoryInfo | undefined { + // @ts-expect-error 2339 + const memory = window.performance.memory; + if (memory) { + return { + memory_js_heap_size_limit: memory.jsHeapSizeLimit, + memory_js_heap_size_total: memory.totalJSHeapSize, + memory_js_heap_size_used: memory.usedJSHeapSize, + }; + } +} diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index dc8fe63724411d..83903942df53d7 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -16,6 +16,7 @@ const createSetupContractMock = () => { getPublicBaseUrl: jest.fn(), getKibanaVersion: jest.fn(), getKibanaBranch: jest.fn(), + getElasticsearchInfo: jest.fn(), getCspConfig: jest.fn(), getExternalUrlConfig: jest.fn(), getAnonymousStatusPage: jest.fn(), diff --git a/src/core/public/injected_metadata/injected_metadata_service.test.ts b/src/core/public/injected_metadata/injected_metadata_service.test.ts index 3237401b38fa80..ba0e2470d7f264 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.test.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.test.ts @@ -9,6 +9,36 @@ import { DiscoveredPlugin } from '../../server'; import { InjectedMetadataService } from './injected_metadata_service'; +describe('setup.getElasticsearchInfo()', () => { + it('returns elasticsearch info from injectedMetadata', () => { + const setup = new InjectedMetadataService({ + injectedMetadata: { + clusterInfo: { + cluster_uuid: 'foo', + cluster_name: 'cluster_name', + cluster_version: 'version', + }, + }, + } as any).setup(); + + expect(setup.getElasticsearchInfo()).toEqual({ + cluster_uuid: 'foo', + cluster_name: 'cluster_name', + cluster_version: 'version', + }); + }); + + it('returns elasticsearch info as undefined if not present in the injectedMetadata', () => { + const setup = new InjectedMetadataService({ + injectedMetadata: { + clusterInfo: {}, + }, + } as any).setup(); + + expect(setup.getElasticsearchInfo()).toEqual({}); + }); +}); + describe('setup.getKibanaBuildNumber()', () => { it('returns buildNumber from injectedMetadata', () => { const setup = new InjectedMetadataService({ diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 07f56b889fc790..2e19da5c2cffe5 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -27,6 +27,12 @@ export interface InjectedPluginMetadata { }; } +export interface InjectedMetadataClusterInfo { + cluster_uuid?: string; + cluster_name?: string; + cluster_version?: string; +} + /** @internal */ export interface InjectedMetadataParams { injectedMetadata: { @@ -36,6 +42,7 @@ export interface InjectedMetadataParams { basePath: string; serverBasePath: string; publicBaseUrl: string; + clusterInfo: InjectedMetadataClusterInfo; category?: AppCategory; csp: { warnLegacyBrowsers: boolean; @@ -143,6 +150,10 @@ export class InjectedMetadataService { getTheme: () => { return this.state.theme; }, + + getElasticsearchInfo: () => { + return this.state.clusterInfo; + }, }; } } @@ -169,6 +180,7 @@ export interface InjectedMetadataSetup { darkMode: boolean; version: ThemeVersion; }; + getElasticsearchInfo: () => InjectedMetadataClusterInfo; /** * An array of frontend plugins in topological order. */ diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 3e431f07bd1cf7..732ba71fcd2afa 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1590,6 +1590,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:192:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:195:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/analytics/analytics_service.ts b/src/core/server/analytics/analytics_service.ts index 3afc997fd52ea4..24389dfa7e9386 100644 --- a/src/core/server/analytics/analytics_service.ts +++ b/src/core/server/analytics/analytics_service.ts @@ -8,6 +8,7 @@ import type { AnalyticsClient } from '@kbn/analytics-client'; import { createAnalytics } from '@kbn/analytics-client'; +import { of } from 'rxjs'; import type { CoreContext } from '../core_context'; /** @@ -43,6 +44,8 @@ export class AnalyticsService { // For now, we are relying on whether it's a distributable or running from source. sendTo: core.env.packageInfo.dist ? 'production' : 'staging', }); + + this.registerBuildInfoAnalyticsContext(core); } public preboot(): AnalyticsServicePreboot { @@ -74,7 +77,44 @@ export class AnalyticsService { telemetryCounter$: this.analyticsClient.telemetryCounter$, }; } + public stop() { this.analyticsClient.shutdown(); } + + /** + * Enriches the event with the build information. + * @param core The core context. + * @private + */ + private registerBuildInfoAnalyticsContext(core: CoreContext) { + this.analyticsClient.registerContextProvider({ + name: 'build info', + context$: of({ + isDev: core.env.mode.dev, + isDistributable: core.env.packageInfo.dist, + version: core.env.packageInfo.version, + branch: core.env.packageInfo.branch, + buildNum: core.env.packageInfo.buildNum, + buildSha: core.env.packageInfo.buildSha, + }), + schema: { + isDev: { + type: 'boolean', + _meta: { description: 'Is it running in development mode?' }, + }, + isDistributable: { + type: 'boolean', + _meta: { description: 'Is it running from a distributable?' }, + }, + version: { type: 'keyword', _meta: { description: 'Version of the Kibana instance.' } }, + branch: { + type: 'keyword', + _meta: { description: 'Branch of source running Kibana from.' }, + }, + buildNum: { type: 'long', _meta: { description: 'Build number of the Kibana instance.' } }, + buildSha: { type: 'keyword', _meta: { description: 'Build SHA of the Kibana instance.' } }, + }, + }); + } } diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index 3ef44e2690a95d..02a846a5b8011c 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -25,6 +25,7 @@ import { } from './types'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus, ServiceStatusLevels } from '../status'; +import type { ClusterInfo } from './get_cluster_info'; type MockedElasticSearchServicePreboot = jest.Mocked; @@ -89,6 +90,11 @@ const createInternalSetupContractMock = () => { warningNodes: [], kibanaVersion: '8.0.0', }), + clusterInfo$: new BehaviorSubject({ + cluster_uuid: 'cluster-uuid', + cluster_name: 'cluster-name', + cluster_version: '8.0.0', + }), status$: new BehaviorSubject>({ level: ServiceStatusLevels.available, summary: 'Elasticsearch is available', diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index def2c400258b54..875995cd7cd96b 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -34,6 +34,7 @@ import { elasticsearchClientMock } from './client/mocks'; import { duration } from 'moment'; import { isValidConnection as isValidConnectionMock } from './is_valid_connection'; import { pollEsNodesVersion as pollEsNodesVersionMocked } from './version_check/ensure_es_version'; +import { analyticsServiceMock } from '../analytics/analytics_service.mock'; const { pollEsNodesVersion: pollEsNodesVersionActual } = jest.requireActual( './version_check/ensure_es_version' @@ -53,6 +54,7 @@ let setupDeps: SetupDeps; beforeEach(() => { setupDeps = { + analytics: analyticsServiceMock.createAnalyticsServiceSetup(), http: httpServiceMock.createInternalSetupContract(), executionContext: executionContextServiceMock.createInternalSetupContract(), }; diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index d0cf23c5394166..09e8b3172c8e75 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -9,6 +9,8 @@ import { firstValueFrom, Observable, Subject } from 'rxjs'; import { map, shareReplay, takeUntil } from 'rxjs/operators'; +import { registerAnalyticsContextProvider } from './register_analytics_context_provider'; +import { AnalyticsServiceSetup } from '../analytics'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; @@ -29,8 +31,10 @@ import { isValidConnection } from './is_valid_connection'; import { isInlineScriptingEnabled } from './is_scripting_enabled'; import type { UnauthorizedErrorHandler } from './client/retry_unauthorized'; import { mergeConfig } from './merge_config'; +import { getClusterInfo$ } from './get_cluster_info'; export interface SetupDeps { + analytics: AnalyticsServiceSetup; http: InternalHttpServiceSetup; executionContext: InternalExecutionContextSetup; } @@ -92,10 +96,14 @@ export class ElasticsearchService this.esNodesCompatibility$ = esNodesCompatibility$; + const clusterInfo$ = getClusterInfo$(this.client.asInternalUser); + registerAnalyticsContextProvider(deps.analytics, clusterInfo$); + return { legacy: { config$: this.config$, }, + clusterInfo$, esNodesCompatibility$, status$: calculateStatus$(esNodesCompatibility$), setUnauthorizedErrorHandler: (handler) => { diff --git a/src/core/server/elasticsearch/get_cluster_info.test.ts b/src/core/server/elasticsearch/get_cluster_info.test.ts new file mode 100644 index 00000000000000..fd3b3b71844acf --- /dev/null +++ b/src/core/server/elasticsearch/get_cluster_info.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { elasticsearchClientMock } from './client/mocks'; +import { firstValueFrom } from 'rxjs'; +import { getClusterInfo$ } from './get_cluster_info'; + +describe('getClusterInfo', () => { + let internalClient: ReturnType; + const infoResponse = { + cluster_name: 'cluster-name', + cluster_uuid: 'cluster_uuid', + name: 'name', + tagline: 'tagline', + version: { + number: '1.2.3', + lucene_version: '1.2.3', + build_date: 'DateString', + build_flavor: 'string', + build_hash: 'string', + build_snapshot: true, + build_type: 'string', + minimum_index_compatibility_version: '1.2.3', + minimum_wire_compatibility_version: '1.2.3', + }, + }; + + beforeEach(() => { + internalClient = elasticsearchClientMock.createInternalClient(); + }); + + test('it provides the context', async () => { + internalClient.info.mockResolvedValue(infoResponse); + const context$ = getClusterInfo$(internalClient); + await expect(firstValueFrom(context$)).resolves.toMatchInlineSnapshot(` + Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "1.2.3", + } + `); + }); + + test('it retries if it fails to fetch the cluster info', async () => { + internalClient.info.mockRejectedValueOnce(new Error('Failed to fetch cluster info')); + internalClient.info.mockResolvedValue(infoResponse); + const context$ = getClusterInfo$(internalClient); + await expect(firstValueFrom(context$)).resolves.toMatchInlineSnapshot(` + Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "1.2.3", + } + `); + expect(internalClient.info).toHaveBeenCalledTimes(2); + }); + + test('multiple subscribers do not trigger more ES requests', async () => { + internalClient.info.mockResolvedValue(infoResponse); + const context$ = getClusterInfo$(internalClient); + await expect(firstValueFrom(context$)).resolves.toMatchInlineSnapshot(` + Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "1.2.3", + } + `); + await expect(firstValueFrom(context$)).resolves.toMatchInlineSnapshot(` + Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "1.2.3", + } + `); + expect(internalClient.info).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/core/server/elasticsearch/get_cluster_info.ts b/src/core/server/elasticsearch/get_cluster_info.ts new file mode 100644 index 00000000000000..c807965d3bbf8a --- /dev/null +++ b/src/core/server/elasticsearch/get_cluster_info.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Observable } from 'rxjs'; +import { defer, map, retry, shareReplay } from 'rxjs'; +import type { ElasticsearchClient } from './client'; + +/** @private */ +export interface ClusterInfo { + cluster_name: string; + cluster_uuid: string; + cluster_version: string; +} + +/** + * Returns the cluster info from the Elasticsearch cluster. + * @param internalClient Elasticsearch client + * @private + */ +export function getClusterInfo$(internalClient: ElasticsearchClient): Observable { + return defer(() => internalClient.info()).pipe( + map((info) => ({ + cluster_name: info.cluster_name, + cluster_uuid: info.cluster_uuid, + cluster_version: info.version.number, + })), + retry({ delay: 1000 }), + shareReplay(1) + ); +} diff --git a/src/core/server/elasticsearch/register_analytics_context_provider.test.ts b/src/core/server/elasticsearch/register_analytics_context_provider.test.ts new file mode 100644 index 00000000000000..4f09ea8677f44e --- /dev/null +++ b/src/core/server/elasticsearch/register_analytics_context_provider.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { firstValueFrom, of } from 'rxjs'; +import type { AnalyticsServiceSetup } from '../analytics'; +import { analyticsServiceMock } from '../analytics/analytics_service.mock'; +import { registerAnalyticsContextProvider } from './register_analytics_context_provider'; + +describe('registerAnalyticsContextProvider', () => { + let analyticsMock: jest.Mocked; + + beforeEach(() => { + analyticsMock = analyticsServiceMock.createAnalyticsServiceSetup(); + }); + + test('it provides the context', async () => { + registerAnalyticsContextProvider( + analyticsMock, + of({ cluster_name: 'cluster-name', cluster_uuid: 'cluster_uuid', cluster_version: '1.2.3' }) + ); + const { context$ } = analyticsMock.registerContextProvider.mock.calls[0][0]; + await expect(firstValueFrom(context$)).resolves.toMatchInlineSnapshot(` + Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "1.2.3", + } + `); + }); +}); diff --git a/src/core/server/elasticsearch/register_analytics_context_provider.ts b/src/core/server/elasticsearch/register_analytics_context_provider.ts new file mode 100644 index 00000000000000..cc4523c0d4eb52 --- /dev/null +++ b/src/core/server/elasticsearch/register_analytics_context_provider.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Observable } from 'rxjs'; +import type { AnalyticsServiceSetup } from '../analytics'; +import type { ClusterInfo } from './get_cluster_info'; + +/** + * Registers the Analytics context provider to enrich events with the cluster info. + * @param analytics Analytics service. + * @param context$ Observable emitting the cluster info. + * @private + */ +export function registerAnalyticsContextProvider( + analytics: AnalyticsServiceSetup, + context$: Observable +) { + analytics.registerContextProvider({ + name: 'elasticsearch info', + context$, + schema: { + cluster_name: { type: 'keyword', _meta: { description: 'The Cluster Name' } }, + cluster_uuid: { type: 'keyword', _meta: { description: 'The Cluster UUID' } }, + cluster_version: { type: 'keyword', _meta: { description: 'The Cluster version' } }, + }, + }); +} diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 1f363804b3a338..12ba2575d2726c 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -14,6 +14,7 @@ import { IClusterClient, ICustomClusterClient, ElasticsearchClientConfig } from import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus } from '../status'; import type { UnauthorizedErrorHandler } from './client/retry_unauthorized'; +import { ClusterInfo } from './get_cluster_info'; /** * @public @@ -97,6 +98,7 @@ export type InternalElasticsearchServicePreboot = ElasticsearchServicePreboot; /** @internal */ export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceSetup { + clusterInfo$: Observable; esNodesCompatibility$: Observable; status$: Observable>; } diff --git a/src/core/server/environment/environment_service.test.ts b/src/core/server/environment/environment_service.test.ts index 0817fad35f882e..c285edc443ce86 100644 --- a/src/core/server/environment/environment_service.test.ts +++ b/src/core/server/environment/environment_service.test.ts @@ -13,10 +13,12 @@ import { resolveInstanceUuid } from './resolve_uuid'; import { createDataFolder } from './create_data_folder'; import { writePidFile } from './write_pid_file'; import { CoreContext } from '../core_context'; +import type { AnalyticsServicePreboot } from '../analytics'; import { configServiceMock } from '../config/mocks'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { mockCoreContext } from '../core_context.mock'; +import { analyticsServiceMock } from '../analytics/analytics_service.mock'; jest.mock('./resolve_uuid', () => ({ resolveInstanceUuid: jest.fn().mockResolvedValue('SOME_UUID'), @@ -63,11 +65,13 @@ describe('UuidService', () => { let configService: ReturnType; let coreContext: CoreContext; let service: EnvironmentService; + let analytics: AnalyticsServicePreboot; beforeEach(async () => { logger = loggingSystemMock.create(); configService = getConfigService(); coreContext = mockCoreContext.create({ logger, configService }); + analytics = analyticsServiceMock.createAnalyticsServicePreboot(); service = new EnvironmentService(coreContext); }); @@ -78,7 +82,7 @@ describe('UuidService', () => { describe('#preboot()', () => { it('calls resolveInstanceUuid with correct parameters', async () => { - await service.preboot(); + await service.preboot({ analytics }); expect(resolveInstanceUuid).toHaveBeenCalledTimes(1); expect(resolveInstanceUuid).toHaveBeenCalledWith({ @@ -89,7 +93,7 @@ describe('UuidService', () => { }); it('calls createDataFolder with correct parameters', async () => { - await service.preboot(); + await service.preboot({ analytics }); expect(createDataFolder).toHaveBeenCalledTimes(1); expect(createDataFolder).toHaveBeenCalledWith({ @@ -99,7 +103,7 @@ describe('UuidService', () => { }); it('calls writePidFile with correct parameters', async () => { - await service.preboot(); + await service.preboot({ analytics }); expect(writePidFile).toHaveBeenCalledTimes(1); expect(writePidFile).toHaveBeenCalledWith({ @@ -109,14 +113,14 @@ describe('UuidService', () => { }); it('returns the uuid resolved from resolveInstanceUuid', async () => { - const preboot = await service.preboot(); + const preboot = await service.preboot({ analytics }); expect(preboot.instanceUuid).toEqual('SOME_UUID'); }); describe('process warnings', () => { it('logs warnings coming from the process', async () => { - await service.preboot(); + await service.preboot({ analytics }); const warning = new Error('something went wrong'); process.emit('warning', warning); @@ -126,7 +130,7 @@ describe('UuidService', () => { }); it('does not log deprecation warnings', async () => { - await service.preboot(); + await service.preboot({ analytics }); const warning = new Error('something went wrong'); warning.name = 'DeprecationWarning'; @@ -139,7 +143,7 @@ describe('UuidService', () => { // TODO: From Nodejs v16 emitting an unhandledRejection will kill the process describe.skip('unhandledRejection warnings', () => { it('logs warn for an unhandeld promise rejected with an Error', async () => { - await service.preboot(); + await service.preboot({ analytics }); const err = new Error('something went wrong'); process.emit('unhandledRejection', err, new Promise((res, rej) => rej(err))); @@ -151,7 +155,7 @@ describe('UuidService', () => { }); it('logs warn for an unhandeld promise rejected with a string', async () => { - await service.preboot(); + await service.preboot({ analytics }); const err = 'something went wrong'; process.emit('unhandledRejection', err, new Promise((res, rej) => rej(err))); @@ -166,7 +170,7 @@ describe('UuidService', () => { describe('#setup()', () => { it('returns the uuid resolved from resolveInstanceUuid', async () => { - await expect(service.preboot()).resolves.toEqual({ instanceUuid: 'SOME_UUID' }); + await expect(service.preboot({ analytics })).resolves.toEqual({ instanceUuid: 'SOME_UUID' }); expect(service.setup()).toEqual({ instanceUuid: 'SOME_UUID' }); }); }); diff --git a/src/core/server/environment/environment_service.ts b/src/core/server/environment/environment_service.ts index 65c03b108b28a0..28e2da446eb958 100644 --- a/src/core/server/environment/environment_service.ts +++ b/src/core/server/environment/environment_service.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { firstValueFrom } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { PathConfigType, config as pathConfigDef } from '@kbn/utils'; +import type { AnalyticsServicePreboot } from '../analytics'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { IConfigService } from '../config'; @@ -17,6 +18,16 @@ import { resolveInstanceUuid } from './resolve_uuid'; import { createDataFolder } from './create_data_folder'; import { writePidFile } from './write_pid_file'; +/** + * @internal + */ +export interface PrebootDeps { + /** + * {@link AnalyticsServicePreboot} + */ + analytics: AnalyticsServicePreboot; +} + /** * @internal */ @@ -45,7 +56,7 @@ export class EnvironmentService { this.configService = core.configService; } - public async preboot() { + public async preboot({ analytics }: PrebootDeps) { // IMPORTANT: This code is based on the assumption that none of the configuration values used // here is supposed to change during preboot phase and it's safe to read them only once. const [pathConfig, serverConfig, pidConfig] = await Promise.all([ @@ -77,6 +88,24 @@ export class EnvironmentService { logger: this.log, }); + analytics.registerContextProvider({ + name: 'kibana info', + context$: of({ + kibana_uuid: this.uuid, + pid: process.pid, + }), + schema: { + kibana_uuid: { + type: 'keyword', + _meta: { description: 'Kibana instance UUID' }, + }, + pid: { + type: 'long', + _meta: { description: 'Process ID' }, + }, + }, + }); + return { instanceUuid: this.uuid, }; diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index f251d3fb64cabd..557a10da0839de 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -45,8 +45,9 @@ export type HttpServiceSetupMock = jest.Mocked< createRouter: jest.MockedFunction<() => RouterMock>; }; export type InternalHttpServiceSetupMock = jest.Mocked< - Omit + Omit > & { + auth: AuthMocked; basePath: BasePathMocked; createRouter: jest.MockedFunction<(path: string) => RouterMock>; authRequestHeaders: jest.Mocked; diff --git a/src/core/server/rendering/__mocks__/params.ts b/src/core/server/rendering/__mocks__/params.ts index 091d185cceefce..b4ead2e628688e 100644 --- a/src/core/server/rendering/__mocks__/params.ts +++ b/src/core/server/rendering/__mocks__/params.ts @@ -7,6 +7,7 @@ */ import { mockCoreContext } from '../../core_context.mock'; +import { elasticsearchServiceMock } from '../../elasticsearch/elasticsearch_service.mock'; import { httpServiceMock } from '../../http/http_service.mock'; import { pluginServiceMock } from '../../plugins/plugins_service.mock'; import { statusServiceMock } from '../../status/status_service.mock'; @@ -15,6 +16,7 @@ const context = mockCoreContext.create(); const httpPreboot = httpServiceMock.createInternalPrebootContract(); const httpSetup = httpServiceMock.createInternalSetupContract(); const status = statusServiceMock.createInternalSetupContract(); +const elasticsearch = elasticsearchServiceMock.createInternalSetup(); export const mockRenderingServiceParams = context; export const mockRenderingPrebootDeps = { @@ -22,6 +24,7 @@ export const mockRenderingPrebootDeps = { uiPlugins: pluginServiceMock.createUiPlugins(), }; export const mockRenderingSetupDeps = { + elasticsearch, http: httpSetup, uiPlugins: pluginServiceMock.createUiPlugins(), status, diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 4abf24911808c6..9fe0cb545e7aa2 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -6,6 +6,7 @@ Object { "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, + "clusterInfo": Object {}, "csp": Object { "warnLegacyBrowsers": true, }, @@ -61,6 +62,7 @@ Object { "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, + "clusterInfo": Object {}, "csp": Object { "warnLegacyBrowsers": true, }, @@ -120,6 +122,7 @@ Object { "basePath": "", "branch": Any, "buildNumber": Any, + "clusterInfo": Object {}, "csp": Object { "warnLegacyBrowsers": true, }, @@ -169,12 +172,69 @@ Object { } `; +exports[`RenderingService preboot() render() renders "core" page for unauthenticated requests 1`] = ` +Object { + "anonymousStatusPage": false, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "clusterInfo": Object {}, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "mode": Object { + "dev": Any, + "name": Any, + "prod": Any, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": Any, + "version": Any, + }, + }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", + "serverBasePath": "/mock-server-basepath", + "theme": Object { + "darkMode": "theme:darkMode", + "version": "v8", + }, + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + exports[`RenderingService preboot() render() renders "core" with excluded user settings 1`] = ` Object { "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, + "clusterInfo": Object {}, "csp": Object { "warnLegacyBrowsers": true, }, @@ -230,6 +290,7 @@ Object { "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, + "clusterInfo": Object {}, "csp": Object { "warnLegacyBrowsers": true, }, @@ -285,6 +346,11 @@ Object { "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, + "clusterInfo": Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster-uuid", + "cluster_version": "8.0.0", + }, "csp": Object { "warnLegacyBrowsers": true, }, @@ -344,6 +410,11 @@ Object { "basePath": "", "branch": Any, "buildNumber": Any, + "clusterInfo": Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster-uuid", + "cluster_version": "8.0.0", + }, "csp": Object { "warnLegacyBrowsers": true, }, @@ -393,12 +464,73 @@ Object { } `; +exports[`RenderingService setup() render() renders "core" page for unauthenticated requests 1`] = ` +Object { + "anonymousStatusPage": false, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "clusterInfo": Object {}, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "mode": Object { + "dev": Any, + "name": Any, + "prod": Any, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": Any, + "version": Any, + }, + }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", + "serverBasePath": "/mock-server-basepath", + "theme": Object { + "darkMode": "theme:darkMode", + "version": "v8", + }, + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + exports[`RenderingService setup() render() renders "core" with excluded user settings 1`] = ` Object { "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, + "clusterInfo": Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster-uuid", + "cluster_version": "8.0.0", + }, "csp": Object { "warnLegacyBrowsers": true, }, diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index cb10d01e857739..8aecc536d8846d 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -25,6 +25,7 @@ import { } from './__mocks__/params'; import { InternalRenderingServicePreboot, InternalRenderingServiceSetup } from './types'; import { RenderingService } from './rendering_service'; +import { AuthStatus } from '../http/auth_state_storage'; const INJECTED_METADATA = { version: expect.any(String), @@ -75,6 +76,23 @@ function renderTestCases( expect(data).toMatchSnapshot(INJECTED_METADATA); }); + it('renders "core" page for unauthenticated requests', async () => { + mockRenderingSetupDeps.http.auth.get.mockReturnValueOnce({ + status: AuthStatus.unauthenticated, + state: {}, + }); + + const [render] = await getRender(); + const content = await render( + createKibanaRequest({ auth: { isAuthenticated: false } }), + uiSettings + ); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); + + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + it('renders "core" page for blank basepath', async () => { const [render, deps] = await getRender(); deps.http.basePath.get.mockReturnValueOnce(''); diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 73746a8f202ffe..3e50aac6fcbdda 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -8,10 +8,11 @@ import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; -import { take } from 'rxjs/operators'; +import { catchError, take, timeout } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; +import { firstValueFrom, of } from 'rxjs'; import type { UiPlugins } from '../plugins'; import { CoreContext } from '../core_context'; import { Template } from './views'; @@ -25,11 +26,13 @@ import { } from './types'; import { registerBootstrapRoute, bootstrapRendererFactory } from './bootstrap'; import { getSettingValue, getStylesheetPaths } from './render_utils'; -import { KibanaRequest } from '../http'; +import type { HttpAuth, KibanaRequest } from '../http'; import { IUiSettingsClient } from '../ui_settings'; import { filterUiPlugins } from './filter_ui_plugins'; -type RenderOptions = (RenderingPrebootDeps & { status?: never }) | RenderingSetupDeps; +type RenderOptions = + | (RenderingPrebootDeps & { status?: never; elasticsearch?: never }) + | RenderingSetupDeps; /** @internal */ export class RenderingService { @@ -57,6 +60,7 @@ export class RenderingService { } public async setup({ + elasticsearch, http, status, uiPlugins, @@ -72,12 +76,12 @@ export class RenderingService { }); return { - render: this.render.bind(this, { http, uiPlugins, status }), + render: this.render.bind(this, { elasticsearch, http, uiPlugins, status }), }; } private async render( - { http, uiPlugins, status }: RenderOptions, + { elasticsearch, http, uiPlugins, status }: RenderOptions, request: KibanaRequest, uiSettings: IUiSettingsClient, { isAnonymousPage = false, vars, includeExposedConfigKeys }: IRenderOptions = {} @@ -94,6 +98,21 @@ export class RenderingService { user: isAnonymousPage ? {} : await uiSettings.getUserProvided(), }; + let clusterInfo = {}; + try { + // Only provide the clusterInfo if the request is authenticated and the elasticsearch service is available. + if (isAuthenticated(http.auth, request) && elasticsearch) { + clusterInfo = await firstValueFrom( + elasticsearch.clusterInfo$.pipe( + timeout(50), // If not available, just return undefined + catchError(() => of({})) + ) + ); + } + } catch (err) { + // swallow error + } + const darkMode = getSettingValue('theme:darkMode', settings, Boolean); const themeVersion: ThemeVersion = 'v8'; @@ -123,6 +142,7 @@ export class RenderingService { serverBasePath, publicBaseUrl, env, + clusterInfo, anonymousStatusPage: status?.isStatusPageAnonymous() ?? false, i18n: { translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`, @@ -164,3 +184,9 @@ const getUiConfig = async (uiPlugins: UiPlugins, pluginId: string) => { exposedConfigKeys: {}, }) as { browserConfig: Record; exposedConfigKeys: Record }; }; + +const isAuthenticated = (auth: HttpAuth, request: KibanaRequest) => { + const { status: authStatus } = auth.get(request); + // status is 'unknown' when auth is disabled. we just need to not be `unauthenticated` here. + return authStatus !== 'unauthenticated'; +}; diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index 2c0aafe61e0189..82758018b859d9 100644 --- a/src/core/server/rendering/types.ts +++ b/src/core/server/rendering/types.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; +import { InternalElasticsearchServiceSetup } from '../elasticsearch'; import { EnvironmentMode, PackageInfo } from '../config'; import { ICspConfig } from '../csp'; import { InternalHttpServicePreboot, InternalHttpServiceSetup, KibanaRequest } from '../http'; @@ -38,6 +39,11 @@ export interface InjectedMetadata { basePath: string; serverBasePath: string; publicBaseUrl?: string; + clusterInfo: { + cluster_uuid?: string; + cluster_name?: string; + cluster_version?: string; + }; env: { mode: EnvironmentMode; packageInfo: PackageInfo; @@ -74,6 +80,7 @@ export interface RenderingPrebootDeps { /** @internal */ export interface RenderingSetupDeps { + elasticsearch: InternalElasticsearchServiceSetup; http: InternalHttpServiceSetup; status: InternalStatusServiceSetup; uiPlugins: UiPlugins; diff --git a/src/core/server/saved_objects/migrations/integration_tests/cluster_routing_allocation_disabled.test.ts b/src/core/server/saved_objects/migrations/integration_tests/cluster_routing_allocation_disabled.test.ts index 37b278fe9ccf05..525b9b3585c3f6 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/cluster_routing_allocation_disabled.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/cluster_routing_allocation_disabled.test.ts @@ -114,7 +114,7 @@ describe('unsupported_cluster_routing_allocation', () => { await root.setup(); await expect(root.start()).rejects.toThrowError( - /Unable to complete saved object migrations for the \[\.kibana\] index: \[unsupported_cluster_routing_allocation\] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue\. To proceed, please remove the cluster routing allocation settings with PUT \/_cluster\/settings {\"transient\": {\"cluster\.routing\.allocation\.enable\": null}, \"persistent\": {\"cluster\.routing\.allocation\.enable\": null}}\. Refer to https:\/\/www.elastic.co\/guide\/en\/kibana\/master\/resolve-migrations-failures.html#routing-allocation-disabled for more information on how to resolve the issue\./ + /Unable to complete saved object migrations for the \[\.kibana.*\] index: \[unsupported_cluster_routing_allocation\] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue\. To proceed, please remove the cluster routing allocation settings with PUT \/_cluster\/settings {\"transient\": {\"cluster\.routing\.allocation\.enable\": null}, \"persistent\": {\"cluster\.routing\.allocation\.enable\": null}}\. Refer to https:\/\/www.elastic.co\/guide\/en\/kibana\/master\/resolve-migrations-failures.html#routing-allocation-disabled for more information on how to resolve the issue\./ ); await retryAsync( @@ -149,7 +149,7 @@ describe('unsupported_cluster_routing_allocation', () => { await root.setup(); await expect(root.start()).rejects.toThrowError( - /Unable to complete saved object migrations for the \[\.kibana\] index: \[unsupported_cluster_routing_allocation\] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue\. To proceed, please remove the cluster routing allocation settings with PUT \/_cluster\/settings {\"transient\": {\"cluster\.routing\.allocation\.enable\": null}, \"persistent\": {\"cluster\.routing\.allocation\.enable\": null}}\. Refer to https:\/\/www.elastic.co\/guide\/en\/kibana\/master\/resolve-migrations-failures.html#routing-allocation-disabled for more information on how to resolve the issue\./ + /Unable to complete saved object migrations for the \[\.kibana.*\] index: \[unsupported_cluster_routing_allocation\] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue\. To proceed, please remove the cluster routing allocation settings with PUT \/_cluster\/settings {\"transient\": {\"cluster\.routing\.allocation\.enable\": null}, \"persistent\": {\"cluster\.routing\.allocation\.enable\": null}}\. Refer to https:\/\/www.elastic.co\/guide\/en\/kibana\/master\/resolve-migrations-failures.html#routing-allocation-disabled for more information on how to resolve the issue\./ ); }); }); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index bc5048de45cba2..234630734d437b 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -56,10 +56,25 @@ import { config as executionContextConfig } from './execution_context'; import { PrebootCoreRouteHandlerContext } from './preboot_core_route_handler_context'; import { PrebootService } from './preboot'; import { DiscoveredPlugins } from './plugins'; -import { AnalyticsService } from './analytics'; +import { AnalyticsService, AnalyticsServiceSetup } from './analytics'; const coreId = Symbol('core'); const rootConfigPath = ''; +const KIBANA_STARTED_EVENT = 'kibana_started'; + +/** @internal */ +interface UptimePerStep { + start: number; + end: number; +} + +/** @internal */ +interface UptimeSteps { + constructor: UptimePerStep; + preboot: UptimePerStep; + setup: UptimePerStep; + start: UptimePerStep; +} export class Server { public readonly configService: ConfigService; @@ -94,11 +109,15 @@ export class Server { private discoveredPlugins?: DiscoveredPlugins; private readonly logger: LoggerFactory; + private readonly uptimePerStep: Partial = {}; + constructor( rawConfigProvider: RawConfigurationProvider, public readonly env: Env, private readonly loggingSystem: ILoggingSystem ) { + const constructorStartUptime = process.uptime(); + this.logger = this.loggingSystem.asLoggerFactory(); this.log = this.logger.get('server'); this.configService = new ConfigService(rawConfigProvider, env, this.logger); @@ -129,15 +148,18 @@ export class Server { this.savedObjectsStartPromise = new Promise((resolve) => { this.resolveSavedObjectsStartPromise = resolve; }); + + this.uptimePerStep.constructor = { start: constructorStartUptime, end: process.uptime() }; } public async preboot() { this.log.debug('prebooting server'); + const prebootStartUptime = process.uptime(); const prebootTransaction = apm.startTransaction('server-preboot', 'kibana-platform'); const analyticsPreboot = this.analytics.preboot(); - const environmentPreboot = await this.environment.preboot(); + const environmentPreboot = await this.environment.preboot({ analytics: analyticsPreboot }); // Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph. this.discoveredPlugins = await this.plugins.discover({ environment: environmentPreboot }); @@ -187,15 +209,19 @@ export class Server { this.coreApp.preboot(corePreboot, uiPlugins); prebootTransaction?.end(); + this.uptimePerStep.preboot = { start: prebootStartUptime, end: process.uptime() }; return corePreboot; } public async setup() { this.log.debug('setting up server'); + const setupStartUptime = process.uptime(); const setupTransaction = apm.startTransaction('server-setup', 'kibana-platform'); const analyticsSetup = this.analytics.setup(); + this.registerKibanaStartedEventType(analyticsSetup); + const environmentSetup = this.environment.setup(); // Configuration could have changed after preboot. @@ -223,6 +249,7 @@ export class Server { const capabilitiesSetup = this.capabilities.setup({ http: httpSetup }); const elasticsearchServiceSetup = await this.elasticsearch.setup({ + analytics: analyticsSetup, http: httpSetup, executionContext: executionContextSetup, }); @@ -249,6 +276,7 @@ export class Server { }); const statusSetup = await this.status.setup({ + analytics: analyticsSetup, elasticsearch: elasticsearchServiceSetup, pluginDependencies: pluginTree.asNames, savedObjects: savedObjectsSetup, @@ -259,6 +287,7 @@ export class Server { }); const renderingSetup = await this.rendering.setup({ + elasticsearch: elasticsearchServiceSetup, http: httpSetup, status: statusSetup, uiPlugins, @@ -299,11 +328,13 @@ export class Server { this.coreApp.setup(coreSetup, uiPlugins); setupTransaction?.end(); + this.uptimePerStep.setup = { start: setupStartUptime, end: process.uptime() }; return coreSetup; } public async start() { this.log.debug('starting server'); + const startStartUptime = process.uptime(); const startTransaction = apm.startTransaction('server-start', 'kibana-platform'); const analyticsStart = this.analytics.start(); @@ -352,6 +383,9 @@ export class Server { startTransaction?.end(); + this.uptimePerStep.start = { start: startStartUptime, end: process.uptime() }; + analyticsStart.reportEvent(KIBANA_STARTED_EVENT, { uptime_per_step: this.uptimePerStep }); + return this.coreStart; } @@ -405,4 +439,92 @@ export class Server { this.configService.setSchema(descriptor.path, descriptor.schema); } } + + private registerKibanaStartedEventType(analyticsSetup: AnalyticsServiceSetup) { + analyticsSetup.registerEventType<{ uptime_per_step: UptimeSteps }>({ + eventType: KIBANA_STARTED_EVENT, + schema: { + uptime_per_step: { + properties: { + constructor: { + properties: { + start: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until the constructor was called', + }, + }, + end: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until the constructor finished', + }, + }, + }, + }, + preboot: { + properties: { + start: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `preboot` was called', + }, + }, + end: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `preboot` finished', + }, + }, + }, + }, + setup: { + properties: { + start: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `setup` was called', + }, + }, + end: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `setup` finished', + }, + }, + }, + }, + start: { + properties: { + start: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `start` was called', + }, + }, + end: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `start` finished', + }, + }, + }, + }, + }, + _meta: { + description: + 'Number of seconds the Node.js process has been running until each phase of the server execution is called and finished.', + }, + }, + }, + }); + } } diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index 262667fddf26a3..70181db9380ff6 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -6,11 +6,16 @@ * Side Public License, v 1. */ -import { of, BehaviorSubject } from 'rxjs'; - -import { ServiceStatus, ServiceStatusLevels, CoreStatus } from './types'; +import { of, BehaviorSubject, firstValueFrom } from 'rxjs'; + +import { + ServiceStatus, + ServiceStatusLevels, + CoreStatus, + InternalStatusServiceSetup, +} from './types'; import { StatusService } from './status_service'; -import { first } from 'rxjs/operators'; +import { first, take, toArray } from 'rxjs/operators'; import { mockCoreContext } from '../core_context.mock'; import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; import { environmentServiceMock } from '../environment/environment_service.mock'; @@ -19,6 +24,8 @@ import { mockRouter, RouterMock } from '../http/router/router.mock'; import { metricsServiceMock } from '../metrics/metrics_service.mock'; import { configServiceMock } from '../config/mocks'; import { coreUsageDataServiceMock } from '../core_usage_data/core_usage_data_service.mock'; +import { analyticsServiceMock } from '../analytics/analytics_service.mock'; +import { AnalyticsServiceSetup } from '..'; expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); @@ -47,6 +54,7 @@ describe('StatusService', () => { type SetupDeps = Parameters[0]; const setupDeps = (overrides: Partial): SetupDeps => { return { + analytics: analyticsServiceMock.createAnalyticsServiceSetup(), elasticsearch: { status$: of(available), }, @@ -535,5 +543,50 @@ describe('StatusService', () => { ); }); }); + + describe('analytics', () => { + let analyticsMock: jest.Mocked; + let setup: InternalStatusServiceSetup; + + beforeEach(async () => { + analyticsMock = analyticsServiceMock.createAnalyticsServiceSetup(); + setup = await service.setup(setupDeps({ analytics: analyticsMock })); + }); + + test('registers a context provider', async () => { + expect(analyticsMock.registerContextProvider).toHaveBeenCalledTimes(1); + const { context$ } = analyticsMock.registerContextProvider.mock.calls[0][0]; + await expect(firstValueFrom(context$.pipe(take(2), toArray()))).resolves + .toMatchInlineSnapshot(` + Array [ + Object { + "overall_status_level": "initializing", + "overall_status_summary": "Kibana is starting up", + }, + Object { + "overall_status_level": "available", + "overall_status_summary": "All services are available", + }, + ] + `); + }); + + test('registers and reports an event', async () => { + expect(analyticsMock.registerEventType).toHaveBeenCalledTimes(1); + expect(analyticsMock.reportEvent).toHaveBeenCalledTimes(0); + // wait for an emission of overall$ + await firstValueFrom(setup.overall$); + expect(analyticsMock.reportEvent).toHaveBeenCalledTimes(1); + expect(analyticsMock.reportEvent.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "core-overall_status_changed", + Object { + "overall_status_level": "available", + "overall_status_summary": "All services are available", + }, + ] + `); + }); + }); }); }); diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index 6c8f8716c036ea..a3dc0335c88af4 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -6,10 +6,21 @@ * Side Public License, v 1. */ -import { Observable, combineLatest, Subscription, Subject, firstValueFrom } from 'rxjs'; -import { map, distinctUntilChanged, shareReplay, debounceTime } from 'rxjs/operators'; +import { + Observable, + combineLatest, + Subscription, + Subject, + firstValueFrom, + tap, + BehaviorSubject, +} from 'rxjs'; +import { map, distinctUntilChanged, shareReplay, debounceTime, takeUntil } from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; +import type { RootSchema } from '@kbn/analytics-client'; + +import { AnalyticsServiceSetup } from '../analytics'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Logger, LogMeta } from '../logging'; @@ -32,7 +43,13 @@ interface StatusLogMeta extends LogMeta { kibana: { status: ServiceStatus }; } +interface StatusAnalyticsPayload { + overall_status_level: string; + overall_status_summary: string; +} + export interface SetupDeps { + analytics: AnalyticsServiceSetup; elasticsearch: Pick; environment: InternalEnvironmentServiceSetup; pluginDependencies: ReadonlyMap; @@ -57,6 +74,7 @@ export class StatusService implements CoreService { } public async setup({ + analytics, elasticsearch, pluginDependencies, http, @@ -88,6 +106,8 @@ export class StatusService implements CoreService { shareReplay(1) ); + this.setupAnalyticsContextAndEvents(analytics); + const coreOverall$ = core$.pipe( // Prevent many emissions at once from dependency status resolution from making this too noisy debounceTime(25), @@ -192,4 +212,40 @@ export class StatusService implements CoreService { shareReplay(1) ); } + + private setupAnalyticsContextAndEvents(analytics: AnalyticsServiceSetup) { + // Set an initial "initializing" status, so we can attach it to early events. + const context$ = new BehaviorSubject({ + overall_status_level: 'initializing', + overall_status_summary: 'Kibana is starting up', + }); + + // The schema is the same for the context and the events. + const schema: RootSchema = { + overall_status_level: { + type: 'keyword', + _meta: { description: 'The current availability level of the service.' }, + }, + overall_status_summary: { + type: 'text', + _meta: { description: 'A high-level summary of the service status.' }, + }, + }; + + const overallStatusChangedEventName = 'core-overall_status_changed'; + + analytics.registerEventType({ eventType: overallStatusChangedEventName, schema }); + analytics.registerContextProvider({ name: 'status info', context$, schema }); + + this.overall$!.pipe( + takeUntil(this.stop$), + map(({ level, summary }) => ({ + overall_status_level: level.toString(), + overall_status_summary: summary, + })), + // Emit the event before spreading the status to the context. + // This way we see from the context the previous status and the current one. + tap((statusPayload) => analytics.reportEvent(overallStatusChangedEventName, statusPayload)) + ).subscribe(context$); + } } diff --git a/src/core/types/execution_context.ts b/src/core/types/execution_context.ts index d790b8d855fd4e..d1e5cd10e5e914 100644 --- a/src/core/types/execution_context.ts +++ b/src/core/types/execution_context.ts @@ -14,7 +14,7 @@ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type KibanaExecutionContext = { /** - * Kibana application initated an operation. + * Kibana application initiated an operation. * */ readonly type?: string; // 'visualization' | 'actions' | 'server' | ..; /** public name of an application or a user-facing feature */ diff --git a/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/public/custom_shipper.ts b/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/public/custom_shipper.ts index ad45ba871f2c7e..97bf37749c2561 100644 --- a/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/public/custom_shipper.ts +++ b/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/public/custom_shipper.ts @@ -7,14 +7,21 @@ */ import { Subject } from 'rxjs'; +import type { AnalyticsClientInitContext } from '@kbn/analytics-client'; import type { Event, IShipper } from '@kbn/core/public'; export class CustomShipper implements IShipper { public static shipperName = 'FTR-helpers-shipper'; - constructor(private readonly events$: Subject) {} + constructor( + private readonly events$: Subject, + private readonly initContext: AnalyticsClientInitContext + ) {} public reportEvents(events: Event[]) { + this.initContext.logger.info( + `Reporting ${events.length} events to ${CustomShipper.shipperName}: ${JSON.stringify(events)}` + ); events.forEach((event) => { this.events$.next(event); }); diff --git a/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/server/custom_shipper.ts b/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/server/custom_shipper.ts index ed63f9a8db02f1..c76f30c94572e0 100644 --- a/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/server/custom_shipper.ts +++ b/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/server/custom_shipper.ts @@ -7,14 +7,21 @@ */ import { Subject } from 'rxjs'; +import type { AnalyticsClientInitContext } from '@kbn/analytics-client'; import type { IShipper, Event } from '@kbn/core/server'; export class CustomShipper implements IShipper { public static shipperName = 'FTR-helpers-shipper'; - constructor(private readonly events$: Subject) {} + constructor( + private readonly events$: Subject, + private readonly initContext: AnalyticsClientInitContext + ) {} public reportEvents(events: Event[]) { + this.initContext.logger.info( + `Reporting ${events.length} events to ${CustomShipper.shipperName}: ${JSON.stringify(events)}` + ); events.forEach((event) => { this.events$.next(event); }); diff --git a/test/analytics/config.ts b/test/analytics/config.ts index 9dee422762e151..ecb9792b0dff1e 100644 --- a/test/analytics/config.ts +++ b/test/analytics/config.ts @@ -34,7 +34,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), - // Disabling telemetry so it doesn't call opt-in before the tests run. + // Disabling telemetry, so it doesn't call opt-in before the tests run. '--telemetry.enabled=false', `--plugin-path=${path.resolve(__dirname, './__fixtures__/plugins/analytics_plugin_a')}`, `--plugin-path=${path.resolve(__dirname, './__fixtures__/plugins/analytics_ftr_helpers')}`, diff --git a/test/analytics/services/kibana_ebt.ts b/test/analytics/services/kibana_ebt.ts index fd64cbbbc01056..281794e899a3cf 100644 --- a/test/analytics/services/kibana_ebt.ts +++ b/test/analytics/services/kibana_ebt.ts @@ -12,24 +12,27 @@ import '@kbn/analytics-ftr-helpers-plugin/public/types'; export function KibanaEBTServerProvider({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const setOptIn = async (optIn: boolean) => { + await supertest + .post(`/internal/analytics_ftr_helpers/opt_in`) + .set('kbn-xsrf', 'xxx') + .query({ consent: optIn }) + .expect(200); + }; + return { /** * Change the opt-in state of the Kibana EBT client. * @param optIn `true` to opt-in, `false` to opt-out. */ - setOptIn: async (optIn: boolean) => { - await supertest - .post(`/internal/analytics_ftr_helpers/opt_in`) - .set('kbn-xsrf', 'xxx') - .query({ consent: optIn }) - .expect(200); - }, + setOptIn, /** * Returns the last events of the specified types. * @param numberOfEvents - number of events to return * @param eventTypes (Optional) array of event types to return */ getLastEvents: async (takeNumberOfEvents: number, eventTypes: string[] = []) => { + await setOptIn(true); const resp = await supertest .get(`/internal/analytics_ftr_helpers/events`) .query({ takeNumberOfEvents, eventTypes: JSON.stringify(eventTypes) }) @@ -45,6 +48,10 @@ export function KibanaEBTUIProvider({ getService, getPageObjects }: FtrProviderC const { common } = getPageObjects(['common']); const browser = getService('browser'); + const setOptIn = async (optIn: boolean) => { + await browser.execute((isOptIn) => window.__analytics_ftr_helpers__.setOptIn(isOptIn), optIn); + }; + return { /** * Change the opt-in state of the Kibana EBT client. @@ -52,7 +59,7 @@ export function KibanaEBTUIProvider({ getService, getPageObjects }: FtrProviderC */ setOptIn: async (optIn: boolean) => { await common.navigateToApp('home'); - await browser.execute((isOptIn) => window.__analytics_ftr_helpers__.setOptIn(isOptIn), optIn); + await setOptIn(optIn); }, /** * Returns the last events of the specified types. @@ -60,6 +67,7 @@ export function KibanaEBTUIProvider({ getService, getPageObjects }: FtrProviderC * @param eventTypes (Optional) array of event types to return */ getLastEvents: async (numberOfEvents: number, eventTypes: string[] = []) => { + await setOptIn(true); const events = await browser.execute( ({ eventTypes: _eventTypes, numberOfEvents: _numberOfEvents }) => window.__analytics_ftr_helpers__.getLastEvents(_numberOfEvents, _eventTypes), diff --git a/test/analytics/tests/analytics_from_the_browser.ts b/test/analytics/tests/analytics_from_the_browser.ts index 7acabf2112c5d1..c05492fe309617 100644 --- a/test/analytics/tests/analytics_from_the_browser.ts +++ b/test/analytics/tests/analytics_from_the_browser.ts @@ -72,6 +72,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(context).to.have.property('user_agent'); expect(context.user_agent).to.be.a('string'); + // Some context providers emit very early. We are OK with that. + const initialContext = actions[2].meta[0].context; + const reportEventContext = actions[2].meta[1].context; expect(reportEventContext).to.have.property('user_agent'); expect(reportEventContext.user_agent).to.be.a('string'); @@ -85,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { { timestamp: actions[2].meta[0].timestamp, event_type: 'test-plugin-lifecycle', - context: {}, + context: initialContext, properties: { plugin: 'analyticsPluginA', step: 'setup' }, }, { @@ -103,7 +106,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { { timestamp: actions[2].meta[0].timestamp, event_type: 'test-plugin-lifecycle', - context: {}, + context: initialContext, properties: { plugin: 'analyticsPluginA', step: 'setup' }, }, { diff --git a/test/analytics/tests/analytics_from_the_server.ts b/test/analytics/tests/analytics_from_the_server.ts index e5e3573b20fcde..820f7e51adc968 100644 --- a/test/analytics/tests/analytics_from_the_server.ts +++ b/test/analytics/tests/analytics_from_the_server.ts @@ -63,11 +63,19 @@ export default function ({ getService }: FtrProviderContext) { await ebtServerHelper.setOptIn(true); const actions = await getActions(3); + // Validating the remote PID because that's the only field that it's added by the FTR plugin. const context = actions[1].meta; expect(context).to.have.property('pid'); expect(context.pid).to.be.a('number'); + // Some context providers emit very early. We are OK with that. + const initialContext = actions[2].meta[0].context; + + const reportEventContext = actions[2].meta[1].context; + expect(context).to.have.property('pid'); + expect(context.pid).to.be.a('number'); + expect(actions).to.eql([ { action: 'optIn', meta: true }, { action: 'extendContext', meta: context }, @@ -77,13 +85,13 @@ export default function ({ getService }: FtrProviderContext) { { timestamp: actions[2].meta[0].timestamp, event_type: 'test-plugin-lifecycle', - context: {}, + context: initialContext, properties: { plugin: 'analyticsPluginA', step: 'setup' }, }, { timestamp: actions[2].meta[1].timestamp, event_type: 'test-plugin-lifecycle', - context, + context: reportEventContext, properties: { plugin: 'analyticsPluginA', step: 'start' }, }, ], @@ -96,13 +104,13 @@ export default function ({ getService }: FtrProviderContext) { { timestamp: actions[2].meta[0].timestamp, event_type: 'test-plugin-lifecycle', - context: {}, + context: initialContext, properties: { plugin: 'analyticsPluginA', step: 'setup' }, }, { timestamp: actions[2].meta[1].timestamp, event_type: 'test-plugin-lifecycle', - context, + context: reportEventContext, properties: { plugin: 'analyticsPluginA', step: 'start' }, }, ]); diff --git a/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts b/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts new file mode 100644 index 00000000000000..58d8de723639d1 --- /dev/null +++ b/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { Event } from '@kbn/core/public'; +import { FtrProviderContext } from '../../../services'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const deployment = getService('deployment'); + const ebtUIHelper = getService('kibana_ebt_ui'); + const { common } = getPageObjects(['common']); + + describe('Core Context Providers', () => { + let event: Event; + before(async () => { + await common.navigateToApp('home'); + [event] = await ebtUIHelper.getLastEvents(1, ['Loaded Kibana']); // Get the loaded Kibana event + }); + + it('should have the properties provided by the "cluster info" context provider', () => { + expect(event.context).to.have.property('cluster_uuid'); + expect(event.context.cluster_uuid).to.be.a('string'); + expect(event.context).to.have.property('cluster_name'); + expect(event.context.cluster_name).to.be.a('string'); + expect(event.context).to.have.property('cluster_version'); + expect(event.context.cluster_version).to.be.a('string'); + }); + + it('should have the properties provided by the "build info" context provider', () => { + expect(event.context).to.have.property('isDev'); + expect(event.context.isDev).to.be.a('boolean'); + expect(event.context).to.have.property('isDistributable'); + expect(event.context.isDistributable).to.be.a('boolean'); + expect(event.context).to.have.property('version'); + expect(event.context.version).to.be.a('string'); + expect(event.context).to.have.property('branch'); + expect(event.context.branch).to.be.a('string'); + expect(event.context).to.have.property('buildNum'); + expect(event.context.buildNum).to.be.a('number'); + expect(event.context).to.have.property('buildSha'); + expect(event.context.buildSha).to.be.a('string'); + }); + + it('should have the properties provided by the "session-id" context provider', () => { + expect(event.context).to.have.property('session_id'); + expect(event.context.session_id).to.be.a('string'); + }); + + it('should have the properties provided by the "browser info" context provider', () => { + expect(event.context).to.have.property('user_agent'); + expect(event.context.user_agent).to.be.a('string'); + expect(event.context).to.have.property('preferred_language'); + expect(event.context.preferred_language).to.be.a('string'); + expect(event.context).to.have.property('preferred_languages'); + expect(event.context.preferred_languages).to.be.an('array'); + (event.context.preferred_languages as unknown[]).forEach((lang) => + expect(lang).to.be.a('string') + ); + }); + + it('should have the properties provided by the "execution_context" context provider', () => { + expect(event.context).to.have.property('pageName'); + expect(event.context.pageName).to.be.a('string'); + expect(event.context).to.have.property('applicationId'); + expect(event.context.applicationId).to.be.a('string'); + expect(event.context).not.to.have.property('entityId'); // In the Home app it's not available. + expect(event.context).not.to.have.property('page'); // In the Home app it's not available. + }); + + it('should have the properties provided by the "license info" context provider', () => { + expect(event.context).to.have.property('license_id'); + expect(event.context.license_id).to.be.a('string'); + expect(event.context).to.have.property('license_status'); + expect(event.context.license_status).to.be.a('string'); + expect(event.context).to.have.property('license_type'); + expect(event.context.license_type).to.be.a('string'); + }); + + it('should have the properties provided by the "Cloud Deployment ID" context provider', async () => { + if (await deployment.isCloud()) { + expect(event.context).to.have.property('cloudId'); + expect(event.context.cloudId).to.be.a('string'); + } else { + expect(event.context).not.to.have.property('cloudId'); + } + }); + }); +} diff --git a/test/analytics/tests/instrumented_events/from_the_browser/index.ts b/test/analytics/tests/instrumented_events/from_the_browser/index.ts index daf21180d2328d..69aff97006d72d 100644 --- a/test/analytics/tests/instrumented_events/from_the_browser/index.ts +++ b/test/analytics/tests/instrumented_events/from_the_browser/index.ts @@ -8,13 +8,10 @@ import { FtrProviderContext } from '../../../services'; -export default function ({ getService }: FtrProviderContext) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('from the browser', () => { - beforeEach(async () => { - await getService('kibana_ebt_ui').setOptIn(true); - }); - // Add tests for UI-instrumented events here: - // loadTestFile(require.resolve('./some_event')); + loadTestFile(require.resolve('./loaded_kibana')); + loadTestFile(require.resolve('./core_context_providers')); }); } diff --git a/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts b/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts new file mode 100644 index 00000000000000..c7d3291cb03d40 --- /dev/null +++ b/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../services'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const ebtUIHelper = getService('kibana_ebt_ui'); + const { common } = getPageObjects(['common']); + const browser = getService('browser'); + + describe('Loaded Kibana', () => { + beforeEach(async () => { + await common.navigateToApp('home'); + }); + + it('should emit the "Loaded Kibana" event', async () => { + const [event] = await ebtUIHelper.getLastEvents(1, ['Loaded Kibana']); + expect(event.event_type).to.eql('Loaded Kibana'); + expect(event.properties).to.have.property('kibana_version'); + expect(event.properties.kibana_version).to.be.a('string'); + + if (browser.isChromium) { + expect(event.properties).to.have.property('memory_js_heap_size_limit'); + expect(event.properties.memory_js_heap_size_limit).to.be.a('number'); + expect(event.properties).to.have.property('memory_js_heap_size_total'); + expect(event.properties.memory_js_heap_size_total).to.be.a('number'); + expect(event.properties).to.have.property('memory_js_heap_size_used'); + expect(event.properties.memory_js_heap_size_used).to.be.a('number'); + } + }); + }); +} diff --git a/test/analytics/tests/instrumented_events/from_the_server/core_context_providers.ts b/test/analytics/tests/instrumented_events/from_the_server/core_context_providers.ts new file mode 100644 index 00000000000000..743a32fcc58ace --- /dev/null +++ b/test/analytics/tests/instrumented_events/from_the_server/core_context_providers.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { Event } from '@kbn/core/public'; +import { FtrProviderContext } from '../../../services'; + +export default function ({ getService }: FtrProviderContext) { + const deployment = getService('deployment'); + const ebtServerHelper = getService('kibana_ebt_server'); + + describe('Core Context Providers', () => { + let event: Event; + before(async () => { + // Wait for the 2nd "status_changed" event. At that point all the context providers should be set up. + [, event] = await ebtServerHelper.getLastEvents(2, ['core-overall_status_changed']); + }); + + it('should have the properties provided by the "kibana info" context provider', () => { + expect(event.context).to.have.property('kibana_uuid'); + expect(event.context.kibana_uuid).to.be.a('string'); + expect(event.context).to.have.property('pid'); + expect(event.context.pid).to.be.a('number'); + }); + + it('should have the properties provided by the "build info" context provider', () => { + expect(event.context).to.have.property('isDev'); + expect(event.context.isDev).to.be.a('boolean'); + expect(event.context).to.have.property('isDistributable'); + expect(event.context.isDistributable).to.be.a('boolean'); + expect(event.context).to.have.property('version'); + expect(event.context.version).to.be.a('string'); + expect(event.context).to.have.property('branch'); + expect(event.context.branch).to.be.a('string'); + expect(event.context).to.have.property('buildNum'); + expect(event.context.buildNum).to.be.a('number'); + expect(event.context).to.have.property('buildSha'); + expect(event.context.buildSha).to.be.a('string'); + }); + + it('should have the properties provided by the "cluster info" context provider', () => { + expect(event.context).to.have.property('cluster_uuid'); + expect(event.context.cluster_uuid).to.be.a('string'); + expect(event.context).to.have.property('cluster_name'); + expect(event.context.cluster_name).to.be.a('string'); + expect(event.context).to.have.property('cluster_version'); + expect(event.context.cluster_version).to.be.a('string'); + }); + + it('should have the properties provided by the "status info" context provider', () => { + expect(event.context).to.have.property('overall_status_level'); + expect(event.context.overall_status_level).to.be.a('string'); + expect(event.context).to.have.property('overall_status_summary'); + expect(event.context.overall_status_summary).to.be.a('string'); + }); + + it('should have the properties provided by the "license info" context provider', () => { + expect(event.context).to.have.property('license_id'); + expect(event.context.license_id).to.be.a('string'); + expect(event.context).to.have.property('license_status'); + expect(event.context.license_status).to.be.a('string'); + expect(event.context).to.have.property('license_type'); + expect(event.context.license_type).to.be.a('string'); + }); + + it('should have the properties provided by the "Cloud Deployment ID" context provider', async () => { + if (await deployment.isCloud()) { + expect(event.context).to.have.property('cloudId'); + expect(event.context.cloudId).to.be.a('string'); + } else { + expect(event.context).not.to.have.property('cloudId'); + } + }); + }); +} diff --git a/test/analytics/tests/instrumented_events/from_the_server/core_overall_status_changed.ts b/test/analytics/tests/instrumented_events/from_the_server/core_overall_status_changed.ts new file mode 100644 index 00000000000000..fa94e2b69fc3f1 --- /dev/null +++ b/test/analytics/tests/instrumented_events/from_the_server/core_overall_status_changed.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { Event } from '@kbn/analytics-client'; +import { FtrProviderContext } from '../../../services'; + +export default function ({ getService }: FtrProviderContext) { + const ebtServerHelper = getService('kibana_ebt_server'); + + describe('core-overall_status_changed', () => { + let initialEvent: Event; + let secondEvent: Event; + + before(async () => { + [initialEvent, secondEvent] = await ebtServerHelper.getLastEvents(2, [ + 'core-overall_status_changed', + ]); + }); + + it('should emit the initial "degraded" event with the context set to `initializing`', () => { + expect(initialEvent.event_type).to.eql('core-overall_status_changed'); + expect(initialEvent.context).to.have.property('overall_status_level', 'initializing'); + expect(initialEvent.context).to.have.property( + 'overall_status_summary', + 'Kibana is starting up' + ); + expect(initialEvent.properties).to.have.property('overall_status_level', 'degraded'); + expect(initialEvent.properties.overall_status_summary).to.be.a('string'); + }); + + it('should emit the 2nd event as `available` with the context set to the previous values', () => { + expect(secondEvent.event_type).to.eql('core-overall_status_changed'); + expect(secondEvent.context).to.have.property( + 'overall_status_level', + initialEvent.properties.overall_status_level + ); + expect(secondEvent.context).to.have.property( + 'overall_status_summary', + initialEvent.properties.overall_status_summary + ); + expect(secondEvent.properties.overall_status_level).to.be.a('string'); // Ideally we would test it as `available`, but we can't do that as it may result flaky for many side effects in the CI. + expect(secondEvent.properties.overall_status_summary).to.be.a('string'); + }); + }); +} diff --git a/test/analytics/tests/instrumented_events/from_the_server/index.ts b/test/analytics/tests/instrumented_events/from_the_server/index.ts index 8961b9e92994c6..d8150b0519fdeb 100644 --- a/test/analytics/tests/instrumented_events/from_the_server/index.ts +++ b/test/analytics/tests/instrumented_events/from_the_server/index.ts @@ -8,13 +8,11 @@ import { FtrProviderContext } from '../../../services'; -export default function ({ getService }: FtrProviderContext) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('from the server', () => { - beforeEach(async () => { - await getService('kibana_ebt_server').setOptIn(true); - }); - - // Add tests for UI-instrumented events here: - // loadTestFile(require.resolve('./some_event')); + // Add tests for Server-instrumented events here: + loadTestFile(require.resolve('./core_context_providers')); + loadTestFile(require.resolve('./kibana_started')); + loadTestFile(require.resolve('./core_overall_status_changed')); }); } diff --git a/test/analytics/tests/instrumented_events/from_the_server/kibana_started.ts b/test/analytics/tests/instrumented_events/from_the_server/kibana_started.ts new file mode 100644 index 00000000000000..86917b937cbabd --- /dev/null +++ b/test/analytics/tests/instrumented_events/from_the_server/kibana_started.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../services'; + +export default function ({ getService }: FtrProviderContext) { + const ebtServerHelper = getService('kibana_ebt_server'); + + describe('kibana_started', () => { + it('should emit the "kibana_started" event', async () => { + const [event] = await ebtServerHelper.getLastEvents(1, ['kibana_started']); + expect(event.event_type).to.eql('kibana_started'); + expect(event.properties.uptime_per_step.constructor.start).to.be.a('number'); + expect(event.properties.uptime_per_step.constructor.end).to.be.a('number'); + expect(event.properties.uptime_per_step.preboot.start).to.be.a('number'); + expect(event.properties.uptime_per_step.preboot.end).to.be.a('number'); + expect(event.properties.uptime_per_step.setup.start).to.be.a('number'); + expect(event.properties.uptime_per_step.setup.end).to.be.a('number'); + expect(event.properties.uptime_per_step.start.start).to.be.a('number'); + expect(event.properties.uptime_per_step.start.end).to.be.a('number'); + }); + }); +} diff --git a/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.test.ts b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.test.ts new file mode 100644 index 00000000000000..a6dc1f59b00e31 --- /dev/null +++ b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { firstValueFrom } from 'rxjs'; +import { registerCloudDeploymentIdAnalyticsContext } from './register_cloud_deployment_id_analytics_context'; + +describe('registerCloudDeploymentIdAnalyticsContext', () => { + let analytics: { registerContextProvider: jest.Mock }; + beforeEach(() => { + analytics = { + registerContextProvider: jest.fn(), + }; + }); + + test('it does not register the context provider if cloudId not provided', () => { + registerCloudDeploymentIdAnalyticsContext(analytics); + expect(analytics.registerContextProvider).not.toHaveBeenCalled(); + }); + + test('it registers the context provider and emits the cloudId', async () => { + registerCloudDeploymentIdAnalyticsContext(analytics, 'cloud_id'); + expect(analytics.registerContextProvider).toHaveBeenCalledTimes(1); + const [{ context$ }] = analytics.registerContextProvider.mock.calls[0]; + await expect(firstValueFrom(context$)).resolves.toEqual({ cloudId: 'cloud_id' }); + }); +}); diff --git a/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts new file mode 100644 index 00000000000000..e8bdc6b37b50c5 --- /dev/null +++ b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AnalyticsClient } from '@kbn/analytics-client'; +import { of } from 'rxjs'; + +export function registerCloudDeploymentIdAnalyticsContext( + analytics: Pick, + cloudId?: string +) { + if (!cloudId) { + return; + } + analytics.registerContextProvider({ + name: 'Cloud Deployment ID', + context$: of({ cloudId }), + schema: { + cloudId: { + type: 'keyword', + _meta: { description: 'The Cloud Deployment ID' }, + }, + }, + }); +} diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts index 5e0294178a5daf..cfd0d456674175 100644 --- a/x-pack/plugins/cloud/public/plugin.test.ts +++ b/x-pack/plugins/cloud/public/plugin.test.ts @@ -9,9 +9,8 @@ import { nextTick } from '@kbn/test-jest-helpers'; import { coreMock } from '@kbn/core/public/mocks'; import { homePluginMock } from '@kbn/home-plugin/public/mocks'; import { securityMock } from '@kbn/security-plugin/public/mocks'; -import { CloudPlugin, CloudConfigType, loadUserId } from './plugin'; -import { firstValueFrom, Observable, Subject } from 'rxjs'; -import { KibanaExecutionContext } from '@kbn/core/public'; +import { CloudPlugin, CloudConfigType } from './plugin'; +import { firstValueFrom } from 'rxjs'; describe('Cloud Plugin', () => { describe('#setup', () => { @@ -20,17 +19,7 @@ describe('Cloud Plugin', () => { jest.clearAllMocks(); }); - const setupPlugin = async ({ - config = {}, - securityEnabled = true, - currentUserProps = {}, - currentContext$ = undefined, - }: { - config?: Partial; - securityEnabled?: boolean; - currentUserProps?: Record; - currentContext$?: Observable; - }) => { + const setupPlugin = async ({ config = {} }: { config?: Partial }) => { const initContext = coreMock.createPluginInitializerContext({ id: 'cloudId', base_url: 'https://cloud.elastic.co', @@ -49,21 +38,9 @@ describe('Cloud Plugin', () => { const plugin = new CloudPlugin(initContext); const coreSetup = coreMock.createSetup(); - const coreStart = coreMock.createStart(); - if (currentContext$) { - coreStart.executionContext.context$ = currentContext$; - } - - coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]); - - const securitySetup = securityMock.createSetup(); - - securitySetup.authc.getCurrentUser.mockResolvedValue( - securityMock.createMockAuthenticatedUser(currentUserProps) - ); + const setup = plugin.setup(coreSetup, {}); - const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {}); // Wait for FullStory dynamic import to resolve await new Promise((r) => setImmediate(r)); @@ -73,9 +50,6 @@ describe('Cloud Plugin', () => { test('register the shipper FullStory with correct args when enabled and org_id are set', async () => { const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' } }, - currentUserProps: { - username: '1234', - }, }); expect(coreSetup.analytics.registerShipper).toHaveBeenCalled(); @@ -86,9 +60,67 @@ describe('Cloud Plugin', () => { }); }); + it('does not call initializeFullStory when enabled=false', async () => { + const { coreSetup } = await setupPlugin({ + config: { full_story: { enabled: false, org_id: 'foo' } }, + }); + expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled(); + }); + + it('does not call initializeFullStory when org_id is undefined', async () => { + const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true } } }); + expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled(); + }); + }); + + describe('setupTelemetryContext', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const setupPlugin = async ({ + config = {}, + securityEnabled = true, + currentUserProps = {}, + }: { + config?: Partial; + securityEnabled?: boolean; + currentUserProps?: Record | Error; + }) => { + const initContext = coreMock.createPluginInitializerContext({ + base_url: 'https://cloud.elastic.co', + deployment_url: '/abc123', + profile_url: '/profile/alice', + organization_url: '/org/myOrg', + full_story: { + enabled: false, + }, + chat: { + enabled: false, + }, + ...config, + }); + + const plugin = new CloudPlugin(initContext); + + const coreSetup = coreMock.createSetup(); + const securitySetup = securityMock.createSetup(); + if (currentUserProps instanceof Error) { + securitySetup.authc.getCurrentUser.mockRejectedValue(currentUserProps); + } else { + securitySetup.authc.getCurrentUser.mockResolvedValue( + securityMock.createMockAuthenticatedUser(currentUserProps) + ); + } + + const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {}); + + return { initContext, plugin, setup, coreSetup }; + }; + test('register the context provider for the cloud user with hashed user ID when security is available', async () => { const { coreSetup } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' } }, + config: { id: 'cloudId' }, currentUserProps: { username: '1234', }, @@ -105,9 +137,9 @@ describe('Cloud Plugin', () => { }); }); - it('user hash includes org id', async () => { + it('user hash includes cloud id', async () => { const { coreSetup: coreSetup1 } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg1' }, + config: { id: 'esOrg1' }, currentUserProps: { username: '1234', }, @@ -137,146 +169,37 @@ describe('Cloud Plugin', () => { expect(hashId1).not.toEqual(hashId2); }); - it('emits the execution context provider everytime an app changes', async () => { - const currentContext$ = new Subject(); + test('user hash does not include cloudId when not provided', async () => { const { coreSetup } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' } }, + config: {}, currentUserProps: { username: '1234', }, - currentContext$, }); + expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); + const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find( - ([{ name }]) => name === 'execution_context' + ([{ name }]) => name === 'cloud_user_id' )!; - let latestContext; - context$.subscribe((context) => { - latestContext = context; - }); - - // takes the app name - expect(latestContext).toBeUndefined(); - currentContext$.next({ - name: 'App1', - description: '123', - }); - - await new Promise((r) => setImmediate(r)); - - expect(latestContext).toEqual({ - pageName: 'App1', - applicationId: 'App1', - }); - - // context clear - currentContext$.next({}); - expect(latestContext).toEqual({ - pageName: '', - applicationId: 'unknown', - }); - - // different app - currentContext$.next({ - name: 'App2', - page: 'page2', - id: '123', - }); - expect(latestContext).toEqual({ - pageName: 'App2:page2', - applicationId: 'App2', - page: 'page2', - entityId: '123', - }); - - // Back to first app - currentContext$.next({ - name: 'App1', - page: 'page3', - id: '123', - }); - - expect(latestContext).toEqual({ - pageName: 'App1:page3', - applicationId: 'App1', - page: 'page3', - entityId: '123', + await expect(firstValueFrom(context$)).resolves.toEqual({ + userId: '03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4', }); }); - it('does not register the cloud user id context provider when security is not available', async () => { + test('user hash is undefined when failed to fetch a user', async () => { const { coreSetup } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' } }, - securityEnabled: false, - }); - - expect( - coreSetup.analytics.registerContextProvider.mock.calls.find( - ([{ name }]) => name === 'cloud_user_id' - ) - ).toBeUndefined(); - }); - - describe('with memory', () => { - beforeAll(() => { - // @ts-expect-error 2339 - window.performance.memory = { - get jsHeapSizeLimit() { - return 3; - }, - get totalJSHeapSize() { - return 2; - }, - get usedJSHeapSize() { - return 1; - }, - }; + currentUserProps: new Error('failed to fetch a user'), }); - afterAll(() => { - // @ts-expect-error 2339 - delete window.performance.memory; - }); - - it('reports an event when security is available', async () => { - const { initContext, coreSetup } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' } }, - currentUserProps: { - username: '1234', - }, - }); - - expect(coreSetup.analytics.reportEvent).toHaveBeenCalledWith('Loaded Kibana', { - kibana_version: initContext.env.packageInfo.version, - memory_js_heap_size_limit: 3, - memory_js_heap_size_total: 2, - memory_js_heap_size_used: 1, - }); - }); - }); - - it('reports an event when security is not available', async () => { - const { initContext, coreSetup } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' } }, - securityEnabled: false, - }); - - expect(coreSetup.analytics.reportEvent).toHaveBeenCalledWith('Loaded Kibana', { - kibana_version: initContext.env.packageInfo.version, - }); - }); + expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); - it('does not call initializeFullStory when enabled=false', async () => { - const { coreSetup } = await setupPlugin({ - config: { full_story: { enabled: false, org_id: 'foo' } }, - }); - expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled(); - }); + const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find( + ([{ name }]) => name === 'cloud_user_id' + )!; - it('does not call initializeFullStory when org_id is undefined', async () => { - const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true } } }); - expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled(); + await expect(firstValueFrom(context$)).resolves.toEqual({ userId: undefined }); }); }); @@ -652,56 +575,4 @@ describe('Cloud Plugin', () => { expect(securityStart.navControlService.addUserMenuLinks).not.toHaveBeenCalled(); }); }); - - describe('loadFullStoryUserId', () => { - let consoleMock: jest.SpyInstance; - - beforeEach(() => { - consoleMock = jest.spyOn(console, 'debug').mockImplementation(() => {}); - }); - afterEach(() => { - consoleMock.mockRestore(); - }); - - it('returns principal ID when username specified', async () => { - expect( - await loadUserId({ - getCurrentUser: jest.fn().mockResolvedValue({ - username: '1234', - }), - }) - ).toEqual('1234'); - expect(consoleMock).not.toHaveBeenCalled(); - }); - - it('returns undefined if getCurrentUser throws', async () => { - expect( - await loadUserId({ - getCurrentUser: jest.fn().mockRejectedValue(new Error(`Oh no!`)), - }) - ).toBeUndefined(); - }); - - it('returns undefined if getCurrentUser returns undefined', async () => { - expect( - await loadUserId({ - getCurrentUser: jest.fn().mockResolvedValue(undefined), - }) - ).toBeUndefined(); - }); - - it('returns undefined and logs if username undefined', async () => { - expect( - await loadUserId({ - getCurrentUser: jest.fn().mockResolvedValue({ - username: undefined, - metadata: { foo: 'bar' }, - }), - }) - ).toBeUndefined(); - expect(consoleMock).toHaveBeenLastCalledWith( - `[cloud.analytics] username not specified. User metadata: {"foo":"bar"}` - ); - }); - }); }); diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index 4ee3098c709cfe..db6b2305495bff 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -13,21 +13,16 @@ import type { PluginInitializerContext, HttpStart, IBasePath, - ExecutionContextStart, AnalyticsServiceSetup, } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import useObservable from 'react-use/lib/useObservable'; -import { BehaviorSubject, from, of, Subscription } from 'rxjs'; -import { exhaustMap, filter, map } from 'rxjs/operators'; -import { compact } from 'lodash'; +import { BehaviorSubject, catchError, from, map, of } from 'rxjs'; -import type { - AuthenticatedUser, - SecurityPluginSetup, - SecurityPluginStart, -} from '@kbn/security-plugin/public'; +import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import { Sha256 } from '@kbn/core/public/utils'; +import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { ELASTIC_SUPPORT_LINK, @@ -91,11 +86,6 @@ interface SetupFullStoryDeps { analytics: AnalyticsServiceSetup; basePath: IBasePath; } -interface SetupTelemetryContextDeps extends CloudSetupDependencies { - analytics: AnalyticsServiceSetup; - executionContextPromise: Promise; - cloudId?: string; -} interface SetupChatDeps extends Pick { http: CoreSetup['http']; @@ -104,7 +94,6 @@ interface SetupChatDeps extends Pick { export class CloudPlugin implements Plugin { private readonly config: CloudConfigType; private isCloudEnabled: boolean; - private appSubscription?: Subscription; private chatConfig$ = new BehaviorSubject({ enabled: false }); constructor(private readonly initializerContext: PluginInitializerContext) { @@ -113,19 +102,7 @@ export class CloudPlugin implements Plugin { } public setup(core: CoreSetup, { home, security }: CloudSetupDependencies) { - const executionContextPromise = core.getStartServices().then(([coreStart]) => { - return coreStart.executionContext; - }); - - this.setupTelemetryContext({ - analytics: core.analytics, - security, - executionContextPromise, - cloudId: this.config.id, - }).catch((e) => { - // eslint-disable-next-line no-console - console.debug(`Error setting up TelemetryContext: ${e.toString()}`); - }); + this.setupTelemetryContext(core.analytics, security, this.config.id); this.setupFullStory({ analytics: core.analytics, basePath: core.http.basePath }).catch((e) => // eslint-disable-next-line no-console @@ -213,9 +190,7 @@ export class CloudPlugin implements Plugin { }; } - public stop() { - this.appSubscription?.unsubscribe(); - } + public stop() {} /** * Determines if the current user should see links back to Cloud. @@ -272,48 +247,25 @@ export class CloudPlugin implements Plugin { * Set up the Analytics context providers. * @param analytics Core's Analytics service. The Setup contract. * @param security The security plugin. - * @param executionContextPromise Core's executionContext's start contract. - * @param esOrgId The Cloud Org ID. + * @param cloudId The Cloud Org ID. * @private */ - private async setupTelemetryContext({ - analytics, - security, - executionContextPromise, - cloudId, - }: SetupTelemetryContextDeps) { - // Some context providers can be moved to other places for better domain isolation. - // Let's use https://github.com/elastic/kibana/issues/125690 for that purpose. - analytics.registerContextProvider({ - name: 'kibana_version', - context$: of({ version: this.initializerContext.env.packageInfo.version }), - schema: { version: { type: 'keyword', _meta: { description: 'The version of Kibana' } } }, - }); + private setupTelemetryContext( + analytics: AnalyticsServiceSetup, + security?: Pick, + cloudId?: string + ) { + registerCloudDeploymentIdAnalyticsContext(analytics, cloudId); - analytics.registerContextProvider({ - name: 'cloud_org_id', - context$: of({ cloudId }), - schema: { - cloudId: { - type: 'keyword', - _meta: { description: 'The Cloud ID', optional: true }, - }, - }, - }); - - // This needs to be called synchronously to be sure that we populate the user ID soon enough to make sessions merging - // across domains work if (security) { analytics.registerContextProvider({ name: 'cloud_user_id', - context$: from(loadUserId({ getCurrentUser: security.authc.getCurrentUser })).pipe( - filter((userId): userId is string => Boolean(userId)), - exhaustMap(async (userId) => { - const { sha256 } = await import('js-sha256'); - // Join the cloud org id and the user to create a truly unique user id. - // The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs - return { userId: sha256(cloudId ? `${cloudId}:${userId}` : `${userId}`) }; - }) + context$: from(security.authc.getCurrentUser()).pipe( + map((user) => user.username), + // Join the cloud org id and the user to create a truly unique user id. + // The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs + map((userId) => ({ userId: sha256(cloudId ? `${cloudId}:${userId}` : `${userId}`) })), + catchError(() => of({ userId: undefined })) ), schema: { userId: { @@ -323,81 +275,6 @@ export class CloudPlugin implements Plugin { }, }); } - - const executionContext = await executionContextPromise; - analytics.registerContextProvider({ - name: 'execution_context', - context$: executionContext.context$.pipe( - // Update the current context every time it changes - map(({ name, page, id }) => ({ - pageName: `${compact([name, page]).join(':')}`, - applicationId: name ?? 'unknown', - page, - entityId: id, - })) - ), - schema: { - pageName: { - type: 'keyword', - _meta: { description: 'The name of the current page' }, - }, - page: { - type: 'keyword', - _meta: { description: 'The current page', optional: true }, - }, - applicationId: { - type: 'keyword', - _meta: { description: 'The id of the current application' }, - }, - entityId: { - type: 'keyword', - _meta: { - description: - 'The id of the current entity (dashboard, visualization, canvas, lens, etc)', - optional: true, - }, - }, - }, - }); - - analytics.registerEventType({ - eventType: 'Loaded Kibana', - schema: { - kibana_version: { - type: 'keyword', - _meta: { description: 'The version of Kibana', optional: true }, - }, - memory_js_heap_size_limit: { - type: 'long', - _meta: { description: 'The maximum size of the heap', optional: true }, - }, - memory_js_heap_size_total: { - type: 'long', - _meta: { description: 'The total size of the heap', optional: true }, - }, - memory_js_heap_size_used: { - type: 'long', - _meta: { description: 'The used size of the heap', optional: true }, - }, - }, - }); - - // Get performance information from the browser (non standard property - // @ts-expect-error 2339 - const memory = window.performance.memory; - let memoryInfo = {}; - if (memory) { - memoryInfo = { - memory_js_heap_size_limit: memory.jsHeapSizeLimit, - memory_js_heap_size_total: memory.totalJSHeapSize, - memory_js_heap_size_used: memory.usedJSHeapSize, - }; - } - - analytics.reportEvent('Loaded Kibana', { - kibana_version: this.initializerContext.env.packageInfo.version, - ...memoryInfo, - }); } private async setupChat({ http, security }: SetupChatDeps) { @@ -438,32 +315,6 @@ export class CloudPlugin implements Plugin { } } -/** @internal exported for testing */ -export const loadUserId = async ({ - getCurrentUser, -}: { - getCurrentUser: () => Promise; -}) => { - try { - const currentUser = await getCurrentUser().catch(() => undefined); - if (!currentUser) { - return undefined; - } - - // Log very defensively here so we can debug this easily if it breaks - if (!currentUser.username) { - // eslint-disable-next-line no-console - console.debug( - `[cloud.analytics] username not specified. User metadata: ${JSON.stringify( - currentUser.metadata - )}` - ); - } - - return currentUser.username; - } catch (e) { - // eslint-disable-next-line no-console - console.error(`[cloud.analytics] Error loading the current user: ${e.toString()}`, e); - return undefined; - } -}; +function sha256(str: string) { + return new Sha256().update(str, 'utf8').digest('hex'); +} diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index 284d37804be212..2cbb41531ecf54 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -8,6 +8,7 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server'; import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; +import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; import { CloudConfigType } from './config'; import { registerCloudUsageCollector } from './collectors'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; @@ -35,7 +36,7 @@ export interface CloudSetup { export class CloudPlugin implements Plugin { private readonly logger: Logger; private readonly config: CloudConfigType; - private isDev: boolean; + private readonly isDev: boolean; constructor(private readonly context: PluginInitializerContext) { this.logger = this.context.logger.get(); @@ -46,6 +47,7 @@ export class CloudPlugin implements Plugin { public setup(core: CoreSetup, { usageCollection, security }: PluginsSetup): CloudSetup { this.logger.debug('Setting up Cloud plugin'); const isCloudEnabled = getIsCloudEnabled(this.config.id); + registerCloudDeploymentIdAnalyticsContext(core.analytics, this.config.id); registerCloudUsageCollector(usageCollection, { isCloudEnabled }); if (this.config.full_story.enabled) { diff --git a/x-pack/plugins/licensing/common/register_analytics_context_provider.test.ts b/x-pack/plugins/licensing/common/register_analytics_context_provider.test.ts new file mode 100644 index 00000000000000..7edccfd319c91f --- /dev/null +++ b/x-pack/plugins/licensing/common/register_analytics_context_provider.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { firstValueFrom, ReplaySubject, Subject } from 'rxjs'; +import type { ILicense } from './types'; +import { registerAnalyticsContextProvider } from './register_analytics_context_provider'; + +describe('registerAnalyticsContextProvider', () => { + const analyticsClientMock = { + registerContextProvider: jest.fn(), + }; + + let license$: Subject; + + beforeEach(() => { + jest.clearAllMocks(); + license$ = new ReplaySubject(1); + registerAnalyticsContextProvider(analyticsClientMock, license$); + }); + + test('should register the analytics context provider', () => { + expect(analyticsClientMock.registerContextProvider).toHaveBeenCalledTimes(1); + }); + + test('emits a context value the moment license emits', async () => { + license$.next({ + uid: 'uid', + status: 'active', + isActive: true, + type: 'basic', + signature: 'signature', + isAvailable: true, + toJSON: jest.fn(), + getUnavailableReason: jest.fn(), + hasAtLeast: jest.fn(), + check: jest.fn(), + getFeature: jest.fn(), + }); + await expect( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[0][0].context$) + ).resolves.toEqual({ + license_id: 'uid', + license_status: 'active', + license_type: 'basic', + }); + }); +}); diff --git a/x-pack/plugins/licensing/common/register_analytics_context_provider.ts b/x-pack/plugins/licensing/common/register_analytics_context_provider.ts new file mode 100644 index 00000000000000..60f3fbbb3e6033 --- /dev/null +++ b/x-pack/plugins/licensing/common/register_analytics_context_provider.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Observable } from 'rxjs'; +import { map } from 'rxjs'; +import type { AnalyticsClient } from '@kbn/analytics-client'; +import type { ILicense } from './types'; + +export function registerAnalyticsContextProvider( + // Using `AnalyticsClient` from the package to be able to implement this method in the `common` dir. + analytics: Pick, + license$: Observable +) { + analytics.registerContextProvider({ + name: 'license info', + context$: license$.pipe( + map((license) => ({ + license_id: license.uid, + license_status: license.status, + license_type: license.type, + })) + ), + schema: { + license_id: { + type: 'keyword', + _meta: { description: 'The license ID', optional: true }, + }, + license_status: { + type: 'keyword', + _meta: { description: 'The license Status (active/invalid/expired)', optional: true }, + }, + license_type: { + type: 'keyword', + _meta: { + description: 'The license Type (basic/standard/gold/platinum/enterprise/trial)', + optional: true, + }, + }, + }, + }); +} diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index 9ef27e22657aff..3953a29a08214a 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -15,6 +15,7 @@ import { License } from '../common/license'; import { mountExpiredBanner } from './expired_banner'; import { FeatureUsageService } from './services'; import type { PublicLicenseJSON } from '../common/types'; +import { registerAnalyticsContextProvider } from '../common/register_analytics_context_provider'; export const licensingSessionStorageKey = 'xpack.licensing'; @@ -82,6 +83,8 @@ export class LicensingPlugin implements Plugin { if (license.isAvailable) { this.prevSignature = license.signature; diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 98dd1e7cbbb93e..aaeeb4e0580084 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -21,6 +21,7 @@ import { IClusterClient, } from '@kbn/core/server'; +import { registerAnalyticsContextProvider } from '../common/register_analytics_context_provider'; import { ILicense, PublicLicense, @@ -120,6 +121,8 @@ export class LicensingPlugin implements Plugin Date: Fri, 6 May 2022 11:41:51 +0200 Subject: [PATCH 47/83] [Security Solutions] Add risk tab to the user details page (#130256) * Remove traces of host from risk_score_over_time * Remove traces of host from risk_score_contributor and add query toggle * Add user details risk tab * Improve unit test coverage * Create User Risk Information flyout * run i18n_check.js --fix * Update users by risk table to link to users details risk tab * Improve Host Risk Flyout test coverage * Rename user and host risk tabs * Fix user risk CallOut showing when riskyUsersEnabled FF is disabled * Fix eslint warning --- .../integration/host_details/risk_tab.spec.ts | 2 +- .../cypress/screens/hosts/host_risk.ts | 2 +- .../public/app/deep_links/index.ts | 4 +- .../public/common/components/links/index.tsx | 26 ++- .../risk_score_over_time/index.test.tsx | 88 ++++++++ .../components/risk_score_over_time/index.tsx | 200 +++++++++++++++++ .../risk_score_over_time}/translations.ts | 9 +- .../index.test.tsx | 89 ++++++++ .../top_risk_score_contributors/index.tsx | 122 ++++++++++ .../translations.ts | 0 .../host_score_over_time/index.test.tsx | 53 ----- .../components/host_score_over_time/index.tsx | 210 ------------------ .../index.test.tsx | 150 ------------- .../top_host_score_contributors/index.tsx | 176 --------------- .../navigation/host_risk_tab_body.test.tsx | 87 ++++++++ .../pages/navigation/host_risk_tab_body.tsx | 82 +++++-- .../public/hosts/pages/translations.ts | 9 +- .../public/network/pages/details/utils.ts | 1 + .../risk_score/containers/all/index.tsx | 4 +- .../public/risk_score/containers/index.ts | 4 +- .../users/components/kpi_users/index.tsx | 34 ++- .../components/kpi_users/translations.ts | 22 ++ .../user_risk_information/index.test.tsx | 35 +++ .../user_risk_information/index.tsx | 132 +++++++++++ .../user_risk_information/translations.ts | 77 +++++++ .../user_risk_score_table/columns.tsx | 3 +- .../public/users/pages/constants.ts | 2 +- .../users/pages/details/details_tabs.tsx | 4 + .../public/users/pages/details/nav_tabs.tsx | 6 + .../public/users/pages/details/utils.ts | 1 + .../public/users/pages/nav_tabs.tsx | 12 +- .../user_risk_score_tab_body.test.tsx | 9 +- .../navigation/user_risk_tab_body.test.tsx | 86 +++++++ .../pages/navigation/user_risk_tab_body.tsx | 130 +++++++++++ .../public/users/pages/translations.ts | 16 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 38 files changed, 1247 insertions(+), 643 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.tsx rename x-pack/plugins/security_solution/public/{hosts/components/host_score_over_time => common/components/risk_score_over_time}/translations.ts (75%) create mode 100644 x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.tsx rename x-pack/plugins/security_solution/public/{hosts/components/top_host_score_contributors => common/components/top_risk_score_contributors}/translations.ts (100%) delete mode 100644 x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.test.tsx create mode 100644 x-pack/plugins/security_solution/public/users/components/kpi_users/translations.ts create mode 100644 x-pack/plugins/security_solution/public/users/components/user_risk_information/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/users/components/user_risk_information/index.tsx create mode 100644 x-pack/plugins/security_solution/public/users/components/user_risk_information/translations.ts create mode 100644 x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.test.tsx create mode 100644 x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx diff --git a/x-pack/plugins/security_solution/cypress/integration/host_details/risk_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/host_details/risk_tab.spec.ts index 3d26173a64c658..089e7584272f84 100644 --- a/x-pack/plugins/security_solution/cypress/integration/host_details/risk_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/host_details/risk_tab.spec.ts @@ -27,7 +27,7 @@ describe('risk tab', () => { cy.get('[data-test-subj="navigation-hostRisk"]').click(); waitForTableToLoad(); - cy.get('[data-test-subj="topHostScoreContributors"]') + cy.get('[data-test-subj="topRiskScoreContributors"]') .find(TABLE_ROWS) .within(() => { cy.get(TABLE_CELL).contains('Unusual Linux Username'); diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts index 9a691c82be7b81..50c06141c7ba99 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts @@ -5,7 +5,7 @@ * 2.0. */ -export const RULE_NAME = '[data-test-subj="topHostScoreContributors"] .euiTableCellContent'; +export const RULE_NAME = '[data-test-subj="topRiskScoreContributors"] .euiTableCellContent'; export const RISK_FLYOUT = '[data-test-subj="open-risk-information-flyout"] .euiFlyoutHeader'; diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index edfd46d167368e..e9735b8c0b903c 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -260,7 +260,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ { id: SecurityPageName.hostsRisk, title: i18n.translate('xpack.securitySolution.search.hosts.risk', { - defaultMessage: 'Hosts by risk', + defaultMessage: 'Host risk', }), path: `${HOSTS_PATH}/hostRisk`, experimentalKey: 'riskyHostsEnabled', @@ -355,7 +355,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ { id: SecurityPageName.usersRisk, title: i18n.translate('xpack.securitySolution.search.users.risk', { - defaultMessage: 'Users by risk', + defaultMessage: 'User risk', }), path: `${USERS_PATH}/userRisk`, experimentalKey: 'riskyUsersEnabled', diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index fb244c40d6e3d7..0dd0bba916cc92 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -38,9 +38,10 @@ import { isUrlInvalid } from '../../utils/validators'; import * as i18n from './translations'; import { SecurityPageName } from '../../../app/types'; -import { getUsersDetailsUrl } from '../link_to/redirect_to_users'; +import { getTabsOnUsersDetailsUrl, getUsersDetailsUrl } from '../link_to/redirect_to_users'; import { LinkAnchor, GenericLinkButton, PortContainer, Comma, LinkButton } from './helpers'; import { HostsTableType } from '../../../hosts/store/model'; +import { UsersTableType } from '../../../users/store/model'; export { LinkButton, LinkAnchor } from './helpers'; @@ -52,10 +53,11 @@ const UserDetailsLinkComponent: React.FC<{ /** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */ Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon; userName: string; + userTab?: UsersTableType; title?: string; isButton?: boolean; onClick?: (e: SyntheticEvent) => void; -}> = ({ children, Component, userName, isButton, onClick, title }) => { +}> = ({ children, Component, userName, isButton, onClick, title, userTab }) => { const encodedUserName = encodeURIComponent(userName); const { formatUrl, search } = useFormatUrl(SecurityPageName.users); @@ -65,17 +67,29 @@ const UserDetailsLinkComponent: React.FC<{ ev.preventDefault(); navigateToApp(APP_UI_ID, { deepLinkId: SecurityPageName.users, - path: getUsersDetailsUrl(encodedUserName, search), + path: userTab + ? getTabsOnUsersDetailsUrl(encodedUserName, userTab, search) + : getUsersDetailsUrl(encodedUserName, search), }); }, - [encodedUserName, navigateToApp, search] + [encodedUserName, navigateToApp, search, userTab] + ); + + const href = useMemo( + () => + formatUrl( + userTab + ? getTabsOnUsersDetailsUrl(encodedUserName, userTab) + : getUsersDetailsUrl(encodedUserName) + ), + [formatUrl, encodedUserName, userTab] ); return isButton ? ( @@ -85,7 +99,7 @@ const UserDetailsLinkComponent: React.FC<{ {children ? children : userName} diff --git a/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.test.tsx new file mode 100644 index 00000000000000..e07ff93b98c31f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { RiskScoreOverTime, scoreFormatter } from '.'; +import { TestProviders } from '../../mock'; +import { LineSeries } from '@elastic/charts'; + +const mockLineSeries = LineSeries as jest.Mock; + +jest.mock('@elastic/charts', () => { + const original = jest.requireActual('@elastic/charts'); + return { + ...original, + LineSeries: jest.fn().mockImplementation(() => <>), + }; +}); + +describe('Risk Score Over Time', () => { + it('renders', () => { + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('RiskScoreOverTime')).toBeInTheDocument(); + }); + + it('renders loader when loading', () => { + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('RiskScoreOverTime-loading')).toBeInTheDocument(); + }); + + describe('scoreFormatter', () => { + it('renders score formatted', () => { + render( + + + + ); + + const tickFormat = mockLineSeries.mock.calls[0][0].tickFormat; + + expect(tickFormat).toBe(scoreFormatter); + }); + + it('renders a formatted score', () => { + expect(scoreFormatter(3.000001)).toEqual('3'); + expect(scoreFormatter(3.4999)).toEqual('3'); + expect(scoreFormatter(3.51111)).toEqual('4'); + expect(scoreFormatter(3.9999)).toEqual('4'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.tsx b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.tsx new file mode 100644 index 00000000000000..a7a2dc676abc5b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.tsx @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useCallback } from 'react'; +import { + Chart, + LineSeries, + ScaleType, + Settings, + Axis, + Position, + AnnotationDomainType, + LineAnnotation, + TooltipValue, +} from '@elastic/charts'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiText, EuiPanel } from '@elastic/eui'; +import styled from 'styled-components'; +import { chartDefaultSettings, useTheme } from '../charts/common'; +import { useTimeZone } from '../../lib/kibana'; +import { histogramDateTimeFormatter } from '../utils'; +import { HeaderSection } from '../header_section'; +import { InspectButton, InspectButtonContainer } from '../inspect'; +import * as i18n from './translations'; +import { PreferenceFormattedDate } from '../formatted_date'; +import { RiskScore } from '../../../../common/search_strategy'; + +export interface RiskScoreOverTimeProps { + from: string; + to: string; + loading: boolean; + riskScore?: RiskScore[]; + queryId: string; + title: string; + toggleStatus: boolean; + toggleQuery?: (status: boolean) => void; +} + +const RISKY_THRESHOLD = 70; +const DEFAULT_CHART_HEIGHT = 250; + +const StyledEuiText = styled(EuiText)` + font-size: 9px; + font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; + margin-right: ${({ theme }) => theme.eui.paddingSizes.xs}; +`; + +const LoadingChart = styled(EuiLoadingChart)` + display: block; + text-align: center; +`; + +export const scoreFormatter = (d: number) => Math.round(d).toString(); + +const RiskScoreOverTimeComponent: React.FC = ({ + from, + to, + riskScore, + loading, + queryId, + title, + toggleStatus, + toggleQuery, +}) => { + const timeZone = useTimeZone(); + + const dataTimeFormatter = useMemo(() => histogramDateTimeFormatter([from, to]), [from, to]); + const headerFormatter = useCallback( + (tooltip: TooltipValue) => , + [] + ); + + const theme = useTheme(); + + const graphData = useMemo( + () => + riskScore + ?.map((data) => ({ + x: data['@timestamp'], + y: data.risk_stats.risk_score, + })) + .reverse() ?? [], + [riskScore] + ); + + return ( + + + + + + + {toggleStatus && ( + + + + )} + + + {toggleStatus && ( + + +
+ {loading ? ( + + ) : ( + + + + + + + {i18n.RISKY} + + } + /> + + )} +
+
+
+ )} +
+
+ ); +}; + +RiskScoreOverTimeComponent.displayName = 'RiskScoreOverTimeComponent'; +export const RiskScoreOverTime = React.memo(RiskScoreOverTimeComponent); +RiskScoreOverTime.displayName = 'RiskScoreOverTime'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/translations.ts b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/translations.ts similarity index 75% rename from x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/translations.ts rename to x-pack/plugins/security_solution/public/common/components/risk_score_over_time/translations.ts index 5e1b4ca7410a83..a3d32f5e5d59fc 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/translations.ts @@ -7,14 +7,7 @@ import { i18n } from '@kbn/i18n'; -export const HOST_RISK_SCORE_OVER_TIME = i18n.translate( - 'xpack.securitySolution.hosts.hostScoreOverTime.title', - { - defaultMessage: 'Host risk score over time', - } -); - -export const HOST_RISK_THRESHOLD = i18n.translate( +export const RISK_THRESHOLD = i18n.translate( 'xpack.securitySolution.hosts.hostScoreOverTime.riskyThresholdHeader', { defaultMessage: 'Risky threshold', diff --git a/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.test.tsx new file mode 100644 index 00000000000000..4cc6812772b819 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { TopRiskScoreContributors } from '.'; +import { TestProviders } from '../../mock'; +import { RuleRisk } from '../../../../common/search_strategy'; + +jest.mock('../../containers/query_toggle'); +jest.mock('../../../risk_score/containers'); + +const testProps = { + riskScore: [], + setQuery: jest.fn(), + deleteQuery: jest.fn(), + hostName: 'test-host-name', + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', + loading: false, + toggleStatus: true, + queryId: 'test-query-id', +}; + +describe('Top Risk Score Contributors', () => { + it('renders', () => { + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('topRiskScoreContributors')).toBeInTheDocument(); + }); + + it('renders sorted items', () => { + const ruleRisk: RuleRisk[] = [ + { + rule_name: 'third', + rule_risk: 10, + rule_id: '3', + }, + { + rule_name: 'first', + rule_risk: 99, + rule_id: '1', + }, + { + rule_name: 'second', + rule_risk: 55, + rule_id: '2', + }, + ]; + + const { queryAllByRole } = render( + + + + ); + + expect(queryAllByRole('row')[1]).toHaveTextContent('first'); + expect(queryAllByRole('row')[2]).toHaveTextContent('second'); + expect(queryAllByRole('row')[3]).toHaveTextContent('third'); + }); + + describe('toggleStatus', () => { + test('toggleStatus=true, render components', () => { + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('topRiskScoreContributors-table')).toBeTruthy(); + }); + + test('toggleStatus=false, do not render components', () => { + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('topRiskScoreContributors-table')).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.tsx new file mode 100644 index 00000000000000..7ee2ae5e214131 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiInMemoryTable, + EuiTableFieldDataColumnType, +} from '@elastic/eui'; + +import { HeaderSection } from '../header_section'; +import { InspectButton, InspectButtonContainer } from '../inspect'; +import * as i18n from './translations'; + +import { RuleRisk } from '../../../../common/search_strategy'; + +import { RuleLink } from '../../../detections/pages/detection_engine/rules/all/use_columns'; + +export interface TopRiskScoreContributorsProps { + loading: boolean; + rules?: RuleRisk[]; + queryId: string; + toggleStatus: boolean; + toggleQuery?: (status: boolean) => void; +} +interface TableItem { + rank: number; + name: string; + id: string; +} + +const columns: Array> = [ + { + name: i18n.RANK_TITLE, + field: 'rank', + width: '45px', + align: 'right', + }, + { + name: i18n.RULE_NAME_TITLE, + field: 'name', + sortable: true, + truncateText: true, + render: (value: TableItem['name'], { id }: TableItem) => + id ? : value, + }, +]; + +const PAGE_SIZE = 5; + +const TopRiskScoreContributorsComponent: React.FC = ({ + rules = [], + loading, + queryId, + toggleStatus, + toggleQuery, +}) => { + const items = useMemo(() => { + return rules + ?.sort((a, b) => b.rule_risk - a.rule_risk) + .map(({ rule_name: name, rule_id: id }, i) => ({ rank: i + 1, name, id })); + }, [rules]); + + const tablePagination = useMemo( + () => ({ + showPerPageOptions: false, + pageSize: PAGE_SIZE, + totalItemCount: items.length, + }), + [items.length] + ); + + return ( + + + + + + + {toggleStatus && ( + + + + )} + + + {toggleStatus && ( + + + + + + )} + + + ); +}; + +export const TopRiskScoreContributors = React.memo(TopRiskScoreContributorsComponent); +TopRiskScoreContributors.displayName = 'TopRiskScoreContributors'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/translations.ts b/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/translations.ts rename to x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/translations.ts diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.test.tsx deleted file mode 100644 index a96ffb577d90c6..00000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React from 'react'; -import { HostRiskScoreOverTime } from '.'; -import { TestProviders } from '../../../common/mock'; -import { useHostRiskScore } from '../../../risk_score/containers'; - -jest.mock('../../../risk_score/containers'); -const useHostRiskScoreMock = useHostRiskScore as jest.Mock; - -describe('Host Risk Flyout', () => { - it('renders', () => { - useHostRiskScoreMock.mockReturnValueOnce([false, { data: [], isModuleEnabled: true }]); - - const { queryByTestId } = render( - - - - ); - - expect(queryByTestId('hostRiskScoreOverTime')).toBeInTheDocument(); - }); - - it('renders loader when HostsRiskScore is laoding', () => { - useHostRiskScoreMock.mockReturnValueOnce([true, { data: [], isModuleEnabled: true }]); - - const { queryByTestId } = render( - - - - ); - - expect(queryByTestId('HostRiskScoreOverTime-loading')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx deleted file mode 100644 index 52a840e857ffff..00000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo, useCallback } from 'react'; -import { - Chart, - LineSeries, - ScaleType, - Settings, - Axis, - Position, - AnnotationDomainType, - LineAnnotation, - TooltipValue, -} from '@elastic/charts'; -import { euiThemeVars } from '@kbn/ui-theme'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiText, EuiPanel } from '@elastic/eui'; -import styled from 'styled-components'; -import { chartDefaultSettings, useTheme } from '../../../common/components/charts/common'; -import { useTimeZone } from '../../../common/lib/kibana'; -import { histogramDateTimeFormatter } from '../../../common/components/utils'; -import { HeaderSection } from '../../../common/components/header_section'; -import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; -import * as i18n from './translations'; -import { PreferenceFormattedDate } from '../../../common/components/formatted_date'; -import { useQueryInspector } from '../../../common/components/page/manage_query'; -import { HostsComponentsQueryProps } from '../../pages/navigation/types'; -import { buildHostNamesFilter } from '../../../../common/search_strategy/security_solution/risk_score'; -import { HostRiskScoreQueryId, useHostRiskScore } from '../../../risk_score/containers'; - -export interface HostRiskScoreOverTimeProps - extends Pick { - hostName: string; - from: string; - to: string; -} - -const RISKY_THRESHOLD = 70; -const DEFAULT_CHART_HEIGHT = 250; -const QUERY_ID = HostRiskScoreQueryId.HOST_RISK_SCORE_OVER_TIME; - -const StyledEuiText = styled(EuiText)` - font-size: 9px; - font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; - margin-right: ${({ theme }) => theme.eui.paddingSizes.xs}; -`; - -const LoadingChart = styled(EuiLoadingChart)` - display: block; - text-align: center; -`; - -const HostRiskScoreOverTimeComponent: React.FC = ({ - hostName, - from, - to, - setQuery, - deleteQuery, -}) => { - const timeZone = useTimeZone(); - - const dataTimeFormatter = useMemo(() => histogramDateTimeFormatter([from, to]), [from, to]); - const scoreFormatter = useCallback((d: number) => Math.round(d).toString(), []); - const headerFormatter = useCallback( - (tooltip: TooltipValue) => , - [] - ); - - const timerange = useMemo( - () => ({ - from, - to, - }), - [from, to] - ); - const theme = useTheme(); - - const [loading, { data, refetch, inspect }] = useHostRiskScore({ - filterQuery: hostName ? buildHostNamesFilter([hostName]) : undefined, - onlyLatest: false, - timerange, - }); - - const graphData = useMemo( - () => - data - ?.map((hostRisk) => ({ - x: hostRisk['@timestamp'], - y: hostRisk.risk_stats.risk_score, - })) - .reverse() ?? [], - [data] - ); - - useQueryInspector({ - queryId: QUERY_ID, - loading, - refetch, - setQuery, - deleteQuery, - inspect, - }); - - return ( - - - - - - - - - - - - - - -
- {loading ? ( - - ) : ( - - - - - - - {i18n.RISKY} - - } - /> - - )} -
-
-
-
-
- ); -}; - -HostRiskScoreOverTimeComponent.displayName = 'HostRiskScoreOverTimeComponent'; -export const HostRiskScoreOverTime = React.memo(HostRiskScoreOverTimeComponent); -HostRiskScoreOverTime.displayName = 'HostRiskScoreOverTime'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx deleted file mode 100644 index 5ff8696ae5be35..00000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render, fireEvent } from '@testing-library/react'; -import React from 'react'; -import { TopHostScoreContributors } from '.'; -import { TestProviders } from '../../../common/mock'; -import { useHostRiskScore } from '../../../risk_score/containers'; -import { useQueryToggle } from '../../../common/containers/query_toggle'; - -jest.mock('../../../common/containers/query_toggle'); -jest.mock('../../../risk_score/containers'); -const useHostRiskScoreMock = useHostRiskScore as jest.Mock; -const testProps = { - setQuery: jest.fn(), - deleteQuery: jest.fn(), - hostName: 'test-host-name', - from: '2020-07-07T08:20:18.966Z', - to: '2020-07-08T08:20:18.966Z', -}; -describe('Host Risk Flyout', () => { - const mockUseQueryToggle = useQueryToggle as jest.Mock; - const mockSetToggle = jest.fn(); - beforeEach(() => { - jest.clearAllMocks(); - mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); - }); - it('renders', () => { - useHostRiskScoreMock.mockReturnValueOnce([ - true, - { - data: [], - isModuleEnabled: true, - }, - ]); - - const { queryByTestId } = render( - - - - ); - - expect(queryByTestId('topHostScoreContributors')).toBeInTheDocument(); - }); - - it('renders sorted items', () => { - useHostRiskScoreMock.mockReturnValueOnce([ - true, - { - data: [ - { - risk_stats: { - rule_risks: [ - { - rule_name: 'third', - rule_risk: '10', - }, - { - rule_name: 'first', - rule_risk: '99', - }, - { - rule_name: 'second', - rule_risk: '55', - }, - ], - }, - }, - ], - isModuleEnabled: true, - }, - ]); - - const { queryAllByRole } = render( - - - - ); - - expect(queryAllByRole('row')[1]).toHaveTextContent('first'); - expect(queryAllByRole('row')[2]).toHaveTextContent('second'); - expect(queryAllByRole('row')[3]).toHaveTextContent('third'); - }); - - describe('toggleQuery', () => { - beforeEach(() => { - useHostRiskScoreMock.mockReturnValue([ - true, - { - data: [], - isModuleEnabled: true, - }, - ]); - }); - - test('toggleQuery updates toggleStatus', () => { - const { getByTestId } = render( - - - - ); - expect(useHostRiskScoreMock.mock.calls[0][0].skip).toEqual(false); - fireEvent.click(getByTestId('query-toggle-header')); - expect(mockSetToggle).toBeCalledWith(false); - expect(useHostRiskScoreMock.mock.calls[1][0].skip).toEqual(true); - }); - - test('toggleStatus=true, do not skip', () => { - render( - - - - ); - expect(useHostRiskScoreMock.mock.calls[0][0].skip).toEqual(false); - }); - - test('toggleStatus=true, render components', () => { - const { queryByTestId } = render( - - - - ); - expect(queryByTestId('topHostScoreContributors-table')).toBeTruthy(); - }); - - test('toggleStatus=false, do not render components', () => { - mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); - const { queryByTestId } = render( - - - - ); - expect(queryByTestId('topHostScoreContributors-table')).toBeFalsy(); - }); - - test('toggleStatus=false, skip', () => { - mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); - render( - - - - ); - expect(useHostRiskScoreMock.mock.calls[0][0].skip).toEqual(true); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx deleted file mode 100644 index ceb4394619fc5e..00000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiInMemoryTable, - EuiTableFieldDataColumnType, -} from '@elastic/eui'; - -import { Direction } from '@kbn/timelines-plugin/common'; -import { HeaderSection } from '../../../common/components/header_section'; -import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; -import * as i18n from './translations'; - -import { buildHostNamesFilter, RiskScoreFields } from '../../../../common/search_strategy'; - -import { useQueryInspector } from '../../../common/components/page/manage_query'; -import { HostsComponentsQueryProps } from '../../pages/navigation/types'; - -import { RuleLink } from '../../../detections/pages/detection_engine/rules/all/use_columns'; -import { HostRiskScoreQueryId, useHostRiskScore } from '../../../risk_score/containers'; -import { useQueryToggle } from '../../../common/containers/query_toggle'; - -export interface TopHostScoreContributorsProps - extends Pick { - hostName: string; - from: string; - to: string; -} -interface TableItem { - rank: number; - name: string; - id: string; -} - -const columns: Array> = [ - { - name: i18n.RANK_TITLE, - field: 'rank', - width: '45px', - align: 'right', - }, - { - name: i18n.RULE_NAME_TITLE, - field: 'name', - sortable: true, - truncateText: true, - render: (value: TableItem['name'], { id }: TableItem) => - id ? : value, - }, -]; - -const PAGE_SIZE = 5; -const QUERY_ID = HostRiskScoreQueryId.TOP_HOST_SCORE_CONTRIBUTORS; - -const TopHostScoreContributorsComponent: React.FC = ({ - hostName, - from, - to, - setQuery, - deleteQuery, -}) => { - const timerange = useMemo( - () => ({ - from, - to, - }), - [from, to] - ); - - const sort = useMemo(() => ({ field: RiskScoreFields.timestamp, direction: Direction.desc }), []); - - const { toggleStatus, setToggleStatus } = useQueryToggle(QUERY_ID); - const [querySkip, setQuerySkip] = useState(!toggleStatus); - useEffect(() => { - setQuerySkip(!toggleStatus); - }, [toggleStatus]); - - const toggleQuery = useCallback( - (status: boolean) => { - setToggleStatus(status); - // toggle on = skipQuery false - setQuerySkip(!status); - }, - [setQuerySkip, setToggleStatus] - ); - - const [loading, { data, refetch, inspect }] = useHostRiskScore({ - filterQuery: hostName ? buildHostNamesFilter([hostName]) : undefined, - timerange, - onlyLatest: false, - sort, - skip: querySkip, - pagination: { - querySize: 1, - cursorStart: 0, - }, - }); - - const items = useMemo(() => { - const rules = data && data.length > 0 ? data[0].risk_stats.rule_risks : []; - - return rules - .sort((a, b) => b.rule_risk - a.rule_risk) - .map(({ rule_name: name, rule_id: id }, i) => ({ rank: i + 1, name, id })); - }, [data]); - - const tablePagination = useMemo( - () => ({ - showPerPageOptions: false, - pageSize: PAGE_SIZE, - totalItemCount: items.length, - }), - [items.length] - ); - - useQueryInspector({ - queryId: QUERY_ID, - loading, - refetch, - setQuery, - deleteQuery, - inspect, - }); - - return ( - - - - - - - {toggleStatus && ( - - - - )} - - - {toggleStatus && ( - - - - - - )} - - - ); -}; - -export const TopHostScoreContributors = React.memo(TopHostScoreContributorsComponent); -TopHostScoreContributors.displayName = 'TopHostScoreContributors'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.test.tsx new file mode 100644 index 00000000000000..bab6809afc6f67 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../common/mock'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; + +import { useHostRiskScore } from '../../../risk_score/containers'; +import { HostRiskTabBody } from './host_risk_tab_body'; +import { HostsType } from '../../store/model'; + +jest.mock('../../../risk_score/containers'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('Host query tab body', () => { + const mockUseUserRiskScore = useHostRiskScore as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + hostName: 'testUser', + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: HostsType.page, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockUseUserRiskScore.mockReturnValue([ + false, + { + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + refetch: jest.fn(), + isModuleEnabled: true, + }, + ]); + }); + + it("doesn't skip when both toggleStatus are true", () => { + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() }); + + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(false); + }); + + it("doesn't skip when at least one toggleStatus is true", () => { + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() }); + + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(false); + }); + + it('does skip when both toggleStatus are false', () => { + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() }); + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() }); + + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx index cebcc0ee855eaf..b23ebb7de9bef0 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx @@ -6,19 +6,26 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { HostRiskScoreOverTime } from '../../components/host_score_over_time'; -import { TopHostScoreContributors } from '../../components/top_host_score_contributors'; + import { HostsComponentsQueryProps } from './types'; import * as i18n from '../translations'; import { useRiskyHostsDashboardButtonHref } from '../../../overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href'; import { HostRiskInformationButtonEmpty } from '../../components/host_risk_information'; +import { HostRiskScoreQueryId, useHostRiskScore } from '../../../risk_score/containers'; +import { buildHostNamesFilter } from '../../../../common/search_strategy'; +import { useQueryInspector } from '../../../common/components/page/manage_query'; +import { RiskScoreOverTime } from '../../../common/components/risk_score_over_time'; +import { TopRiskScoreContributors } from '../../../common/components/top_risk_score_contributors'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const StyledEuiFlexGroup = styled(EuiFlexGroup)` margin-top: ${({ theme }) => theme.eui.paddingSizes.l}; `; +const QUERY_ID = HostRiskScoreQueryId.HOST_DETAILS_RISK_SCORE; + const HostRiskTabBodyComponent: React.FC< Pick & { hostName: string; @@ -26,25 +33,74 @@ const HostRiskTabBodyComponent: React.FC< > = ({ hostName, startDate, endDate, setQuery, deleteQuery }) => { const { buttonHref } = useRiskyHostsDashboardButtonHref(startDate, endDate); + const timerange = useMemo( + () => ({ + from: startDate, + to: endDate, + }), + [startDate, endDate] + ); + + const { toggleStatus: overTimeToggleStatus, setToggleStatus: setOverTimeToggleStatus } = + useQueryToggle(`${QUERY_ID} overTime`); + const { toggleStatus: contributorsToggleStatus, setToggleStatus: setContributorsToggleStatus } = + useQueryToggle(`${QUERY_ID} contributors`); + + const [loading, { data, refetch, inspect }] = useHostRiskScore({ + filterQuery: hostName ? buildHostNamesFilter([hostName]) : undefined, + onlyLatest: false, + skip: !overTimeToggleStatus && !contributorsToggleStatus, + timerange, + }); + + useQueryInspector({ + queryId: QUERY_ID, + loading, + refetch, + setQuery, + deleteQuery, + inspect, + }); + + const toggleContributorsQuery = useCallback( + (status: boolean) => { + setContributorsToggleStatus(status); + }, + [setContributorsToggleStatus] + ); + + const toggleOverTimeQuery = useCallback( + (status: boolean) => { + setOverTimeToggleStatus(status); + }, + [setOverTimeToggleStatus] + ); + + const rules = data && data.length > 0 ? data[data.length - 1].risk_stats.rule_risks : []; + return ( <> - + - diff --git a/x-pack/plugins/security_solution/public/hosts/pages/translations.ts b/x-pack/plugins/security_solution/public/hosts/pages/translations.ts index 8b92ef035405fb..3e6b23a5210261 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/translations.ts @@ -60,7 +60,7 @@ export const NAVIGATION_ALERTS_TITLE = i18n.translate( export const NAVIGATION_HOST_RISK_TITLE = i18n.translate( 'xpack.securitySolution.hosts.navigation.hostRisk', { - defaultMessage: 'Hosts by risk', + defaultMessage: 'Host risk', } ); @@ -97,3 +97,10 @@ export const VIEW_DASHBOARD_BUTTON = i18n.translate( defaultMessage: 'View source dashboard', } ); + +export const HOST_RISK_SCORE_OVER_TIME = i18n.translate( + 'xpack.securitySolution.hosts.navigaton.hostScoreOverTimeTitle', + { + defaultMessage: 'Host risk score over time', + } +); diff --git a/x-pack/plugins/security_solution/public/network/pages/details/utils.ts b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts index 98094665cbcd27..044c1d22a63488 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts @@ -42,6 +42,7 @@ export const getBreadcrumbs = ( }), }, ]; + if (params.detailName != null) { breadcrumb = [ ...breadcrumb, diff --git a/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx b/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx index 8ddd081088686b..368bf2589d5c75 100644 --- a/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx +++ b/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx @@ -103,7 +103,7 @@ export const useUserRiskScore = ({ const spaceId = useSpaceId(); const defaultIndex = spaceId ? getUserRiskIndex(spaceId, onlyLatest) : undefined; - const usersFeatureEnabled = useIsExperimentalFeatureEnabled('usersEnabled'); + const riskyUsersFeatureEnabled = useIsExperimentalFeatureEnabled('riskyUsersEnabled'); return useRiskScore({ timerange, onlyLatest, @@ -111,7 +111,7 @@ export const useUserRiskScore = ({ sort, skip, pagination, - featureEnabled: usersFeatureEnabled, + featureEnabled: riskyUsersFeatureEnabled, defaultIndex, }); }; diff --git a/x-pack/plugins/security_solution/public/risk_score/containers/index.ts b/x-pack/plugins/security_solution/public/risk_score/containers/index.ts index 089c88aa9be371..ffe964b974776a 100644 --- a/x-pack/plugins/security_solution/public/risk_score/containers/index.ts +++ b/x-pack/plugins/security_solution/public/risk_score/containers/index.ts @@ -12,12 +12,12 @@ export * from './kpi'; export const enum UserRiskScoreQueryId { USERS_BY_RISK = 'UsersByRisk', + USER_DETAILS_RISK_SCORE = 'UserDetailsRiskScore', } export const enum HostRiskScoreQueryId { DEFAULT = 'HostRiskScore', - HOST_RISK_SCORE_OVER_TIME = 'HostRiskScoreOverTimeQuery', - TOP_HOST_SCORE_CONTRIBUTORS = 'TopHostScoreContributorsQuery', + HOST_DETAILS_RISK_SCORE = 'HostDetailsRiskScore', OVERVIEW_RISKY_HOSTS = 'OverviewRiskyHosts', HOSTS_BY_RISK = 'HostsByRisk', } diff --git a/x-pack/plugins/security_solution/public/users/components/kpi_users/index.tsx b/x-pack/plugins/security_solution/public/users/components/kpi_users/index.tsx index 11d577a037ddb7..768248cf9b6acd 100644 --- a/x-pack/plugins/security_solution/public/users/components/kpi_users/index.tsx +++ b/x-pack/plugins/security_solution/public/users/components/kpi_users/index.tsx @@ -6,17 +6,47 @@ */ import React from 'react'; -import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import { UsersKpiProps } from './types'; import { HostsKpiAuthentications } from '../../../hosts/components/kpi_hosts/authentications'; import { TotalUsersKpi } from './total_users'; +import { useUserRiskScore } from '../../../risk_score/containers'; +import { CallOutSwitcher } from '../../../common/components/callouts'; +import * as i18n from './translations'; export const UsersKpiComponent = React.memo( ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => { + const [_, { isModuleEnabled }] = useUserRiskScore({}); + return ( <> + {isModuleEnabled === false && ( + <> + + {/* + TODO PENDING ON USER RISK DOCUMENTATION} + */} + {i18n.LEARN_MORE} {i18n.USER_RISK_DATA} + {/* */} + + + ), + }} + /> + + + )} ( } ); -UsersKpiComponent.displayName = 'HostsKpiComponent'; +UsersKpiComponent.displayName = 'UsersKpiComponent'; diff --git a/x-pack/plugins/security_solution/public/users/components/kpi_users/translations.ts b/x-pack/plugins/security_solution/public/users/components/kpi_users/translations.ts new file mode 100644 index 00000000000000..8315b6dc21c192 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/components/kpi_users/translations.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; + +export const ENABLE_USER_RISK_TEXT = i18n.translate( + 'xpack.securitySolution.kpiUser.enableUserRiskText', + { + defaultMessage: 'Enable user risk module to see more data', + } +); + +export const LEARN_MORE = i18n.translate('xpack.securitySolution.kpiUser.learnMore', { + defaultMessage: 'Learn more about', +}); + +export const USER_RISK_DATA = i18n.translate('xpack.securitySolution.kpiUser.userRiskData', { + defaultMessage: 'user risk data', +}); diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.test.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.test.tsx new file mode 100644 index 00000000000000..764d732fa28982 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, fireEvent } from '@testing-library/react'; +import React from 'react'; +import { UserRiskInformationButtonEmpty } from '.'; +import { TestProviders } from '../../../common/mock'; + +describe('User Risk Flyout', () => { + describe('UserRiskInformationButtonEmpty', () => { + it('renders', () => { + const { queryByTestId } = render(); + + expect(queryByTestId('open-risk-information-flyout-trigger')).toBeInTheDocument(); + }); + }); + + it('opens and displays table with 5 rows', () => { + const NUMBER_OF_ROWS = 1 + 5; // 1 header row + 5 severity rows + const { getByTestId, queryByTestId, queryAllByRole } = render( + + + + ); + + fireEvent.click(getByTestId('open-risk-information-flyout-trigger')); + + expect(queryByTestId('risk-information-table')).toBeInTheDocument(); + expect(queryAllByRole('row')).toHaveLength(NUMBER_OF_ROWS); + }); +}); diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.tsx new file mode 100644 index 00000000000000..6ae647544d965b --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + useGeneratedHtmlId, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiText, + EuiTitle, + EuiBasicTable, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, + EuiButton, + EuiSpacer, + EuiBasicTableColumn, + EuiButtonEmpty, +} from '@elastic/eui'; + +import React from 'react'; + +import * as i18n from './translations'; +import { useOnOpenCloseHandler } from '../../../helper_hooks'; +import { RiskScore } from '../../../common/components/severity/common'; +import { RiskSeverity } from '../../../../common/search_strategy'; + +const tableColumns: Array> = [ + { + field: 'classification', + name: i18n.INFORMATION_CLASSIFICATION_HEADER, + render: (riskScore?: RiskSeverity) => { + if (riskScore != null) { + return ; + } + }, + }, + { + field: 'range', + name: i18n.INFORMATION_RISK_HEADER, + }, +]; + +interface TableItem { + range?: string; + classification: RiskSeverity; +} + +const tableItems: TableItem[] = [ + { classification: RiskSeverity.critical, range: i18n.CRITICAL_RISK_DESCRIPTION }, + { classification: RiskSeverity.high, range: '70 - 90 ' }, + { classification: RiskSeverity.moderate, range: '40 - 70' }, + { classification: RiskSeverity.low, range: '20 - 40' }, + { classification: RiskSeverity.unknown, range: i18n.UNKNOWN_RISK_DESCRIPTION }, +]; + +export const USER_RISK_INFO_BUTTON_CLASS = 'UserRiskInformation__button'; + +export const UserRiskInformationButtonEmpty = () => { + const [isFlyoutVisible, handleOnOpen, handleOnClose] = useOnOpenCloseHandler(); + + return ( + <> + + {i18n.INFO_BUTTON_TEXT} + + {isFlyoutVisible && } + + ); +}; + +const UserRiskInformationFlyout = ({ handleOnClose }: { handleOnClose: () => void }) => { + const simpleFlyoutTitleId = useGeneratedHtmlId({ + prefix: 'UserRiskInformation', + }); + + return ( + + + +

{i18n.TITLE}

+
+
+ + +

{i18n.INTRODUCTION}

+

{i18n.EXPLANATION_MESSAGE}

+
+ + + {/* TODO PENDING ON USER RISK DOCUMENTATION + + + + + ), + }} + /> */} +
+ + + + + {i18n.CLOSE_BUTTON_LTEXT} + + + +
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_information/translations.ts b/x-pack/plugins/security_solution/public/users/components/user_risk_information/translations.ts new file mode 100644 index 00000000000000..dbf4ad96e486c5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_information/translations.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const INFORMATION_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.informationAriaLabel', + { + defaultMessage: 'Information', + } +); + +export const INFORMATION_CLASSIFICATION_HEADER = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.classificationHeader', + { + defaultMessage: 'Classification', + } +); + +export const INFORMATION_RISK_HEADER = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.riskHeader', + { + defaultMessage: 'User risk score range', + } +); + +export const UNKNOWN_RISK_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.unknownRiskDescription', + { + defaultMessage: 'Less than 20', + } +); + +export const CRITICAL_RISK_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.criticalRiskDescription', + { + defaultMessage: '90 and above', + } +); + +export const TITLE = i18n.translate('xpack.securitySolution.users.userRiskInformation.title', { + defaultMessage: 'How is user risk calculated?', +}); + +export const INTRODUCTION = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.introduction', + { + defaultMessage: + 'The User Risk Score capability surfaces risky users from within your environment.', + } +); + +export const EXPLANATION_MESSAGE = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.explanation', + { + defaultMessage: + 'This feature utilizes a transform, with a scripted metric aggregation to calculate user risk scores based on detection rule alerts with an "open" status, within a 5 day time window. The transform runs hourly to keep the score updated as new detection rule alerts stream in.', + } +); + +export const CLOSE_BUTTON_LTEXT = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.closeBtn', + { + defaultMessage: 'Close', + } +); + +export const INFO_BUTTON_TEXT = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.buttonLabel', + { + defaultMessage: 'How is risk score calculated?', + } +); diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.tsx index c3b26aa1e44d32..3ea4d6a14c247f 100644 --- a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.tsx @@ -22,6 +22,7 @@ import * as i18n from './translations'; import { RiskScore } from '../../../common/components/severity/common'; import { RiskSeverity } from '../../../../common/search_strategy'; import { UserDetailsLink } from '../../../common/components/links'; +import { UsersTableType } from '../../store/model'; export const getUserRiskScoreColumns = ({ dispatchSeverityUpdate, @@ -55,7 +56,7 @@ export const getUserRiskScoreColumns = ({ ) : ( - + ) } /> diff --git a/x-pack/plugins/security_solution/public/users/pages/constants.ts b/x-pack/plugins/security_solution/public/users/pages/constants.ts index 44d3b0ba83e1f5..e8b9e4a4118a18 100644 --- a/x-pack/plugins/security_solution/public/users/pages/constants.ts +++ b/x-pack/plugins/security_solution/public/users/pages/constants.ts @@ -12,4 +12,4 @@ export const usersDetailsPagePath = `${USERS_PATH}/:detailName`; export const usersTabPath = `${USERS_PATH}/:tabName(${UsersTableType.allUsers}|${UsersTableType.authentications}|${UsersTableType.anomalies}|${UsersTableType.risk}|${UsersTableType.events}|${UsersTableType.alerts})`; -export const usersDetailsTabPath = `${usersDetailsPagePath}/:tabName(${UsersTableType.authentications}|${UsersTableType.anomalies}|${UsersTableType.events}|${UsersTableType.alerts})`; +export const usersDetailsTabPath = `${usersDetailsPagePath}/:tabName(${UsersTableType.authentications}|${UsersTableType.anomalies}|${UsersTableType.events}|${UsersTableType.alerts}|${UsersTableType.risk})`; diff --git a/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx index d3c3a4607b39cd..22b394f41bfaf6 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx @@ -21,6 +21,7 @@ import { EventsQueryTabBody } from '../../../common/components/events_tab/events import { AlertsView } from '../../../common/components/alerts_viewer'; import { userNameExistsFilter } from './helpers'; import { AuthenticationsQueryTabBody } from '../navigation'; +import { UserRiskTabBody } from '../navigation/user_risk_tab_body'; export const UsersDetailsTabs = React.memo( ({ @@ -107,6 +108,9 @@ export const UsersDetailsTabs = React.memo( {...tabProps} /> + + + ); } diff --git a/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx index ee070f749925e0..9f12d8824f8178 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx @@ -43,6 +43,12 @@ export const navTabsUsersDetails = ( href: getTabsOnUsersDetailsUrl(userName, UsersTableType.alerts), disabled: false, }, + [UsersTableType.risk]: { + id: UsersTableType.risk, + name: i18n.NAVIGATION_RISK_TITLE, + href: getTabsOnUsersDetailsUrl(userName, UsersTableType.risk), + disabled: false, + }, }; return hasMlUserPermissions diff --git a/x-pack/plugins/security_solution/public/users/pages/details/utils.ts b/x-pack/plugins/security_solution/public/users/pages/details/utils.ts index 4b85d0f59314f5..26ed75997a85df 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/users/pages/details/utils.ts @@ -27,6 +27,7 @@ const TabNameMappedToI18nKey: Record = { [UsersTableType.risk]: i18n.NAVIGATION_RISK_TITLE, [UsersTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, [UsersTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, + [UsersTableType.risk]: i18n.NAVIGATION_RISK_TITLE, }; export const getBreadcrumbs = ( diff --git a/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx index 3097fdeb604f33..046b8b70881252 100644 --- a/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx @@ -38,12 +38,6 @@ export const navTabsUsers = ( href: getTabsOnUsersUrl(UsersTableType.anomalies), disabled: false, }, - [UsersTableType.risk]: { - id: UsersTableType.risk, - name: i18n.NAVIGATION_RISK_TITLE, - href: getTabsOnUsersUrl(UsersTableType.risk), - disabled: false, - }, [UsersTableType.events]: { id: UsersTableType.events, name: i18n.NAVIGATION_EVENTS_TITLE, @@ -56,6 +50,12 @@ export const navTabsUsers = ( href: getTabsOnUsersUrl(UsersTableType.alerts), disabled: false, }, + [UsersTableType.risk]: { + id: UsersTableType.risk, + name: i18n.NAVIGATION_RISK_TITLE, + href: getTabsOnUsersUrl(UsersTableType.risk), + disabled: false, + }, }; if (!hasMlUserPermissions) { diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx index 6b5ec66f864bb7..10c85be1b72f7b 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx @@ -29,23 +29,22 @@ describe('All users query tab body', () => { endDate: '2019-06-25T06:31:59.345Z', type: UsersType.page, }; + beforeEach(() => { jest.clearAllMocks(); mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseUserRiskScore.mockReturnValue([ false, { - authentications: [], - id: '123', inspect: { dsl: [], response: [], }, isInspected: false, totalCount: 0, - pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, - loadPage: jest.fn(), refetch: jest.fn(), + isModuleEnabled: true, }, ]); mockUseUserRiskScoreKpi.mockReturnValue({ @@ -59,6 +58,7 @@ describe('All users query tab body', () => { }, }); }); + it('toggleStatus=true, do not skip', () => { render( @@ -68,6 +68,7 @@ describe('All users query tab body', () => { expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(false); expect(mockUseUserRiskScoreKpi.mock.calls[0][0].skip).toEqual(false); }); + it('toggleStatus=false, skip', () => { mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); render( diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.test.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.test.tsx new file mode 100644 index 00000000000000..539b6df2d8f0ae --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../common/mock'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { UsersType } from '../../store/model'; +import { useUserRiskScore } from '../../../risk_score/containers'; +import { UserRiskTabBody } from './user_risk_tab_body'; + +jest.mock('../../../risk_score/containers'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('User query tab body', () => { + const mockUseUserRiskScore = useUserRiskScore as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + userName: 'testUser', + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: UsersType.page, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockUseUserRiskScore.mockReturnValue([ + false, + { + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + refetch: jest.fn(), + isModuleEnabled: true, + }, + ]); + }); + + it("doesn't skip when both toggleStatus are true", () => { + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() }); + + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(false); + }); + + it("doesn't skip when at least one toggleStatus is true", () => { + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() }); + + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(false); + }); + + it('does skip when at both toggleStatus are false', () => { + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() }); + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() }); + + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx new file mode 100644 index 00000000000000..ee37df16fd19c7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; + +import * as i18n from '../translations'; + +import { useQueryInspector } from '../../../common/components/page/manage_query'; +import { RiskScoreOverTime } from '../../../common/components/risk_score_over_time'; +import { TopRiskScoreContributors } from '../../../common/components/top_risk_score_contributors'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { UserRiskScoreQueryId, useUserRiskScore } from '../../../risk_score/containers'; +import { buildUserNamesFilter } from '../../../../common/search_strategy'; +import { UsersComponentsQueryProps } from './types'; +import { UserRiskInformationButtonEmpty } from '../../components/user_risk_information'; + +const QUERY_ID = UserRiskScoreQueryId.USER_DETAILS_RISK_SCORE; + +const StyledEuiFlexGroup = styled(EuiFlexGroup)` + margin-top: ${({ theme }) => theme.eui.paddingSizes.l}; +`; + +const UserRiskTabBodyComponent: React.FC< + Pick & { + userName: string; + } +> = ({ userName, startDate, endDate, setQuery, deleteQuery }) => { + const timerange = useMemo( + () => ({ + from: startDate, + to: endDate, + }), + [startDate, endDate] + ); + + const { toggleStatus: overTimeToggleStatus, setToggleStatus: setOverTimeToggleStatus } = + useQueryToggle(`${QUERY_ID} overTime`); + const { toggleStatus: contributorsToggleStatus, setToggleStatus: setContributorsToggleStatus } = + useQueryToggle(`${QUERY_ID} contributors`); + + const [loading, { data, refetch, inspect }] = useUserRiskScore({ + filterQuery: userName ? buildUserNamesFilter([userName]) : undefined, + onlyLatest: false, + skip: !overTimeToggleStatus && !contributorsToggleStatus, + timerange, + }); + + useQueryInspector({ + queryId: QUERY_ID, + loading, + refetch, + setQuery, + deleteQuery, + inspect, + }); + + const toggleContributorsQuery = useCallback( + (status: boolean) => { + setContributorsToggleStatus(status); + }, + [setContributorsToggleStatus] + ); + + const toggleOverTimeQuery = useCallback( + (status: boolean) => { + setOverTimeToggleStatus(status); + }, + [setOverTimeToggleStatus] + ); + + const rules = data && data.length > 0 ? data[data.length - 1].risk_stats.rule_risks : []; + + return ( + <> + + + + + + + + + + + + {/* // TODO PENDING ON USER RISK DOCUMENTATION + + + {i18n.VIEW_DASHBOARD_BUTTON} + + */} + + + + + + ); +}; + +UserRiskTabBodyComponent.displayName = 'UserRiskTabBodyComponent'; + +export const UserRiskTabBody = React.memo(UserRiskTabBodyComponent); + +UserRiskTabBody.displayName = 'UserRiskTabBody'; diff --git a/x-pack/plugins/security_solution/public/users/pages/translations.ts b/x-pack/plugins/security_solution/public/users/pages/translations.ts index 41fec21c5bfb00..c36abbaab86ec4 100644 --- a/x-pack/plugins/security_solution/public/users/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/users/pages/translations.ts @@ -35,7 +35,7 @@ export const NAVIGATION_ANOMALIES_TITLE = i18n.translate( export const NAVIGATION_RISK_TITLE = i18n.translate( 'xpack.securitySolution.users.navigation.riskTitle', { - defaultMessage: 'Users by risk', + defaultMessage: 'User risk', } ); @@ -52,3 +52,17 @@ export const NAVIGATION_ALERTS_TITLE = i18n.translate( defaultMessage: 'External alerts', } ); + +export const USER_RISK_SCORE_OVER_TIME = i18n.translate( + 'xpack.securitySolution.users.navigation.userScoreOverTimeTitle', + { + defaultMessage: 'User risk score over time', + } +); + +export const VIEW_DASHBOARD_BUTTON = i18n.translate( + 'xpack.securitySolution.hosts.navigaton.hostRisk.viewDashboardButtonLabel', + { + defaultMessage: 'View source dashboard', + } +); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index a01dd557b1b043..5380b97f300eed 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -25837,7 +25837,6 @@ "xpack.securitySolution.hosts.hostScoreOverTime.riskScore": "Score de risque", "xpack.securitySolution.hosts.hostScoreOverTime.riskyLabel": "À risque", "xpack.securitySolution.hosts.hostScoreOverTime.riskyThresholdHeader": "Seuil du niveau À risque", - "xpack.securitySolution.hosts.hostScoreOverTime.title": "Score de risque de l'hôte sur la durée", "xpack.securitySolution.hosts.kqlPlaceholder": "par ex. hôte.nom : \"foo\"", "xpack.securitySolution.hosts.navigation.alertsTitle": "Alertes externes", "xpack.securitySolution.hosts.navigation.allHostsTitle": "Tous les hôtes", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a50acac34f2ed7..4104c04a6464be 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -26001,7 +26001,6 @@ "xpack.securitySolution.hosts.hostScoreOverTime.riskScore": "リスクスコア", "xpack.securitySolution.hosts.hostScoreOverTime.riskyLabel": "高リスク", "xpack.securitySolution.hosts.hostScoreOverTime.riskyThresholdHeader": "高リスクしきい値", - "xpack.securitySolution.hosts.hostScoreOverTime.title": "経時的なホストリスクスコア", "xpack.securitySolution.hosts.kqlPlaceholder": "例:host.name:\"foo\"", "xpack.securitySolution.hosts.navigation.alertsTitle": "外部アラート", "xpack.securitySolution.hosts.navigation.allHostsTitle": "すべてのホスト", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e7985798dd465b..451a1412003918 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -26036,7 +26036,6 @@ "xpack.securitySolution.hosts.hostScoreOverTime.riskScore": "风险分数", "xpack.securitySolution.hosts.hostScoreOverTime.riskyLabel": "有风险", "xpack.securitySolution.hosts.hostScoreOverTime.riskyThresholdHeader": "有风险的阈值", - "xpack.securitySolution.hosts.hostScoreOverTime.title": "一段时间的主机风险分数", "xpack.securitySolution.hosts.kqlPlaceholder": "例如 host.name:“foo”", "xpack.securitySolution.hosts.navigation.alertsTitle": "外部告警", "xpack.securitySolution.hosts.navigation.allHostsTitle": "所有主机", From 741d7e9f2731b4d68aaf1fb590a1e16b395056c1 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Fri, 6 May 2022 11:56:56 +0200 Subject: [PATCH 48/83] [Cases] Add severity field to the Case view page. (#131521) --- x-pack/plugins/cases/common/ui/types.ts | 2 +- .../cases/public/common/translations.ts | 4 ++ .../components/case_view_activity.tsx | 14 ++++ .../case_view/use_on_update_field.ts | 5 ++ .../public/components/severity/config.ts | 29 +++++++++ .../components/severity/selector.test.tsx | 60 +++++++++++++++++ .../public/components/severity/selector.tsx | 64 +++++++++++++++++++ .../components/severity/sidebar_selector.tsx | 43 +++++++++++++ .../components/severity/translations.ts | 28 ++++++++ .../components/user_actions/builder.tsx | 6 +- .../components/user_actions/severity.test.tsx | 39 +++++++++++ .../components/user_actions/severity.tsx | 53 +++++++++++++++ 12 files changed, 342 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/severity/config.ts create mode 100644 x-pack/plugins/cases/public/components/severity/selector.test.tsx create mode 100644 x-pack/plugins/cases/public/components/severity/selector.tsx create mode 100644 x-pack/plugins/cases/public/components/severity/sidebar_selector.tsx create mode 100644 x-pack/plugins/cases/public/components/severity/translations.ts create mode 100644 x-pack/plugins/cases/public/components/user_actions/severity.test.tsx create mode 100644 x-pack/plugins/cases/public/components/user_actions/severity.tsx diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index b993ae6602ae9f..7ed9bfb3f22942 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -144,7 +144,7 @@ export interface FieldMappings { export type UpdateKey = keyof Pick< CasePatchRequest, - 'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings' + 'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings' | 'severity' >; export interface UpdateByKey { diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index 10005b2c87bce1..dbc57e163d3ff9 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -198,6 +198,10 @@ export const MARKED_CASE_AS = i18n.translate('xpack.cases.caseView.markedCaseAs' defaultMessage: 'marked case as', }); +export const SET_SEVERITY_TO = i18n.translate('xpack.cases.caseView.setSeverityTo', { + defaultMessage: 'set severity to', +}); + export const OPEN_CASES = i18n.translate('xpack.cases.caseTable.openCases', { defaultMessage: 'Open cases', }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx index b9e4beb5d7e26d..452601d1428486 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx @@ -7,6 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; +import { CaseSeverity } from '../../../../common/api'; import { useConnectors } from '../../../containers/configure/use_connectors'; import { useCaseViewNavigation } from '../../../common/navigation'; import { UpdateKey, UseFetchAlertData } from '../../../../common/ui/types'; @@ -23,6 +24,7 @@ import * as i18n from '../translations'; import { getNoneConnector, normalizeActionConnector } from '../../configure_cases/utils'; import { getConnectorById } from '../../utils'; import { UseGetCaseUserActions } from '../../../containers/use_get_case_user_actions'; +import { SeveritySidebarSelector } from '../../severity/sidebar_selector'; export const CaseViewActivity = ({ initLoadingData, @@ -108,6 +110,12 @@ export const CaseViewActivity = ({ (newTags) => onUpdateField({ key: 'tags', value: newTags }), [onUpdateField] ); + + const onUpdateSeverity = useCallback( + (newSeverity: CaseSeverity) => onUpdateField({ key: 'severity', value: newSeverity }), + [onUpdateField] + ); + const { loading: isLoadingConnectors, connectors } = useConnectors(); const [connectorName, isValidConnector] = useMemo(() => { @@ -180,6 +188,12 @@ export const CaseViewActivity = ({ )}
+ (value); + if (caseData.severity !== value) { + callUpdate('severity', severityUpdate); + } default: return null; } diff --git a/x-pack/plugins/cases/public/components/severity/config.ts b/x-pack/plugins/cases/public/components/severity/config.ts new file mode 100644 index 00000000000000..945eb94640d3c1 --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/config.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { euiLightVars } from '@kbn/ui-theme'; +import { CaseSeverity } from '../../../common/api'; +import { CRITICAL, HIGH, LOW, MEDIUM } from './translations'; + +export const severities = { + [CaseSeverity.LOW]: { + color: euiLightVars.euiColorVis0, + label: LOW, + }, + [CaseSeverity.MEDIUM]: { + color: euiLightVars.euiColorVis5, + label: MEDIUM, + }, + [CaseSeverity.HIGH]: { + color: euiLightVars.euiColorVis7, + label: HIGH, + }, + [CaseSeverity.CRITICAL]: { + color: euiLightVars.euiColorVis9, + label: CRITICAL, + }, +}; diff --git a/x-pack/plugins/cases/public/components/severity/selector.test.tsx b/x-pack/plugins/cases/public/components/severity/selector.test.tsx new file mode 100644 index 00000000000000..126dc64e7af1bd --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/selector.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseSeverity } from '../../../common/api'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { SeveritySelector } from './selector'; +import userEvent from '@testing-library/user-event'; + +describe('Severity field selector', () => { + const onSeverityChange = jest.fn(); + it('renders a list of severity fields', () => { + const result = render( + + ); + + expect(result.getByTestId('case-severity-selection')).toBeTruthy(); + expect(result.getAllByTestId('case-severity-selection-medium').length).toBeTruthy(); + }); + + it('renders a list of severity options when clicked', () => { + const result = render( + + ); + userEvent.click(result.getByTestId('case-severity-selection')); + expect(result.getByTestId('case-severity-selection-low')).toBeTruthy(); + expect(result.getAllByTestId('case-severity-selection-medium').length).toBeTruthy(); + expect(result.getByTestId('case-severity-selection-high')).toBeTruthy(); + expect(result.getByTestId('case-severity-selection-critical')).toBeTruthy(); + }); + + it('calls onSeverityChange with the newly selected severity when clicked', () => { + const result = render( + + ); + userEvent.click(result.getByTestId('case-severity-selection')); + expect(result.getByTestId('case-severity-selection-low')).toBeTruthy(); + userEvent.click(result.getByTestId('case-severity-selection-low')); + expect(onSeverityChange).toHaveBeenLastCalledWith('low'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/severity/selector.tsx b/x-pack/plugins/cases/public/components/severity/selector.tsx new file mode 100644 index 00000000000000..0d1ff4b319f2b7 --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/selector.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiSuperSelect, + EuiSuperSelectOption, +} from '@elastic/eui'; +import React from 'react'; +import { CaseSeverity } from '../../../common/api'; +import { severities } from './config'; + +interface Props { + selectedSeverity: CaseSeverity; + onSeverityChange: (status: CaseSeverity) => void; + isLoading: boolean; + isDisabled: boolean; +} + +export const SeveritySelector: React.FC = ({ + selectedSeverity, + onSeverityChange, + isLoading, + isDisabled, +}) => { + const caseSeverities = Object.keys(severities) as CaseSeverity[]; + const options: Array> = caseSeverities.map((severity) => { + const severityData = severities[severity]; + return { + value: severity, + inputDisplay: ( + + + {severityData.label} + + + ), + }; + }); + + return ( + + ); +}; +SeveritySelector.displayName = 'SeveritySelector'; diff --git a/x-pack/plugins/cases/public/components/severity/sidebar_selector.tsx b/x-pack/plugins/cases/public/components/severity/sidebar_selector.tsx new file mode 100644 index 00000000000000..ff591e342793f7 --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/sidebar_selector.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexItem, EuiHorizontalRule, EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; +import { CaseSeverity } from '../../../common/api'; +import { SeveritySelector } from './selector'; +import { SEVERITY_TITLE } from './translations'; + +interface Props { + selectedSeverity: CaseSeverity; + onSeverityChange: (status: CaseSeverity) => void; + isLoading: boolean; + isDisabled: boolean; +} + +export const SeveritySidebarSelector: React.FC = ({ + selectedSeverity, + onSeverityChange, + isLoading, + isDisabled, +}) => { + return ( + + +

{SEVERITY_TITLE}

+
+ + + +
+ ); +}; +SeveritySidebarSelector.displayName = 'SeveritySidebarSelector'; diff --git a/x-pack/plugins/cases/public/components/severity/translations.ts b/x-pack/plugins/cases/public/components/severity/translations.ts new file mode 100644 index 00000000000000..b5982c70ed6907 --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const LOW = i18n.translate('xpack.cases.severity.low', { + defaultMessage: 'Low', +}); + +export const MEDIUM = i18n.translate('xpack.cases.severity.medium', { + defaultMessage: 'Medium', +}); + +export const HIGH = i18n.translate('xpack.cases.severity.high', { + defaultMessage: 'High', +}); + +export const CRITICAL = i18n.translate('xpack.cases.severity.critical', { + defaultMessage: 'Critical', +}); + +export const SEVERITY_TITLE = i18n.translate('xpack.cases.severity.title', { + defaultMessage: 'Severity', +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/builder.tsx b/x-pack/plugins/cases/public/components/user_actions/builder.tsx index 019e37396a7ce4..36298bbae601b3 100644 --- a/x-pack/plugins/cases/public/components/user_actions/builder.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/builder.tsx @@ -10,6 +10,7 @@ import { createConnectorUserActionBuilder } from './connector'; import { createDescriptionUserActionBuilder } from './description'; import { createPushedUserActionBuilder } from './pushed'; import { createSettingsUserActionBuilder } from './settings'; +import { createSeverityUserActionBuilder } from './severity'; import { createStatusUserActionBuilder } from './status'; import { createTagsUserActionBuilder } from './tags'; import { createTitleUserActionBuilder } from './title'; @@ -20,10 +21,7 @@ export const builderMap: UserActionBuilderMap = { tags: createTagsUserActionBuilder, title: createTitleUserActionBuilder, status: createStatusUserActionBuilder, - // TODO: Build severity user action - severity: () => ({ - build: () => [], - }), + severity: createSeverityUserActionBuilder, pushed: createPushedUserActionBuilder, comment: createCommentUserActionBuilder, description: createDescriptionUserActionBuilder, diff --git a/x-pack/plugins/cases/public/components/user_actions/severity.test.tsx b/x-pack/plugins/cases/public/components/user_actions/severity.test.tsx new file mode 100644 index 00000000000000..d92a5cb5a153dd --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/severity.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCommentList } from '@elastic/eui'; +import { Actions, CaseSeverity } from '../../../common/api'; +import React from 'react'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; +import { getUserAction } from '../../containers/mock'; +import { getMockBuilderArgs } from './mock'; +import { createSeverityUserActionBuilder } from './severity'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); + +const builderArgs = getMockBuilderArgs(); +describe('createSeverityUserActionBuilder', () => { + let appMockRenderer: AppMockRenderer; + beforeEach(() => { + appMockRenderer = createAppMockRenderer(); + }); + it('renders correctly', () => { + const userAction = getUserAction('severity', Actions.update, { + payload: { severity: CaseSeverity.LOW }, + }); + const builder = createSeverityUserActionBuilder({ + ...builderArgs, + userAction, + }); + const createdUserAction = builder.build(); + + const result = appMockRenderer.render(); + expect(result.getByTestId('severity-update-user-action-severity-title')).toBeTruthy(); + expect(result.getByTestId('severity-update-user-action-severity-title-low')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/severity.tsx b/x-pack/plugins/cases/public/components/user_actions/severity.tsx new file mode 100644 index 00000000000000..3e2cf8605b080e --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/severity.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiHealth } from '@elastic/eui'; +import React from 'react'; +import { SeverityUserAction } from '../../../common/api/cases/user_actions/severity'; +import { SET_SEVERITY_TO } from '../create/translations'; +import { createCommonUpdateUserActionBuilder } from './common'; +import { UserActionBuilder, UserActionResponse } from './types'; +import { severities } from '../severity/config'; + +const getLabelTitle = (userAction: UserActionResponse) => { + const severity = userAction.payload.severity; + const severityData = severities[severity]; + if (severityData === undefined) { + return null; + } + return ( + + {SET_SEVERITY_TO} + + {severityData.label} + + + ); +}; + +export const createSeverityUserActionBuilder: UserActionBuilder = ({ + userAction, + handleOutlineComment, +}) => ({ + build: () => { + const severityUserAction = userAction as UserActionResponse; + const label = getLabelTitle(severityUserAction); + const commonBuilder = createCommonUpdateUserActionBuilder({ + userAction, + handleOutlineComment, + label, + icon: 'dot', + }); + + return commonBuilder.build(); + }, +}); From 3d947896aa5c1acac92ee8bef52d17e67364b5b8 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 6 May 2022 13:44:38 +0200 Subject: [PATCH 49/83] [Synthetics] Hide behind feature flag (#131502) Co-authored-by: Colleen McGinnis Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../server/collectors/management/schema.ts | 4 +++ .../server/collectors/management/types.ts | 1 + src/plugins/telemetry/schema/oss_plugins.json | 26 ++++++++------ .../observability/common/ui_settings_keys.ts | 1 + x-pack/plugins/observability/public/index.ts | 1 + .../observability/server/ui_settings.ts | 19 +++++++++- x-pack/plugins/synthetics/public/plugin.ts | 35 +++++++++++-------- x-pack/test/functional/config.base.js | 1 + 8 files changed, 62 insertions(+), 26 deletions(-) diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 7a19ff022226e6..a948a035f2d488 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -418,6 +418,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:enableNewSyntheticsView': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'observability:maxSuggestions': { type: 'integer', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index b9d50f888fa93b..718f75b80a77df 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -36,6 +36,7 @@ export interface UsageStats { 'discover:maxDocFieldsDisplayed': number; 'securitySolution:rulesTableRefresh': string; 'observability:enableInspectEsQueries': boolean; + 'observability:enableNewSyntheticsView': boolean; 'observability:maxSuggestions': number; 'observability:enableComparisonByDefault': boolean; 'observability:enableInfrastructureView': boolean; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 59d7ba693156d8..0c0cafad6bec65 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -202,29 +202,29 @@ } } }, - "search": { + "search-session": { "properties": { - "successCount": { + "transientCount": { "type": "long" }, - "errorCount": { + "persistedCount": { "type": "long" }, - "averageDuration": { - "type": "float" + "totalCount": { + "type": "long" } } }, - "search-session": { + "search": { "properties": { - "transientCount": { + "successCount": { "type": "long" }, - "persistedCount": { + "errorCount": { "type": "long" }, - "totalCount": { - "type": "long" + "averageDuration": { + "type": "float" } } }, @@ -8133,6 +8133,12 @@ "description": "Non-default value of setting." } }, + "observability:enableNewSyntheticsView": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "observability:maxSuggestions": { "type": "integer", "_meta": { diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index 4c1b1dc729feab..287fe541cc7b6c 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -5,6 +5,7 @@ * 2.0. */ +export const enableNewSyntheticsView = 'observability:enableNewSyntheticsView'; export const enableInspectEsQueries = 'observability:enableInspectEsQueries'; export const maxSuggestions = 'observability:maxSuggestions'; export const enableComparisonByDefault = 'observability:enableComparisonByDefault'; diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 19468ef0e27369..00db5b1873980f 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -28,6 +28,7 @@ export { enableComparisonByDefault, enableInfrastructureView, enableServiceGroups, + enableNewSyntheticsView, } from '../common/ui_settings_keys'; export { uptimeOverviewLocatorID } from '../common'; diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index 02c519b10d19ce..5b21b07d1cea3e 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -18,6 +18,7 @@ import { apmProgressiveLoading, enableServiceGroups, apmServiceInventoryOptimizedSorting, + enableNewSyntheticsView, } from '../common/ui_settings_keys'; const technicalPreviewLabel = i18n.translate( @@ -31,6 +32,22 @@ const technicalPreviewLabel = i18n.translate( * uiSettings definitions for Observability. */ export const uiSettings: Record> = { + [enableNewSyntheticsView]: { + category: [observabilityFeatureId], + name: i18n.translate('xpack.observability.enableNewSyntheticsViewExperimentName', { + defaultMessage: 'Enable new synthetic monitoring application', + }), + value: false, + description: i18n.translate( + 'xpack.observability.enableNewSyntheticsViewExperimentDescription', + { + defaultMessage: + 'Enable new synthetic monitoring application in observability. Refresh the page to apply the setting.', + } + ), + schema: schema.boolean(), + requiresPageReload: true, + }, [enableInspectEsQueries]: { category: [observabilityFeatureId], name: i18n.translate('xpack.observability.enableInspectEsQueriesExperimentName', { @@ -71,7 +88,7 @@ export const uiSettings: Record { - const [coreStart, corePlugins] = await core.getStartServices(); + const isSyntheticsViewEnabled = core.uiSettings.get(enableNewSyntheticsView); - const { renderApp } = await import('./apps/synthetics/render_app'); - return renderApp(coreStart, plugins, corePlugins, params, this.initContext.env.mode.dev); - }, - }); + if (isSyntheticsViewEnabled) { + // Register the Synthetics UI plugin + core.application.register({ + id: 'synthetics', + euiIconType: 'logoObservability', + order: 8400, + title: PLUGIN.SYNTHETICS, + category: DEFAULT_APP_CATEGORIES.observability, + keywords: appKeywords, + deepLinks: [], + mount: async (params: AppMountParameters) => { + const [coreStart, corePlugins] = await core.getStartServices(); + + const { renderApp } = await import('./apps/synthetics/render_app'); + return renderApp(coreStart, plugins, corePlugins, params, this.initContext.env.mode.dev); + }, + }); + } } public start(start: CoreStart, plugins: ClientPluginsStart): void { diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index b5b671a54744e8..a4d73d40e2d4d1 100644 --- a/x-pack/test/functional/config.base.js +++ b/x-pack/test/functional/config.base.js @@ -51,6 +51,7 @@ export default async function ({ readConfigFile }) { '--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"', '--xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled=true', '--savedObjects.maxImportPayloadBytes=10485760', // for OSS test management/_import_objects, + '--uiSettings.overrides.observability:enableNewSyntheticsView=true', // for OSS test management/_import_objects, ], }, uiSettings: { From ed70cb1dbee55eb9bac42801e7a42eab994ac762 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 6 May 2022 15:40:10 +0300 Subject: [PATCH 50/83] [TSVB] Wait for viz to be stabilized after changing the timerange mode (#131690) * [TSVB] Wait for viz to be stabilized after changing the timerange mode * Address PR comments --- test/functional/page_objects/visual_builder_page.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index fbf6b96b3136d3..f96e4088da78fb 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -374,11 +374,13 @@ export class VisualBuilderPageObject extends FtrService { } public async getTopNLabel() { + await this.visChart.waitForVisualizationRenderingStabilized(); const topNLabel = await this.find.byCssSelector('.tvbVisTopN__label'); return await topNLabel.getVisibleText(); } public async getTopNCount() { + await this.visChart.waitForVisualizationRenderingStabilized(); const gaugeCount = await this.find.byCssSelector('.tvbVisTopN__value'); return await gaugeCount.getVisibleText(); } From 43c4134e8d85d84afe1eae5607f949af0aa0cf4b Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 6 May 2022 09:12:34 -0400 Subject: [PATCH 51/83] Using data view service to fetch data views (#131330) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/app.tsx | 6 +- .../public/common/lib/data_apis.test.ts | 69 ++----------------- .../public/common/lib/data_apis.ts | 49 +++---------- 3 files changed, 19 insertions(+), 105 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 71bcb2ee7d760c..c4c273bd003c5c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -31,7 +31,7 @@ import { } from '../types'; import { Section, routeToRuleDetails, legacyRouteToRuleDetails } from './constants'; -import { setSavedObjectsClient } from '../common/lib/data_apis'; +import { setDataViewsService } from '../common/lib/data_apis'; import { KibanaContextProvider } from '../common/lib/kibana'; const TriggersActionsUIHome = lazy(() => import('./home')); @@ -67,12 +67,12 @@ export const renderApp = (deps: TriggersAndActionsUiServices) => { }; export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => { - const { savedObjects, uiSettings, theme$ } = deps; + const { dataViews, uiSettings, theme$ } = deps; const sections: Section[] = ['rules', 'connectors', 'alerts', '__components_sandbox']; const isDarkMode = useObservable(uiSettings.get$('theme:darkMode')); const sectionsRegex = sections.join('|'); - setSavedObjectsClient(savedObjects.client); + setDataViewsService(dataViews); return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.test.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.test.ts index 9b0d122e24d4e0..178c891dc3a34e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.test.ts @@ -7,7 +7,7 @@ import { loadIndexPatterns, - setSavedObjectsClient, + setDataViewsService, getMatchingIndices, getESIndexFields, } from './data_apis'; @@ -19,10 +19,8 @@ const http = httpServiceMock.createStartContract(); const pattern = 'test-pattern'; const indexes = ['test-index']; -const generateIndexPattern = (title: string) => ({ - attributes: { - title, - }, +const generateDataView = (title: string) => ({ + title, }); const mockIndices = { indices: ['indices1', 'indices2'] }; @@ -67,7 +65,7 @@ describe('Data API', () => { describe('index patterns', () => { beforeEach(() => { - setSavedObjectsClient({ + setDataViewsService({ find: mockFind, }); }); @@ -76,68 +74,15 @@ describe('Data API', () => { }); test('fetches the index patterns', async () => { - mockFind.mockResolvedValueOnce({ - savedObjects: [generateIndexPattern('index-1'), generateIndexPattern('index-2')], - total: 2, - }); + mockFind.mockResolvedValueOnce([generateDataView('index-1'), generateDataView('index-2')]); const results = await loadIndexPatterns(mockPattern); expect(mockFind).toBeCalledTimes(1); - expect(mockFind).toBeCalledWith({ - fields: ['title'], - page: 1, - perPage, - search: '*test-pattern*', - type: 'index-pattern', - }); + expect(mockFind).toBeCalledWith('*test-pattern*', perPage); expect(results).toEqual(['index-1', 'index-2']); }); - test(`fetches the index patterns as chunks and merges them, if the total number of index patterns more than ${perPage}`, async () => { - mockFind.mockResolvedValueOnce({ - savedObjects: [generateIndexPattern('index-1'), generateIndexPattern('index-2')], - total: 2010, - }); - mockFind.mockResolvedValueOnce({ - savedObjects: [generateIndexPattern('index-3'), generateIndexPattern('index-4')], - total: 2010, - }); - mockFind.mockResolvedValueOnce({ - savedObjects: [generateIndexPattern('index-5'), generateIndexPattern('index-6')], - total: 2010, - }); - const results = await loadIndexPatterns(mockPattern); - - expect(mockFind).toBeCalledTimes(3); - expect(mockFind).toHaveBeenNthCalledWith(1, { - fields: ['title'], - page: 1, - perPage, - search: '*test-pattern*', - type: 'index-pattern', - }); - expect(mockFind).toHaveBeenNthCalledWith(2, { - fields: ['title'], - page: 2, - perPage, - search: '*test-pattern*', - type: 'index-pattern', - }); - expect(mockFind).toHaveBeenNthCalledWith(3, { - fields: ['title'], - page: 3, - perPage, - search: '*test-pattern*', - type: 'index-pattern', - }); - expect(results).toEqual(['index-1', 'index-2', 'index-3', 'index-4', 'index-5', 'index-6']); - }); - - test('returns an empty array if one of the requests fails', async () => { - mockFind.mockResolvedValueOnce({ - savedObjects: [generateIndexPattern('index-1'), generateIndexPattern('index-2')], - total: 1010, - }); + test('returns an empty array if find requests fails', async () => { mockFind.mockRejectedValueOnce(500); const results = await loadIndexPatterns(mockPattern); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts index 55b1ef4be2c742..90f80dd3dc2f0c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts @@ -6,6 +6,7 @@ */ import { HttpSetup } from '@kbn/core/public'; +import { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; const DATA_API_ROOT = '/api/triggers_actions_ui/data'; @@ -62,57 +63,25 @@ export async function getESIndexFields({ return fields; } -let savedObjectsClient: any; +type DataViewsService = Pick; +let dataViewsService: DataViewsService; -export const setSavedObjectsClient = (aSavedObjectsClient: any) => { - savedObjectsClient = aSavedObjectsClient; +export const setDataViewsService = (aDataViewsService: DataViewsService) => { + dataViewsService = aDataViewsService; }; -export const getSavedObjectsClient = () => { - return savedObjectsClient; +export const getDataViewsService = () => { + return dataViewsService; }; export const loadIndexPatterns = async (pattern: string) => { - let allSavedObjects = []; const formattedPattern = formatPattern(pattern); const perPage = 1000; try { - const { savedObjects, total } = await getSavedObjectsClient().find({ - type: 'index-pattern', - fields: ['title'], - page: 1, - search: formattedPattern, - perPage, - }); + const dataViews: DataView[] = await getDataViewsService().find(formattedPattern, perPage); - allSavedObjects = savedObjects; - - if (total > perPage) { - let currentPage = 2; - const numberOfPages = Math.ceil(total / perPage); - const promises = []; - - while (currentPage <= numberOfPages) { - promises.push( - getSavedObjectsClient().find({ - type: 'index-pattern', - page: currentPage, - fields: ['title'], - search: formattedPattern, - perPage, - }) - ); - currentPage++; - } - - const paginatedResults = await Promise.all(promises); - - allSavedObjects = paginatedResults.reduce((oldResult, result) => { - return oldResult.concat(result.savedObjects); - }, allSavedObjects); - } - return allSavedObjects.map((indexPattern: any) => indexPattern.attributes.title); + return dataViews.map((dataView: DataView) => dataView.title); } catch (e) { return []; } From d145b9ddd74c3bb1f3ad626350ba69837dafa74a Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 6 May 2022 15:24:45 +0200 Subject: [PATCH 52/83] [ML] Propagate execution context (#131374) * report execution context for routes * report execution context for routes * useExecutionContext for embeddable swim lane * useExecutionContext for embeddable anomaly charts * fix activeRoute * refactor --- .../application/routing/ml_page_wrapper.tsx | 1 + .../application/routing/use_active_route.ts | 20 +++++++- .../embeddable_anomaly_charts_container.tsx | 9 ++++ .../embeddable_swim_lane_container.tsx | 9 ++++ .../use_embeddable_execution_context.ts | 48 +++++++++++++++++++ 5 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/ml/public/embeddables/common/use_embeddable_execution_context.ts diff --git a/x-pack/plugins/ml/public/application/routing/ml_page_wrapper.tsx b/x-pack/plugins/ml/public/application/routing/ml_page_wrapper.tsx index f1674a12b77c6b..4c2a6d0058edc5 100644 --- a/x-pack/plugins/ml/public/application/routing/ml_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/routing/ml_page_wrapper.tsx @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import React, { FC } from 'react'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; diff --git a/x-pack/plugins/ml/public/application/routing/use_active_route.ts b/x-pack/plugins/ml/public/application/routing/use_active_route.ts index 925db8185c379b..9183e45c3d0aed 100644 --- a/x-pack/plugins/ml/public/application/routing/use_active_route.ts +++ b/x-pack/plugins/ml/public/application/routing/use_active_route.ts @@ -8,11 +8,21 @@ import { useLocation, useRouteMatch } from 'react-router-dom'; import { keyBy } from 'lodash'; import { useMemo } from 'react'; +import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; +import { useMlKibana } from '../contexts/kibana'; import type { MlRoute } from './router'; +/** + * Provides an active route of the ML app. + * @param routesList + */ export const useActiveRoute = (routesList: MlRoute[]): MlRoute => { const { pathname } = useLocation(); + const { + services: { executionContext }, + } = useMlKibana(); + /** * Temp fix for routes with params. */ @@ -30,8 +40,14 @@ export const useActiveRoute = (routesList: MlRoute[]): MlRoute => { } // Remove trailing slash from the pathname const pathnameKey = pathname.replace(/\/$/, ''); - return routesMap[pathnameKey]; + return routesMap[pathnameKey] ?? routesMap['/overview']; }, [pathname]); - return activeRoute ?? routesMap['/overview']; + useExecutionContext(executionContext, { + name: 'Machine Learning', + type: 'application', + page: activeRoute?.path, + }); + + return activeRoute; }; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.tsx index 9f31f5777f9de2..85350629263e4a 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.tsx @@ -11,6 +11,7 @@ import { Observable } from 'rxjs'; import { FormattedMessage } from '@kbn/i18n-react'; import { throttle } from 'lodash'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import { useEmbeddableExecutionContext } from '../common/use_embeddable_execution_context'; import { useAnomalyChartsInputResolver } from './use_anomaly_charts_input_resolver'; import type { IAnomalyChartsEmbeddable } from './anomaly_charts_embeddable'; import type { @@ -27,6 +28,7 @@ import { ANOMALY_THRESHOLD } from '../../../common'; import { TimeBuckets } from '../../application/util/time_buckets'; import { EXPLORER_ENTITY_FIELD_SELECTION_TRIGGER } from '../../ui_actions/triggers'; import { MlLocatorParams } from '../../../common/types/locator'; +import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '..'; const RESIZE_THROTTLE_TIME_MS = 500; @@ -55,6 +57,13 @@ export const EmbeddableAnomalyChartsContainer: FC { + useEmbeddableExecutionContext( + services[0].executionContext, + embeddableInput, + ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, + id + ); + const [chartWidth, setChartWidth] = useState(0); const [severity, setSeverity] = useState( optionValueToThreshold( diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx index 06c400481491a4..c354057d971bb7 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx @@ -11,6 +11,7 @@ import { Observable } from 'rxjs'; import { CoreStart } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useEmbeddableExecutionContext } from '../common/use_embeddable_execution_context'; import { IAnomalySwimlaneEmbeddable } from './anomaly_swimlane_embeddable'; import { useSwimlaneInputResolver } from './swimlane_input_resolver'; import { SwimlaneType } from '../../application/explorer/explorer_constants'; @@ -22,6 +23,7 @@ import { AppStateSelectedCells } from '../../application/explorer/explorer_utils import { MlDependencies } from '../../application/app'; import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions'; import { + ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableOutput, AnomalySwimlaneServices, @@ -52,6 +54,13 @@ export const EmbeddableSwimLaneContainer: FC = ( onLoading, onError, }) => { + useEmbeddableExecutionContext( + services[0].executionContext, + embeddableInput, + ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, + id + ); + const [chartWidth, setChartWidth] = useState(0); const [fromPage, setFromPage] = useState(1); diff --git a/x-pack/plugins/ml/public/embeddables/common/use_embeddable_execution_context.ts b/x-pack/plugins/ml/public/embeddables/common/use_embeddable_execution_context.ts new file mode 100644 index 00000000000000..68306c54c85902 --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/common/use_embeddable_execution_context.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import useObservable from 'react-use/lib/useObservable'; +import { map } from 'rxjs/operators'; +import { KibanaExecutionContext } from '@kbn/core/types'; +import { useMemo } from 'react'; +import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; +import type { Observable } from 'rxjs'; +import type { EmbeddableInput } from '@kbn/embeddable-plugin/common'; +import { ExecutionContextStart } from '@kbn/core/public'; + +/** + * Use execution context for ML embeddables. + * @param executionContext + * @param embeddableInput$ + * @param embeddableType + * @param id + */ +export function useEmbeddableExecutionContext( + executionContext: ExecutionContextStart, + embeddableInput$: Observable, + embeddableType: string, + id: string +) { + const parentExecutionContext = useObservable( + embeddableInput$.pipe(map((v) => v.executionContext)) + ); + + const embeddableExecutionContext: KibanaExecutionContext = useMemo(() => { + const child: KibanaExecutionContext = { + type: 'visualization', + name: embeddableType, + id, + }; + + return { + ...parentExecutionContext, + child, + }; + }, [parentExecutionContext, id]); + + useExecutionContext(executionContext, embeddableExecutionContext); +} From 0998b67ded645b1e12ee7e9c6181bb54700e5425 Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Fri, 6 May 2022 08:44:56 -0500 Subject: [PATCH 53/83] exclude "Team:Unified observability" from Kibana QA team PR list (#131673) * exclude "Team:Unified observability" * exclude "Feature:Unified Integrations" * exclude "Team: AWP: Visualization" * skip config file in ci Co-authored-by: spalger --- .buildkite/scripts/pipelines/pull_request/pipeline.js | 1 + src/dev/prs/kibana_qa_pr_list.json | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.js b/.buildkite/scripts/pipelines/pull_request/pipeline.js index 35ea19c78cd93d..65742902943ecc 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.js +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.js @@ -21,6 +21,7 @@ const SKIPPABLE_PATHS = [ /\.md$/, /^\.backportrc\.json$/, /^nav-kibana-dev\.docnav\.json$/, + /^src\/dev\/prs\/kibana_qa_pr_list\.json$/, ]; const REQUIRED_PATHS = [ diff --git a/src/dev/prs/kibana_qa_pr_list.json b/src/dev/prs/kibana_qa_pr_list.json index e8d27ba9f2f0a4..503c95d2e7c0fc 100644 --- a/src/dev/prs/kibana_qa_pr_list.json +++ b/src/dev/prs/kibana_qa_pr_list.json @@ -89,8 +89,10 @@ "Feature:Observability Landing - Milestone 1", "Feature:Osquery", "Feature:Transforms", +"Feature:Unified Integrations", "Synthetics", "Team: AWL: Platform", +"Team: AWP: Visualization", "Team: Actionable Observability", "Team: CTI", "Team: SecuritySolution", @@ -109,6 +111,7 @@ "Team:Infra Monitoring UI", "Team:Ingest Management", "Team:Observability", +"Team:Unified observability", "Team:Onboarding and Lifecycle Mgt", "Team:Operations", "Team:QA", From 2fb361b0920b22410d3941bc482d4cab9f25ef23 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 6 May 2022 10:15:48 -0400 Subject: [PATCH 54/83] [Connectors] Capturing validation errors in action execution event log (#131557) * Capturing validation errors in action execution event log * Fixing functional test * Fixing functional test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/action_executor.test.ts | 2 +- .../actions/server/lib/action_executor.ts | 82 +++++++++++++------ .../lib/errors/action_execution_error.ts | 27 ++++++ .../spaces_only/tests/actions/execute.ts | 6 +- .../tests/alerting/get_execution_log.ts | 2 +- 5 files changed, 89 insertions(+), 30 deletions(-) create mode 100644 x-pack/plugins/actions/server/lib/errors/action_execution_error.ts diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 093236c939aa12..12898cea5a4828 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -521,7 +521,7 @@ test('logs a warning when alert executor throws an error', async () => { executorMock.mockRejectedValue(new Error('this action execution is intended to fail')); await actionExecutor.execute(executeParams); expect(loggerMock.warn).toBeCalledWith( - 'action execution failure: test:1: action-1: an error occurred while running the action executor: this action execution is intended to fail' + 'action execution failure: test:1: action-1: an error occurred while running the action: this action execution is intended to fail' ); }); diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index fe77b72f47aa36..b9ed252c6afc2d 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -19,6 +19,7 @@ import { validateConnector, } from './validate_with_schema'; import { + ActionType, ActionTypeExecutorResult, ActionTypeRegistryContract, GetServicesFunction, @@ -30,6 +31,7 @@ import { ActionsClient } from '../actions_client'; import { ActionExecutionSource } from './action_execution_source'; import { RelatedSavedObjects } from './related_saved_objects'; import { createActionEventLogRecordObject } from './create_action_event_log_record_object'; +import { ActionExecutionError, ActionExecutionErrorReason } from './errors/action_execution_error'; // 1,000,000 nanoseconds in 1 millisecond const Millis2Nanos = 1000 * 1000; @@ -157,24 +159,6 @@ export class ActionExecutor { } const actionType = actionTypeRegistry.get(actionTypeId); - let validatedParams: Record; - let validatedConfig: Record; - let validatedSecrets: Record; - try { - validatedParams = validateParams(actionType, params); - validatedConfig = validateConfig(actionType, config); - validatedSecrets = validateSecrets(actionType, secrets); - if (actionType.validate?.connector) { - validateConnector(actionType, { - config, - secrets, - }); - } - } catch (err) { - span?.setOutcome('failure'); - return { status: 'error', actionId, message: err.message, retry: false }; - } - const actionLabel = `${actionTypeId}:${actionId}: ${name}`; logger.debug(`executing action ${actionLabel}`); @@ -221,6 +205,14 @@ export class ActionExecutor { let rawResult: ActionTypeExecutorResult; try { + const { validatedParams, validatedConfig, validatedSecrets } = validateAction({ + actionId, + actionType, + params, + config, + secrets, + }); + rawResult = await actionType.executor({ actionId, services, @@ -231,14 +223,19 @@ export class ActionExecutor { taskInfo, }); } catch (err) { - rawResult = { - actionId, - status: 'error', - message: 'an error occurred while running the action executor', - serviceMessage: err.message, - retry: false, - }; + if (err.reason === ActionExecutionErrorReason.Validation) { + rawResult = err.result; + } else { + rawResult = { + actionId, + status: 'error', + message: 'an error occurred while running the action', + serviceMessage: err.message, + retry: false, + }; + } } + eventLogger.stopTiming(event); // allow null-ish return to indicate success @@ -411,3 +408,38 @@ function actionErrorToMessage(result: ActionTypeExecutorResult): string return message; } + +interface ValidateActionOpts { + actionId: string; + actionType: ActionType; + params: Record; + config: unknown; + secrets: unknown; +} + +function validateAction({ actionId, actionType, params, config, secrets }: ValidateActionOpts) { + let validatedParams: Record; + let validatedConfig: Record; + let validatedSecrets: Record; + + try { + validatedParams = validateParams(actionType, params); + validatedConfig = validateConfig(actionType, config); + validatedSecrets = validateSecrets(actionType, secrets); + if (actionType.validate?.connector) { + validateConnector(actionType, { + config, + secrets, + }); + } + + return { validatedParams, validatedConfig, validatedSecrets }; + } catch (err) { + throw new ActionExecutionError(err.message, ActionExecutionErrorReason.Validation, { + actionId, + status: 'error', + message: err.message, + retry: false, + }); + } +} diff --git a/x-pack/plugins/actions/server/lib/errors/action_execution_error.ts b/x-pack/plugins/actions/server/lib/errors/action_execution_error.ts new file mode 100644 index 00000000000000..ad43008ef8e20f --- /dev/null +++ b/x-pack/plugins/actions/server/lib/errors/action_execution_error.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionTypeExecutorResult } from '../../types'; + +export enum ActionExecutionErrorReason { + Validation = 'validation', +} + +export class ActionExecutionError extends Error { + public readonly reason: ActionExecutionErrorReason; + public readonly result: ActionTypeExecutorResult; + + constructor( + message: string, + reason: ActionExecutionErrorReason, + result: ActionTypeExecutorResult + ) { + super(message); + this.reason = reason; + this.result = result; + } +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index 24c6427fdf2f69..c6330e660aa240 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -131,7 +131,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body).to.eql({ connector_id: createdAction.id, status: 'error', - message: 'an error occurred while running the action executor', + message: 'an error occurred while running the action', service_message: `expected failure for ${ES_TEST_INDEX_NAME} ${reference}`, retry: false, }); @@ -142,7 +142,7 @@ export default function ({ getService }: FtrProviderContext) { actionTypeId: 'test.failing', outcome: 'failure', message: `action execution failure: test.failing:${createdAction.id}: failing action`, - errorMessage: `an error occurred while running the action executor: expected failure for .kibana-alerting-test-data actions-failure-1:space1`, + errorMessage: `an error occurred while running the action: expected failure for .kibana-alerting-test-data actions-failure-1:space1`, }); }); @@ -325,7 +325,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body).to.eql({ actionId: createdAction.id, status: 'error', - message: 'an error occurred while running the action executor', + message: 'an error occurred while running the action', serviceMessage: `expected failure for ${ES_TEST_INDEX_NAME} ${reference}`, retry: false, }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts index 5e9387ad4f0f9c..6ae75c71d3bcf7 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts @@ -419,7 +419,7 @@ export default function createGetExecutionLogTests({ getService }: FtrProviderCo for (const errors of response.body.errors) { expect(errors.type).to.equal('actions'); expect(errors.message).to.equal( - `action execution failure: test.throw:${createdConnector.id}: connector that throws - an error occurred while running the action executor: this action is intended to fail` + `action execution failure: test.throw:${createdConnector.id}: connector that throws - an error occurred while running the action: this action is intended to fail` ); } }); From 2786899970ee161399e23f641fb58b71318fd1bf Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Fri, 6 May 2022 10:16:24 -0400 Subject: [PATCH 55/83] [Security Solution][Endpoint] Response actions console command definition interface redesign (#131355) - Removed `CommandServiceInterface` as well as `BuiltinCommandServiceInterface` - Refactored the command definition interface to store the React component constructor that will render the component - Internal `` state no longer stores JSX. It now stores the `Command` entered by the user and maintains component render state. Rendering of the component happens every time the console is shown. - `` component props were updated to simplify the interface --- .../console/components/bad_argument.tsx | 40 +-- .../builtin_commands/clear_command.tsx | 24 ++ .../builtin_commands/help_command.tsx | 39 +++ .../help_command_argument.tsx | 44 +++ .../components/command_execution_output.tsx | 139 +++----- .../console/components/command_usage.tsx | 30 +- .../console_manager/console_manager.test.tsx | 44 ++- .../components/console_manager/mocks.tsx | 4 +- .../console_state/console_state.tsx | 4 +- .../components/console_state/state_reducer.ts | 21 +- .../handle_execute_command.test.tsx | 26 +- .../handle_execute_command.tsx | 325 +++++++++--------- .../handle_update_command_state.ts | 66 ++++ .../console/components/console_state/types.ts | 43 ++- .../console/components/help_output.tsx | 61 +--- .../console/components/history_output.tsx | 16 +- .../console/components/unknow_comand.tsx | 52 ++- .../management/components/console/console.tsx | 5 +- ...nd_service.ts => use_with_command_list.ts} | 9 +- ...e.ts => use_with_custom_help_component.ts} | 6 +- .../management/components/console/index.ts | 2 +- .../management/components/console/mocks.tsx | 196 ++++++----- .../service/builtin_command_service.tsx | 102 ------ .../console/service/builtin_commands.tsx | 30 ++ .../service/types.builtin_command_service.ts | 27 -- .../management/components/console/types.ts | 74 +++- .../endpoint_console/endpoint_console.tsx | 25 -- .../endpoint_console_command_service.tsx | 22 -- .../components/endpoint_console/index.ts | 8 - .../pages/endpoint_hosts/view/dev_console.tsx | 214 ++++++++---- .../translations/translations/fr-FR.json | 3 - .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 33 files changed, 938 insertions(+), 769 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/clear_command.tsx create mode 100644 x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command.tsx create mode 100644 x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command_argument.tsx create mode 100644 x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_update_command_state.ts rename x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/{use_command_service.ts => use_with_command_list.ts} (58%) rename x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/{use_builtin_command_service.ts => use_with_custom_help_component.ts} (62%) delete mode 100644 x-pack/plugins/security_solution/public/management/components/console/service/builtin_command_service.tsx create mode 100644 x-pack/plugins/security_solution/public/management/components/console/service/builtin_commands.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/components/console/service/types.builtin_command_service.ts delete mode 100644 x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console_command_service.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/components/endpoint_console/index.ts diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx index 8ff4b71668fd43..2f4b4d241f48c0 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import React, { memo, PropsWithChildren } from 'react'; -import { EuiCallOut, EuiText } from '@elastic/eui'; -import { UserCommandInput } from './user_command_input'; +import React, { memo, PropsWithChildren, useEffect } from 'react'; +import { EuiCallOut } from '@elastic/eui'; import { ParsedCommandInput } from '../service/parsed_command_input'; -import { CommandDefinition } from '../types'; +import { CommandDefinition, CommandExecutionComponentProps } from '../types'; import { CommandInputUsage } from './command_usage'; import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; @@ -19,21 +18,22 @@ export type BadArgumentProps = PropsWithChildren<{ commandDefinition: CommandDefinition; }>; -export const BadArgument = memo( - ({ parsedInput, commandDefinition, children = null }) => { - const getTestId = useTestIdGenerator(useDataTestSubj()); +/** + * Shows a bad argument error. The error message needs to be defined via the Command History Item's + * `state.errorMessage` + */ +export const BadArgument = memo(({ command, setStatus, store }) => { + const getTestId = useTestIdGenerator(useDataTestSubj()); + + useEffect(() => { + setStatus('success'); + }, [setStatus]); - return ( - <> - - - - - {children} - - - - ); - } -); + return ( + + {store.errorMessage} + + + ); +}); BadArgument.displayName = 'BadArgument'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/clear_command.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/clear_command.tsx new file mode 100644 index 00000000000000..bfa06f55d26659 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/clear_command.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { memo, useEffect } from 'react'; +import { useConsoleStateDispatch } from '../../hooks/state_selectors/use_console_state_dispatch'; +import { CommandExecutionComponentProps } from '../../types'; + +export const ClearCommand = memo(({ status, setStatus }) => { + const dispatch = useConsoleStateDispatch(); + + useEffect(() => { + if (status === 'pending') { + dispatch({ type: 'clear' }); + } + setStatus('success'); + }, [status, setStatus, dispatch]); + + return null; +}); +ClearCommand.displayName = 'ClearCommand'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command.tsx new file mode 100644 index 00000000000000..f8c66f31e396d5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { memo, useEffect } from 'react'; +import { useWithCustomHelpComponent } from '../../hooks/state_selectors/use_with_custom_help_component'; +import { CommandList } from '../command_list'; +import { useWithCommandList } from '../../hooks/state_selectors/use_with_command_list'; +import type { CommandExecutionComponentProps } from '../../types'; +import { HelpOutput } from '../help_output'; + +export const HelpCommand = memo((props) => { + const commands = useWithCommandList(); + const CustomHelpComponent = useWithCustomHelpComponent(); + + useEffect(() => { + if (!CustomHelpComponent) { + props.setStatus('success'); + } + }, [CustomHelpComponent, props]); + + return CustomHelpComponent ? ( + + ) : ( + + + + ); +}); +HelpCommand.displayName = 'HelpCommand'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command_argument.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command_argument.tsx new file mode 100644 index 00000000000000..f67c44013d059b --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command_argument.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useEffect } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { CommandUsage } from '../command_usage'; +import { HelpOutput } from '../help_output'; +import { CommandExecutionComponentProps } from '../../types'; + +/** + * Builtin component that handles the output of command's `--help` argument + */ +export const HelpCommandArgument = memo((props) => { + const CustomCommandHelp = props.command.commandDefinition.HelpComponent; + + useEffect(() => { + if (!CustomCommandHelp) { + props.setStatus('success'); + } + }, [CustomCommandHelp, props]); + + return CustomCommandHelp ? ( + + ) : ( + + + + ); +}); +HelpCommandArgument.displayName = 'HelpCommandArgument'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_output.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_output.tsx index 8bb97699809142..8a6611ffbbb18f 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_output.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_output.tsx @@ -5,103 +5,74 @@ * 2.0. */ -import React, { memo, ReactNode, useCallback, useEffect, useState } from 'react'; -import { EuiButton, EuiLoadingChart } from '@elastic/eui'; +import React, { memo, useCallback, useMemo } from 'react'; +import { EuiLoadingChart } from '@elastic/eui'; import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { CommandExecutionFailure } from './command_execution_failure'; +import type { CommandExecutionState, CommandHistoryItem } from './console_state/types'; import { UserCommandInput } from './user_command_input'; -import { Command } from '../types'; -import { useCommandService } from '../hooks/state_selectors/use_command_service'; import { useConsoleStateDispatch } from '../hooks/state_selectors/use_console_state_dispatch'; const CommandOutputContainer = styled.div` position: relative; - - .run-in-background { - position: absolute; - right: 0; - top: 1em; - } `; export interface CommandExecutionOutputProps { - command: Command; + item: CommandHistoryItem; } -export const CommandExecutionOutput = memo(({ command }) => { - const commandService = useCommandService(); - const [isRunning, setIsRunning] = useState(true); - const [output, setOutput] = useState(null); - const dispatch = useConsoleStateDispatch(); - - // FIXME:PT implement the `run in the background` functionality - const [showRunInBackground, setShowRunInTheBackground] = useState(false); - const handleRunInBackgroundClick = useCallback(() => { - setShowRunInTheBackground(false); - }, []); - - useEffect(() => { - (async () => { - const timeoutId = setTimeout(() => { - setShowRunInTheBackground(true); - }, 15000); +export const CommandExecutionOutput = memo( + ({ item: { command, state, id } }) => { + const dispatch = useConsoleStateDispatch(); + const RenderComponent = command.commandDefinition.RenderComponent; - try { - const commandOutput = await commandService.executeCommand(command); - setOutput(commandOutput.result); + const isRunning = useMemo(() => { + return state.status === 'pending'; + }, [state.status]); - // FIXME: PT the console should scroll the bottom as well - } catch (error) { - setOutput(); - } + /** Updates the Command's status */ + const setCommandStatus = useCallback( + (status: CommandExecutionState['status']) => { + dispatch({ + type: 'updateCommandStatusState', + payload: { + id, + value: status, + }, + }); + }, + [dispatch, id] + ); - clearTimeout(timeoutId); - setIsRunning(false); - setShowRunInTheBackground(false); - })(); - }, [command, commandService]); + /** Updates the Command's execution store */ + const setCommandStore = useCallback( + (store) => { + dispatch({ + type: 'updateCommandStoreState', + payload: { + id, + value: store, + }, + }); + }, + [dispatch, id] + ); - useEffect(() => { - if (!isRunning) { - dispatch({ type: 'scrollDown' }); - } - }, [isRunning, dispatch]); - - return ( - - {showRunInBackground && ( -
- - - + return ( + +
+ + {isRunning && } +
+
+
- )} -
- - {isRunning && ( - <> - - - )} -
-
{output}
-
- ); -}); + + ); + } +); CommandExecutionOutput.displayName = 'CommandExecutionOutput'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx index 9d17d83f0266f9..68b2aab558d836 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx @@ -79,22 +79,20 @@ export const CommandUsage = memo(({ commandDef }) => { {hasArgs && ( <> -

- - - {commandDef.mustHaveArgs && commandDef.args && hasArgs && ( - - - - )} - -

+ + + {commandDef.mustHaveArgs && commandDef.args && hasArgs && ( + + + + )} + {commandDef.args && ( { it("should persist a console's command output history on hide/show", async () => { await render(); enterConsoleCommand(renderResult, 'help', { dataTestSubj: 'testRunningConsole' }); - enterConsoleCommand(renderResult, 'help', { dataTestSubj: 'testRunningConsole' }); + enterConsoleCommand(renderResult, 'cmd1', { dataTestSubj: 'testRunningConsole' }); await waitFor(() => { expect(renderResult.queryAllByTestId('testRunningConsole-historyItem')).toHaveLength(2); }); + // Hide the console userEvent.click(renderResult.getByTestId('consolePopupHideButton')); await waitFor(() => { expect( @@ -317,6 +318,7 @@ describe('When using ConsoleManager', () => { ).toBe(true); }); + // Open the console back up and ensure prior items still there await openRunningConsole(); await waitFor(() => { @@ -324,6 +326,46 @@ describe('When using ConsoleManager', () => { }); }); + it('should provide console rendering state between show/hide', async () => { + const expectedStoreValue = JSON.stringify({ foo: 'bar' }, null, 2); + await render(); + enterConsoleCommand(renderResult, 'cmd1', { dataTestSubj: 'testRunningConsole' }); + + // Command should have `pending` status and no store values + expect(renderResult.getByTestId('exec-output-statusState').textContent).toEqual( + 'status: pending' + ); + expect(renderResult.getByTestId('exec-output-storeStateJson').textContent).toEqual('{}'); + + // Wait for component to update the status and store values + await waitFor(() => { + expect(renderResult.getByTestId('exec-output-statusState').textContent).toMatch( + 'status: success' + ); + }); + expect(renderResult.getByTestId('exec-output-storeStateJson').textContent).toEqual( + expectedStoreValue + ); + + // Hide the console + userEvent.click(renderResult.getByTestId('consolePopupHideButton')); + await waitFor(() => { + expect( + renderResult.getByTestId('consolePopupWrapper').classList.contains('is-hidden') + ).toBe(true); + }); + + // Open the console back up and ensure `status` and `store` are the last set of values + await openRunningConsole(); + + expect(renderResult.getByTestId('exec-output-statusState').textContent).toMatch( + 'status: success' + ); + expect(renderResult.getByTestId('exec-output-storeStateJson').textContent).toEqual( + expectedStoreValue + ); + }); + describe('and the terminate confirmation is shown', () => { const clickOnTerminateButton = async () => { userEvent.click(renderResult.getByTestId('consolePopupTerminateButton')); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/mocks.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/mocks.tsx index 57ec4246caf415..0b841f4118d1f4 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/mocks.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/mocks.tsx @@ -9,7 +9,7 @@ import React, { memo, useCallback } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { ConsoleRegistrationInterface, RegisteredConsoleClient } from './types'; import { useConsoleManager } from './console_manager'; -import { getCommandServiceMock } from '../../mocks'; +import { getCommandListMock } from '../../mocks'; export const getNewConsoleRegistrationMock = ( overrides: Partial = {} @@ -20,7 +20,7 @@ export const getNewConsoleRegistrationMock = ( meta: { about: 'for unit testing ' }, consoleProps: { 'data-test-subj': 'testRunningConsole', - commandService: getCommandServiceMock(), + commands: getCommandListMock(), }, onBeforeTerminate: jest.fn(), ...overrides, diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/console_state.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/console_state.tsx index 852b2b1ab58fe2..66c874e4e27a87 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/console_state.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/console_state.tsx @@ -17,10 +17,10 @@ type ConsoleStateProviderProps = PropsWithChildren<{}> & InitialStateInterface; * A Console wide data store for internal state management between inner components */ export const ConsoleStateProvider = memo( - ({ commandService, scrollToBottom, dataTestSubj, children }) => { + ({ commands, scrollToBottom, HelpComponent, dataTestSubj, children }) => { const [state, dispatch] = useReducer( stateDataReducer, - { commandService, scrollToBottom, dataTestSubj }, + { commands, scrollToBottom, HelpComponent, dataTestSubj }, initiateState ); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_reducer.ts b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_reducer.ts index 94175d9821ae72..68024aa5b7cfcf 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_reducer.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_reducer.ts @@ -5,26 +5,28 @@ * 2.0. */ -import { ConsoleDataState, ConsoleStoreReducer } from './types'; +import { handleUpdateCommandState } from './state_update_handlers/handle_update_command_state'; +import type { ConsoleDataState, ConsoleStoreReducer } from './types'; import { handleExecuteCommand } from './state_update_handlers/handle_execute_command'; -import { ConsoleBuiltinCommandsService } from '../../service/builtin_command_service'; +import { getBuiltinCommands } from '../../service/builtin_commands'; export type InitialStateInterface = Pick< ConsoleDataState, - 'commandService' | 'scrollToBottom' | 'dataTestSubj' + 'commands' | 'scrollToBottom' | 'dataTestSubj' | 'HelpComponent' >; export const initiateState = ({ - commandService, + commands, scrollToBottom, dataTestSubj, + HelpComponent, }: InitialStateInterface): ConsoleDataState => { return { - commandService, + commands: getBuiltinCommands().concat(commands), scrollToBottom, + HelpComponent, dataTestSubj, commandHistory: [], - builtinCommandService: new ConsoleBuiltinCommandsService(), }; }; @@ -36,6 +38,13 @@ export const stateDataReducer: ConsoleStoreReducer = (state, action) => { case 'executeCommand': return handleExecuteCommand(state, action); + + case 'updateCommandStatusState': + case 'updateCommandStoreState': + return handleUpdateCommandState(state, action); + + case 'clear': + return { ...state, commandHistory: [] }; } return state; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.test.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.test.tsx index 06ecc344d55965..d19376395742f8 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.test.tsx @@ -15,13 +15,13 @@ import { ConsoleProps } from '../../../types'; describe('When a Console command is entered by the user', () => { let render: (props?: Partial) => ReturnType; let renderResult: ReturnType; - let commandServiceMock: ConsoleTestSetup['commandServiceMock']; + let commands: ConsoleTestSetup['commands']; let enterCommand: ConsoleTestSetup['enterCommand']; beforeEach(() => { const testSetup = getConsoleTestSetup(); - ({ commandServiceMock, enterCommand } = testSetup); + ({ commands, enterCommand } = testSetup); render = (props = {}) => (renderResult = testSetup.renderConsole(props)); }); @@ -34,18 +34,16 @@ describe('When a Console command is entered by the user', () => { await waitFor(() => { expect(renderResult.getAllByTestId('test-commandList-command')).toHaveLength( // `+2` to account for builtin commands - commandServiceMock.getCommandList().length + 2 + commands.length + 2 ); }); }); it('should display custom help output when Command service has `getHelp()` defined', async () => { - commandServiceMock.getHelp = async () => { - return { - result:
{'help output'}
, - }; + const HelpComponent: React.FunctionComponent = () => { + return
{'help output'}
; }; - render(); + render({ HelpComponent }); enterCommand('help'); await waitFor(() => { @@ -73,11 +71,15 @@ describe('When a Console command is entered by the user', () => { }); it('should should custom command `--help` output when Command service defines `getCommandUsage()`', async () => { - commandServiceMock.getCommandUsage = async () => { - return { - result:
{'command help here'}
, + const cmd2 = commands.find((command) => command.name === 'cmd2'); + + if (cmd2) { + cmd2.HelpComponent = () => { + return
{'command help here'}
; }; - }; + cmd2.HelpComponent.displayName = 'HelpComponent'; + } + render(); enterCommand('cmd2 --help'); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx index 2815ec46059171..c387cf3d90a8f6 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx @@ -5,19 +5,19 @@ * 2.0. */ -/* eslint complexity: ["error", 40]*/ -// FIXME:PT remove the complexity - -import React from 'react'; import { i18n } from '@kbn/i18n'; -import { ConsoleDataAction, ConsoleDataState, ConsoleStoreReducer } from '../types'; +import { v4 as uuidV4 } from 'uuid'; +import { HelpCommandArgument } from '../../builtin_commands/help_command_argument'; +import { + CommandHistoryItem, + ConsoleDataAction, + ConsoleDataState, + ConsoleStoreReducer, +} from '../types'; import { parseCommandInput } from '../../../service/parsed_command_input'; -import { HistoryItem } from '../../history_item'; import { UnknownCommand } from '../../unknow_comand'; -import { HelpOutput } from '../../help_output'; import { BadArgument } from '../../bad_argument'; -import { CommandExecutionOutput } from '../../command_execution_output'; -import { CommandDefinition } from '../../../types'; +import { Command, CommandDefinition, CommandExecutionComponentProps } from '../../../types'; const toCliArgumentOption = (argName: string) => `--${argName}`; @@ -41,6 +41,36 @@ const updateStateWithNewCommandHistoryItem = ( }; }; +const UnknownCommandDefinition: CommandDefinition = { + name: 'unknown-command', + about: 'unknown command', + RenderComponent: () => null, +}; + +const createCommandExecutionState = ( + store: CommandExecutionComponentProps['store'] = {} +): CommandHistoryItem['state'] => { + return { + status: 'pending', + store, + }; +}; + +const cloneCommandDefinitionWithNewRenderComponent = ( + command: Command, + RenderComponent: CommandDefinition['RenderComponent'] +): Command => { + return { + ...command, + commandDefinition: { + ...command.commandDefinition, + // We use the original command definition, but replace + // the RenderComponent for this invocation + RenderComponent, + }, + }; +}; + export const handleExecuteCommand: ConsoleStoreReducer< ConsoleDataAction & { type: 'executeCommand' } > = (state, action) => { @@ -50,116 +80,98 @@ export const handleExecuteCommand: ConsoleStoreReducer< return state; } - const { commandService, builtinCommandService } = state; - - // Is it an internal command? - if (builtinCommandService.isBuiltin(parsedInput.name)) { - const commandOutput = builtinCommandService.executeBuiltinCommand(parsedInput, commandService); - - if (commandOutput.clearBuffer) { - return { - ...state, - commandHistory: [], - }; - } - - return updateStateWithNewCommandHistoryItem(state, commandOutput.result); - } - - // ---------------------------------------------------- - // Validate and execute the user defined command - // ---------------------------------------------------- - const commandDefinition = commandService - .getCommandList() - .find((definition) => definition.name === parsedInput.name); + const { commands } = state; + const commandDefinition: CommandDefinition | undefined = commands.find( + (definition) => definition.name === parsedInput.name + ); // Unknown command if (!commandDefinition) { - return updateStateWithNewCommandHistoryItem( - state, - - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: { + input: parsedInput.input, + args: parsedInput, + commandDefinition: { + ...UnknownCommandDefinition, + RenderComponent: UnknownCommand, + }, + }, + state: createCommandExecutionState(), + }); } + const command = { + input: parsedInput.input, + args: parsedInput, + commandDefinition, + }; const requiredArgs = getRequiredArguments(commandDefinition.args); // If args were entered, then validate them if (parsedInput.hasArgs()) { // Show command help if (parsedInput.hasArg('help')) { - return updateStateWithNewCommandHistoryItem( - state, - - - {(commandService.getCommandUsage || builtinCommandService.getCommandUsage)( - commandDefinition - )} - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, HelpCommandArgument), + state: createCommandExecutionState(), + }); } // Command supports no arguments if (!commandDefinition.args || Object.keys(commandDefinition.args).length === 0) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate( - 'xpack.securitySolution.console.commandValidation.noArgumentsSupported', - { - defaultMessage: 'command does not support any arguments', - } - )} - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.noArgumentsSupported', + { + defaultMessage: 'command does not support any arguments', + } + ), + }), + }); } // no unknown arguments allowed? if (parsedInput.unknownArgs && parsedInput.unknownArgs.length) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate('xpack.securitySolution.console.commandValidation.unknownArgument', { + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.unknownArgument', + { defaultMessage: 'unknown argument(s): {unknownArgs}', values: { unknownArgs: parsedInput.unknownArgs.join(', '), }, - })} - - - ); + } + ), + }), + }); } // Missing required Arguments for (const requiredArg of requiredArgs) { if (!parsedInput.args[requiredArg]) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate( - 'xpack.securitySolution.console.commandValidation.missingRequiredArg', - { - defaultMessage: 'missing required argument: {argName}', - values: { - argName: toCliArgumentOption(requiredArg), - }, - } - )} - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.missingRequiredArg', + { + defaultMessage: 'missing required argument: {argName}', + values: { + argName: toCliArgumentOption(requiredArg), + }, + } + ), + }), + }); } } @@ -170,17 +182,19 @@ export const handleExecuteCommand: ConsoleStoreReducer< // Unknown argument if (!argDefinition) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate('xpack.securitySolution.console.commandValidation.unsupportedArg', { + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.unsupportedArg', + { defaultMessage: 'unsupported argument: {argName}', values: { argName: toCliArgumentOption(argName) }, - })} - - - ); + } + ), + }), + }); } // does not allow multiple values @@ -189,81 +203,76 @@ export const handleExecuteCommand: ConsoleStoreReducer< Array.isArray(argInput.values) && argInput.values.length > 0 ) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate( - 'xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce', - { - defaultMessage: 'argument can only be used once: {argName}', - values: { argName: toCliArgumentOption(argName) }, - } - )} - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce', + { + defaultMessage: 'argument can only be used once: {argName}', + values: { argName: toCliArgumentOption(argName) }, + } + ), + }), + }); } if (argDefinition.validate) { const validationResult = argDefinition.validate(argInput); if (validationResult !== true) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate( - 'xpack.securitySolution.console.commandValidation.invalidArgValue', - { - defaultMessage: 'invalid argument value: {argName}. {error}', - values: { argName: toCliArgumentOption(argName), error: validationResult }, - } - )} - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.invalidArgValue', + { + defaultMessage: 'invalid argument value: {argName}. {error}', + values: { argName: toCliArgumentOption(argName), error: validationResult }, + } + ), + }), + }); } } } } else if (requiredArgs.length > 0) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate('xpack.securitySolution.console.commandValidation.mustHaveArgs', { + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.mustHaveArgs', + { defaultMessage: 'missing required arguments: {requiredArgs}', values: { requiredArgs: requiredArgs.map((argName) => toCliArgumentOption(argName)).join(', '), }, - })} - - - ); + } + ), + }), + }); } else if (commandDefinition.mustHaveArgs) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate('xpack.securitySolution.console.commandValidation.oneArgIsRequired', { + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.oneArgIsRequired', + { defaultMessage: 'at least one argument must be used', - })} - - - ); + } + ), + }), + }); } // All is good. Execute the command - return updateStateWithNewCommandHistoryItem( - state, - - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command, + state: createCommandExecutionState(), + }); }; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_update_command_state.ts b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_update_command_state.ts new file mode 100644 index 00000000000000..8e176019990a36 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_update_command_state.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + CommandExecutionState, + CommandHistoryItem, + ConsoleDataAction, + ConsoleStoreReducer, +} from '../types'; + +type UpdateCommandStateAction = ConsoleDataAction & { + type: 'updateCommandStoreState' | 'updateCommandStatusState'; +}; + +export const handleUpdateCommandState: ConsoleStoreReducer = ( + state, + { type, payload: { id, value } } +) => { + let foundIt = false; + const updatedCommandHistory = state.commandHistory.map((item) => { + if (foundIt || item.id !== id) { + return item; + } + + foundIt = true; + + const updatedCommandState: CommandHistoryItem = { + ...item, + state: { + ...item.state, + }, + }; + + switch (type) { + case 'updateCommandStoreState': + updatedCommandState.state.store = value as CommandExecutionState['store']; + break; + case 'updateCommandStatusState': + // If the status was not changed, then there is nothing to be done here, so + // instead of triggering a state change (and UI re-render), just return the + // original item; + if (updatedCommandState.state.status === value) { + foundIt = false; + return item; + } + + updatedCommandState.state.status = value as CommandExecutionState['status']; + break; + } + + return updatedCommandState; + }); + + if (foundIt) { + return { + ...state, + commandHistory: updatedCommandHistory, + }; + } + + return state; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/types.ts b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/types.ts index 72810d31e3248b..356033e147c563 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/types.ts @@ -5,28 +5,51 @@ * 2.0. */ -import { Dispatch, Reducer } from 'react'; -import { CommandServiceInterface } from '../../types'; -import { HistoryItemComponent } from '../history_item'; -import { BuiltinCommandServiceInterface } from '../../service/types.builtin_command_service'; +import type { Dispatch, Reducer } from 'react'; +import type { Command, CommandDefinition, CommandExecutionComponent } from '../../types'; export interface ConsoleDataState { - /** Command service defined on input to the `Console` component by consumers of the component */ - commandService: CommandServiceInterface; - /** Command service for builtin console commands */ - builtinCommandService: BuiltinCommandServiceInterface; + /** + * Commands available in the console, which includes both the builtin command and the ones + * defined on input to the `Console` component by consumers of the component + */ + commands: CommandDefinition[]; + /** UI function that scrolls the console down to the bottom */ scrollToBottom: () => void; + /** * List of commands entered by the user and being shown in the UI */ - commandHistory: Array>; + commandHistory: CommandHistoryItem[]; + /** Component defined on input to the Console that will handle the `help` command */ + HelpComponent?: CommandExecutionComponent; dataTestSubj?: string; } +export interface CommandHistoryItem { + id: string; + command: Command; + state: CommandExecutionState; +} + +export interface CommandExecutionState { + status: 'pending' | 'success' | 'error'; + store: Record; +} + export type ConsoleDataAction = | { type: 'scrollDown' } - | { type: 'executeCommand'; payload: { input: string } }; + | { type: 'executeCommand'; payload: { input: string } } + | { type: 'clear' } + | { + type: 'updateCommandStoreState'; + payload: { id: string; value: CommandExecutionState['store'] }; + } + | { + type: 'updateCommandStatusState'; + payload: { id: string; value: CommandExecutionState['status'] }; + }; export interface ConsoleStore { state: ConsoleDataState; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/help_output.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/help_output.tsx index b0a2217e169c43..597d979e00034a 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/help_output.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/help_output.tsx @@ -5,55 +5,30 @@ * 2.0. */ -import React, { memo, ReactNode, useEffect, useState } from 'react'; -import { EuiCallOut, EuiCallOutProps, EuiLoadingChart } from '@elastic/eui'; -import { UserCommandInput } from './user_command_input'; -import { CommandExecutionFailure } from './command_execution_failure'; +import React, { memo, PropsWithChildren, ReactNode } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { MaybeImmutable } from '../../../../../common/endpoint/types'; +import { Command } from '..'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj'; -export interface HelpOutputProps extends Pick { - input: string; - children: ReactNode | Promise<{ result: ReactNode }>; -} -export const HelpOutput = memo(({ input, children, ...euiCalloutProps }) => { - const [content, setContent] = useState(); +type HelpOutputProps = PropsWithChildren<{ + command: MaybeImmutable; + title?: ReactNode; +}>; +export const HelpOutput = memo(({ title, children }) => { const getTestId = useTestIdGenerator(useDataTestSubj()); - useEffect(() => { - if (children instanceof Promise) { - (async () => { - try { - const response = await (children as Promise<{ - result: ReactNode; - }>); - setContent(response.result); - } catch (error) { - setContent(); - } - })(); - - return; - } - - setContent(children); - }, [children]); - return ( -
-
- -
- - {content} - -
+ + {children} + ); }); HelpOutput.displayName = 'HelpOutput'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/history_output.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/history_output.tsx index 088a6fac57ae4e..cd03f9d39a39d9 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/history_output.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/history_output.tsx @@ -5,12 +5,14 @@ * 2.0. */ -import React, { memo, useEffect } from 'react'; +import React, { memo, useEffect, useMemo } from 'react'; import { CommonProps, EuiFlexGroup } from '@elastic/eui'; +import { CommandExecutionOutput } from './command_execution_output'; import { useCommandHistory } from '../hooks/state_selectors/use_command_history'; import { useConsoleStateDispatch } from '../hooks/state_selectors/use_console_state_dispatch'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj'; +import { HistoryItem } from './history_item'; export type OutputHistoryProps = CommonProps; @@ -19,6 +21,16 @@ export const HistoryOutput = memo((commonProps) => { const dispatch = useConsoleStateDispatch(); const getTestId = useTestIdGenerator(useDataTestSubj()); + const historyBody = useMemo(() => { + return historyItems.map((historyItem) => { + return ( + + + + ); + }); + }, [historyItems]); + // Anytime we add a new item to the history // scroll down so that command input remains visible useEffect(() => { @@ -34,7 +46,7 @@ export const HistoryOutput = memo((commonProps) => { alignItems="flexEnd" responsive={false} > - {historyItems} + {historyBody} ); }); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/unknow_comand.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/unknow_comand.tsx index 5529457cbb05a5..8397c7727de812 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/unknow_comand.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/unknow_comand.tsx @@ -5,42 +5,38 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useEffect } from 'react'; import { EuiCallOut, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { UserCommandInput } from './user_command_input'; +import { CommandExecutionComponentProps } from '../types'; import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; -export interface UnknownCommand { - input: string; -} -export const UnknownCommand = memo(({ input }) => { +export const UnknownCommand = memo(({ setStatus }) => { const getTestId = useTestIdGenerator(useDataTestSubj()); + useEffect(() => { + setStatus('success'); + }, [setStatus]); + return ( - <> -
- -
- - - - - - {'help'}, - }} - /> - - - + + + + + + {'help'}, + }} + /> + + ); }); UnknownCommand.displayName = 'UnknownCommand'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/console.tsx b/x-pack/plugins/security_solution/public/management/components/console/console.tsx index 0f3645037df020..874dbc2eabae06 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/console.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/console.tsx @@ -45,7 +45,7 @@ const ConsoleWindow = styled.div` `; export const Console = memo( - ({ prompt, commandService, managedKey, ...commonProps }) => { + ({ prompt, commands, HelpComponent, managedKey, ...commonProps }) => { const consoleWindowRef = useRef(null); const inputFocusRef: CommandInputProps['focusRef'] = useRef(null); const getTestId = useTestIdGenerator(commonProps['data-test-subj']); @@ -72,8 +72,9 @@ export const Console = memo( return ( {/* diff --git a/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_command_service.ts b/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_with_command_list.ts similarity index 58% rename from x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_command_service.ts rename to x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_with_command_list.ts index 66ce0c2b5eb439..4f63c55a36098b 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_command_service.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_with_command_list.ts @@ -6,8 +6,11 @@ */ import { useConsoleStore } from '../../components/console_state/console_state'; -import { CommandServiceInterface } from '../../types'; +import type { CommandDefinition } from '../../types'; -export const useCommandService = (): CommandServiceInterface => { - return useConsoleStore().state.commandService; +/** + * Returns the Command service that the console was provided on input + */ +export const useWithCommandList = (): CommandDefinition[] => { + return useConsoleStore().state.commands; }; diff --git a/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_builtin_command_service.ts b/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_with_custom_help_component.ts similarity index 62% rename from x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_builtin_command_service.ts rename to x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_with_custom_help_component.ts index 22167d50667433..b90e5166c81d73 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_builtin_command_service.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_with_custom_help_component.ts @@ -6,8 +6,8 @@ */ import { useConsoleStore } from '../../components/console_state/console_state'; -import { CommandServiceInterface } from '../../types'; +import { ConsoleDataState } from '../../components/console_state/types'; -export const useCommandService = (): CommandServiceInterface => { - return useConsoleStore().state.builtinCommandService; +export const useWithCustomHelpComponent = (): ConsoleDataState['HelpComponent'] => { + return useConsoleStore().state.HelpComponent; }; diff --git a/x-pack/plugins/security_solution/public/management/components/console/index.ts b/x-pack/plugins/security_solution/public/management/components/console/index.ts index 4264aa5a8f8301..1603d4b15f3536 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/index.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/index.ts @@ -7,7 +7,7 @@ export { Console } from './console'; export { ConsoleManager, useConsoleManager } from './components/console_manager'; -export type { CommandServiceInterface, CommandDefinition, Command, ConsoleProps } from './types'; +export type { CommandDefinition, Command, ConsoleProps } from './types'; export type { ConsoleRegistrationInterface, RegisteredConsoleClient, diff --git a/x-pack/plugins/security_solution/public/management/components/console/mocks.tsx b/x-pack/plugins/security_solution/public/management/components/console/mocks.tsx index d89c5f5374d473..ea24a174498ddf 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/mocks.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/mocks.tsx @@ -7,20 +7,19 @@ /* eslint-disable import/no-extraneous-dependencies */ -import React from 'react'; +import React, { useEffect } from 'react'; import { EuiCode } from '@elastic/eui'; import userEvent from '@testing-library/user-event'; import { act } from '@testing-library/react'; import { Console } from './console'; -import type { Command, CommandServiceInterface, ConsoleProps } from './types'; +import type { ConsoleProps, CommandDefinition, CommandExecutionComponent } from './types'; import type { AppContextTestRender } from '../../../common/mock/endpoint'; import { createAppRootMockRenderer } from '../../../common/mock/endpoint'; -import { CommandDefinition } from './types'; export interface ConsoleTestSetup { renderConsole(props?: Partial): ReturnType; - commandServiceMock: jest.Mocked; + commands: CommandDefinition[]; enterCommand( cmd: string, @@ -74,25 +73,16 @@ export const getConsoleTestSetup = (): ConsoleTestSetup => { let renderResult: ReturnType; - const commandServiceMock = getCommandServiceMock(); + const commandList = getCommandListMock(); const renderConsole: ConsoleTestSetup['renderConsole'] = ({ prompt = '$$>', - commandService = commandServiceMock, + commands = commandList, 'data-test-subj': dataTestSubj = 'test', ...others } = {}) => { - if (commandService !== commandServiceMock) { - throw new Error('Must use CommandService provided by test setup'); - } - return (renderResult = mockedContext.render( - + )); }; @@ -102,91 +92,107 @@ export const getConsoleTestSetup = (): ConsoleTestSetup => { return { renderConsole, - commandServiceMock, + commands: commandList, enterCommand, }; }; -export const getCommandServiceMock = (): jest.Mocked => { - return { - getCommandList: jest.fn(() => { - const commands: CommandDefinition[] = [ - { - name: 'cmd1', - about: 'a command with no options', - }, - { - name: 'cmd2', - about: 'runs cmd 2', - args: { - file: { - about: 'Includes file in the run', - required: true, - allowMultiples: false, - validate: () => { - return true; - }, - }, - ext: { - about: 'optional argument', - required: false, - allowMultiples: false, - }, - bad: { - about: 'will fail validation', - required: false, - allowMultiples: false, - validate: () => 'This is a bad value', - }, +export const getCommandListMock = (): CommandDefinition[] => { + const RenderComponent: CommandExecutionComponent = ({ + command, + status, + setStatus, + setStore, + store, + }) => { + useEffect(() => { + if (status !== 'success') { + new Promise((r) => setTimeout(r, 500)).then(() => { + setStatus('success'); + setStore({ foo: 'bar' }); + }); + } + }, [setStatus, setStore, status]); + + return ( +
+
{`${command.commandDefinition.name}`}
+
{`command input: ${command.input}`}
+ + {JSON.stringify(command.args, null, 2)} + +
{'Command render state:'}
+
{`status: ${status}`}
+ + {JSON.stringify(store, null, 2)} + +
+ ); + }; + + const commands: CommandDefinition[] = [ + { + name: 'cmd1', + about: 'a command with no options', + RenderComponent: jest.fn(RenderComponent), + }, + { + name: 'cmd2', + about: 'runs cmd 2', + RenderComponent: jest.fn(RenderComponent), + args: { + file: { + about: 'Includes file in the run', + required: true, + allowMultiples: false, + validate: () => { + return true; }, }, - { - name: 'cmd3', - about: 'allows argument to be used multiple times', - args: { - foo: { - about: 'foo stuff', - required: true, - allowMultiples: true, - }, - }, + ext: { + about: 'optional argument', + required: false, + allowMultiples: false, }, - { - name: 'cmd4', - about: 'all options optinal, but at least one is required', - mustHaveArgs: true, - args: { - foo: { - about: 'foo stuff', - required: false, - allowMultiples: true, - }, - bar: { - about: 'bar stuff', - required: false, - allowMultiples: true, - }, - }, + bad: { + about: 'will fail validation', + required: false, + allowMultiples: false, + validate: () => 'This is a bad value', }, - ]; - - return commands; - }), - - executeCommand: jest.fn(async (command: Command) => { - await new Promise((r) => setTimeout(r, 1)); - - return { - result: ( -
-
{`${command.commandDefinition.name}`}
-
{`command input: ${command.input}`}
- - {JSON.stringify(command.args, null, 2)} - -
- ), - }; - }), - }; + }, + }, + { + name: 'cmd3', + about: 'allows argument to be used multiple times', + RenderComponent: jest.fn(RenderComponent), + args: { + foo: { + about: 'foo stuff', + required: true, + allowMultiples: true, + }, + }, + }, + { + name: 'cmd4', + about: 'all options optional, but at least one is required', + RenderComponent: jest.fn(RenderComponent), + mustHaveArgs: true, + args: { + foo: { + about: 'foo stuff', + required: false, + allowMultiples: true, + }, + bar: { + about: 'bar stuff', + required: false, + allowMultiples: true, + }, + }, + }, + ]; + + return commands; }; diff --git a/x-pack/plugins/security_solution/public/management/components/console/service/builtin_command_service.tsx b/x-pack/plugins/security_solution/public/management/components/console/service/builtin_command_service.tsx deleted file mode 100644 index 6cd8af0dc6eff6..00000000000000 --- a/x-pack/plugins/security_solution/public/management/components/console/service/builtin_command_service.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { ReactNode } from 'react'; -import { i18n } from '@kbn/i18n'; -import { HistoryItem, HistoryItemComponent } from '../components/history_item'; -import { HelpOutput } from '../components/help_output'; -import { ParsedCommandInput } from './parsed_command_input'; -import { CommandList } from '../components/command_list'; -import { CommandUsage } from '../components/command_usage'; -import { Command, CommandDefinition, CommandServiceInterface } from '../types'; -import { BuiltinCommandServiceInterface } from './types.builtin_command_service'; - -const builtInCommands = (): CommandDefinition[] => { - return [ - { - name: 'help', - about: i18n.translate('xpack.securitySolution.console.builtInCommands.helpAbout', { - defaultMessage: 'View list of available commands', - }), - }, - { - name: 'clear', - about: i18n.translate('xpack.securitySolution.console.builtInCommands.clearAbout', { - defaultMessage: 'Clear the console buffer', - }), - }, - ]; -}; - -export class ConsoleBuiltinCommandsService implements BuiltinCommandServiceInterface { - constructor(private commandList = builtInCommands()) {} - - getCommandList(): CommandDefinition[] { - return this.commandList; - } - - async executeCommand(command: Command): Promise<{ result: ReactNode }> { - return { - result: null, - }; - } - - executeBuiltinCommand( - parsedInput: ParsedCommandInput, - contextConsoleService: CommandServiceInterface - ): { result: ReturnType | null; clearBuffer?: boolean } { - switch (parsedInput.name) { - case 'help': - return { - result: ( - - - {this.getHelpContent(parsedInput, contextConsoleService)} - - - ), - }; - - case 'clear': - return { - result: null, - clearBuffer: true, - }; - } - - return { result: null }; - } - - async getHelpContent( - parsedInput: ParsedCommandInput, - commandService: CommandServiceInterface - ): Promise<{ result: ReactNode }> { - let helpOutput: ReactNode; - - if (commandService.getHelp) { - helpOutput = (await commandService.getHelp()).result; - } else { - helpOutput = ( - - ); - } - - return { - result: helpOutput, - }; - } - - isBuiltin(name: string): boolean { - return !!this.commandList.find((command) => command.name === name); - } - - async getCommandUsage(command: CommandDefinition): Promise<{ result: ReactNode }> { - return { - result: , - }; - } -} diff --git a/x-pack/plugins/security_solution/public/management/components/console/service/builtin_commands.tsx b/x-pack/plugins/security_solution/public/management/components/console/service/builtin_commands.tsx new file mode 100644 index 00000000000000..5869e3b4472cb9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/console/service/builtin_commands.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { ClearCommand } from '../components/builtin_commands/clear_command'; +import { HelpCommand } from '../components/builtin_commands/help_command'; +import { CommandDefinition } from '../types'; + +export const getBuiltinCommands = (): CommandDefinition[] => { + return [ + { + name: 'help', + about: i18n.translate('xpack.securitySolution.console.builtInCommands.helpAbout', { + defaultMessage: 'View list of available commands', + }), + RenderComponent: HelpCommand, + }, + { + name: 'clear', + about: i18n.translate('xpack.securitySolution.console.builtInCommands.clearAbout', { + defaultMessage: 'Clear the console buffer', + }), + RenderComponent: ClearCommand, + }, + ]; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/console/service/types.builtin_command_service.ts b/x-pack/plugins/security_solution/public/management/components/console/service/types.builtin_command_service.ts deleted file mode 100644 index dbd5347ea99c22..00000000000000 --- a/x-pack/plugins/security_solution/public/management/components/console/service/types.builtin_command_service.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ReactNode } from 'react'; -import { CommandDefinition, CommandServiceInterface } from '../types'; -import { ParsedCommandInput } from './parsed_command_input'; -import { HistoryItemComponent } from '../components/history_item'; - -export interface BuiltinCommandServiceInterface extends CommandServiceInterface { - executeBuiltinCommand( - parsedInput: ParsedCommandInput, - contextConsoleService: CommandServiceInterface - ): { result: ReturnType | null; clearBuffer?: boolean }; - - getHelpContent( - parsedInput: ParsedCommandInput, - commandService: CommandServiceInterface - ): Promise<{ result: ReactNode }>; - - isBuiltin(name: string): boolean; - - getCommandUsage(command: CommandDefinition): Promise<{ result: ReactNode }>; -} diff --git a/x-pack/plugins/security_solution/public/management/components/console/types.ts b/x-pack/plugins/security_solution/public/management/components/console/types.ts index 6b15f039883132..fec4b2722cc92c 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/types.ts @@ -5,16 +5,34 @@ * 2.0. */ -import { ReactNode } from 'react'; -import { CommonProps } from '@elastic/eui'; -import { ParsedArgData, ParsedCommandInput } from './service/parsed_command_input'; +import type { ComponentType, ComponentProps } from 'react'; +import type { CommonProps } from '@elastic/eui'; +import type { CommandExecutionState } from './components/console_state/types'; +import type { Immutable } from '../../../../common/endpoint/types'; +import type { ParsedArgData, ParsedCommandInput } from './service/parsed_command_input'; export interface CommandDefinition { name: string; about: string; - validator?: () => Promise; + /** + * The Component that will be used to render the Command + */ + RenderComponent: CommandExecutionComponent; + /** + * If defined, this command's use of `--help` will be displayed using this component instead of + * the console's built in output. + */ + HelpComponent?: CommandExecutionComponent; + /** + * A store for any data needed when the command is executed. + * The entire `CommandDefinition` is passed along to the component + * that will handle it, so this data will be available there + */ + meta?: Record; + /** If all args are optional, but at least one must be defined, set to true */ mustHaveArgs?: boolean; + /** The list of arguments supported by this command */ args?: { [longName: string]: { required: boolean; @@ -28,7 +46,7 @@ export interface CommandDefinition { // Selector: Idea is that the schema can plugin in a rich component for the // user to select something (ex. a file) // FIXME: implement selector - selector?: () => unknown; + selector?: ComponentType; }; }; } @@ -46,26 +64,44 @@ export interface Command { commandDefinition: CommandDefinition; } -export interface CommandServiceInterface { - getCommandList(): CommandDefinition[]; - - executeCommand(command: Command): Promise<{ result: ReactNode }>; - +/** + * The component that will handle the Command execution and display the result. + */ +export type CommandExecutionComponent = ComponentType<{ + command: Command; /** - * If defined, then the `help` builtin command will display this output instead of the default one - * which is generated out of the Command list + * A data store for the command execution to store data in, if needed. + * Because the Console could be closed/opened several times, which will cause this component + * to be `mounted`/`unmounted` several times, this data store will be beneficial for + * persisting data (ex. API response with IDs) that the command can use to determine + * if the command has already been executed or if it's a new instance. */ - getHelp?: () => Promise<{ result: ReactNode }>; - + store: Immutable; + /** Sets the `store` data above */ + setStore: (state: CommandExecutionState['store']) => void; /** - * If defined, then the output of this function will be used to display individual - * command help (`--help`) + * The status of the command execution. + * Note that the console's UI will show the command as "busy" while the status here is + * `pending`. Ensure that once the action processing completes, that this is set to + * either `success` or `error`. */ - getCommandUsage?: (command: CommandDefinition) => Promise<{ result: ReactNode }>; -} + status: CommandExecutionState['status']; + /** Set the status of the command execution */ + setStatus: (status: CommandExecutionState['status']) => void; +}>; + +export type CommandExecutionComponentProps = ComponentProps; export interface ConsoleProps extends CommonProps { - commandService: CommandServiceInterface; + /** + * The list of Commands that will be available in the console for the user to execute + */ + commands: CommandDefinition[]; + /** + * If defined, then the `help` builtin command will display this output instead of the default one + * which is generated out of the Command list. + */ + HelpComponent?: CommandExecutionComponent; prompt?: string; /** * For internal use only! diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console.tsx deleted file mode 100644 index 28472e123380ad..00000000000000 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useMemo } from 'react'; -import { Console } from '../console'; -import { EndpointConsoleCommandService } from './endpoint_console_command_service'; -import type { HostMetadata } from '../../../../common/endpoint/types'; - -export interface EndpointConsoleProps { - endpoint: HostMetadata; -} - -export const EndpointConsole = memo((props) => { - const consoleService = useMemo(() => { - return new EndpointConsoleCommandService(); - }, []); - - return `} commandService={consoleService} />; -}); - -EndpointConsole.displayName = 'EndpointConsole'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console_command_service.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console_command_service.tsx deleted file mode 100644 index 5028879bc1a49b..00000000000000 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console_command_service.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { ReactNode } from 'react'; -import { CommandServiceInterface, CommandDefinition, Command } from '../console'; - -/** - * Endpoint specific Response Actions (commands) for use with Console. - */ -export class EndpointConsoleCommandService implements CommandServiceInterface { - getCommandList(): CommandDefinition[] { - return []; - } - - async executeCommand(command: Command): Promise<{ result: ReactNode }> { - return { result: <> }; - } -} diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_console/index.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_console/index.ts deleted file mode 100644 index 97f7fb61ae607d..00000000000000 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_console/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { EndpointConsole } from './endpoint_console'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/dev_console.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/dev_console.tsx index 6761a32c6fb656..46b2a96faa9c95 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/dev_console.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/dev_console.tsx @@ -5,7 +5,15 @@ * 2.0. */ -import React, { memo, useCallback, useMemo } from 'react'; +import React, { + memo, + ReactElement, + ReactNode, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { EuiButton, EuiCode, @@ -15,12 +23,11 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { useIsMounted } from '../../../components/hooks/use_is_mounted'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { useUrlParams } from '../../../components/hooks/use_url_params'; import { - Command, CommandDefinition, - CommandServiceInterface, Console, RegisteredConsoleClient, useConsoleManager, @@ -28,69 +35,138 @@ import { const delay = async (ms: number = 4000) => new Promise((r) => setTimeout(r, ms)); -class DevCommandService implements CommandServiceInterface { - getCommandList(): CommandDefinition[] { - return [ - { - name: 'cmd1', - about: 'Runs cmd1', - }, - { - name: 'get-file', - about: 'retrieve a file from the endpoint', - args: { - file: { - required: true, - allowMultiples: false, - about: 'the file path for the file to be retrieved', - }, - }, +const getCommandList = (): CommandDefinition[] => { + return [ + { + name: 'cmd1', + about: 'Runs cmd1', + RenderComponent: ({ command, setStatus, store, setStore }) => { + const isMounted = useIsMounted(); + + const [apiResponse, setApiResponse] = useState(null); + const [uiResponse, setUiResponse] = useState(null); + + // Emulate a real action where: + // 1. an api request is done to create the action + // 2. wait for a response + // 3. account for component mount/unmount and prevent duplicate api calls + + useEffect(() => { + (async () => { + // Emulate an api call + if (!store.apiInflight) { + setStore({ + ...store, + apiInflight: true, + }); + + window.console.warn(`${Math.random()} ------> cmd1: doing async work`); + + await delay(6000); + setApiResponse(`API was called at: ${new Date().toLocaleString()}`); + } + })(); + }, [setStore, store]); + + useEffect(() => { + (async () => { + const doUiResponse = () => { + setUiResponse( + + {`${command.commandDefinition.name}`} + {`command input: ${command.input}`} + {'Arguments provided:'} + {JSON.stringify(command.args, null, 2)} + + ); + }; + + if (store.apiResponse) { + doUiResponse(); + } else { + await delay(); + doUiResponse(); + } + })(); + }, [ + command.args, + command.commandDefinition.name, + command.input, + isMounted, + store.apiResponse, + ]); + + useEffect(() => { + if (apiResponse && uiResponse) { + setStatus('success'); + } + }, [apiResponse, setStatus, uiResponse]); + + useEffect(() => { + if (apiResponse && store.apiResponse !== apiResponse) { + setStore({ + ...store, + apiResponse, + }); + } + }, [apiResponse, setStore, store]); + + if (store.apiResponse) { + return ( +
+ {uiResponse} + {store.apiResponse as ReactNode} +
+ ); + } + + return null; }, - { - name: 'cmd2', - about: 'runs cmd 2', - args: { - file: { - required: true, - allowMultiples: false, - about: 'Includes file in the run', - validate: () => { - return true; - }, - }, - bad: { - required: false, - allowMultiples: false, - about: 'will fail validation', - validate: () => 'This is a bad value', - }, + args: { + one: { + required: false, + allowMultiples: false, + about: 'just one', }, }, - { - name: 'cmd-long-delay', - about: 'runs cmd 2', - }, - ]; - } - - async executeCommand(command: Command): Promise<{ result: React.ReactNode }> { - await delay(); - - if (command.commandDefinition.name === 'cmd-long-delay') { - await delay(20000); - } - - return { - result: ( -
-
{`${command.commandDefinition.name}`}
-
{`command input: ${command.input}`}
- {JSON.stringify(command.args, null, 2)} -
- ), - }; - } -} + }, + // { + // name: 'get-file', + // about: 'retrieve a file from the endpoint', + // args: { + // file: { + // required: true, + // allowMultiples: false, + // about: 'the file path for the file to be retrieved', + // }, + // }, + // }, + // { + // name: 'cmd2', + // about: 'runs cmd 2', + // args: { + // file: { + // required: true, + // allowMultiples: false, + // about: 'Includes file in the run', + // validate: () => { + // return true; + // }, + // }, + // bad: { + // required: false, + // allowMultiples: false, + // about: 'will fail validation', + // validate: () => 'This is a bad value', + // }, + // }, + // }, + // { + // name: 'cmd-long-delay', + // about: 'runs cmd 2', + // }, + ]; +}; const RunningConsole = memo<{ registeredConsole: RegisteredConsoleClient }>( ({ registeredConsole }) => { @@ -132,8 +208,8 @@ RunningConsole.displayName = 'RunningConsole'; // ------------------------------------------------------------ export const ShowDevConsole = memo(() => { const consoleManager = useConsoleManager(); - const commandService = useMemo(() => { - return new DevCommandService(); + const commands = useMemo(() => { + return getCommandList(); }, []); const handleRegisterOnClick = useCallback(() => { @@ -146,12 +222,12 @@ export const ShowDevConsole = memo(() => { }, consoleProps: { prompt: '>>', - commandService, + commands, 'data-test-subj': 'dev', }, }) .show(); - }, [commandService, consoleManager]); + }, [commands, consoleManager]); return ( @@ -173,8 +249,8 @@ export const ShowDevConsole = memo(() => {

{'Un-managed console'}

- - + + ); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 5380b97f300eed..f23d9cf64202b7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -23766,13 +23766,10 @@ "xpack.securitySolution.console.builtInCommands.clearAbout": "Purger la mémoire tampon de la console", "xpack.securitySolution.console.builtInCommands.helpAbout": "Afficher la liste des commandes disponibles", "xpack.securitySolution.console.commandList.footerText": "Pour plus d’informations sur les commandes ci-dessus, utilisez l’argument {helpOption}. Exemple : {cmdExample}", - "xpack.securitySolution.console.commandOutput.runInBackgroundButtonLabel": "Exécuter en arrière-plan", - "xpack.securitySolution.console.commandOutput.runInBackgroundMsg": "La réponse à la commande prend un peu de temps. Cliquez ici pour l’exécuter en arrière-plan et être averti lors de la réception de la réponse.", "xpack.securitySolution.console.commandUsage.atLeastOneOptionRequiredMessage": "Remarque : au moins une option doit être utilisée.", "xpack.securitySolution.console.commandUsage.inputUsage": "Utilisation :", "xpack.securitySolution.console.commandUsage.optionsLabel": "Options :", "xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce": "cet argument ne peut être utilisé qu’une fois : {argName}.", - "xpack.securitySolution.console.commandValidation.cmdHelpTitle": "commande {cmdName}", "xpack.securitySolution.console.commandValidation.invalidArgValue": "valeur d’argument non valide : {argName}. {error}", "xpack.securitySolution.console.commandValidation.missingRequiredArg": "argument requis manquant : {argName}", "xpack.securitySolution.console.commandValidation.mustHaveArgs": "arguments requis manquants : {requiredArgs}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4104c04a6464be..9b42d92c275e92 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23917,13 +23917,10 @@ "xpack.securitySolution.console.builtInCommands.clearAbout": "コンソールバッファーを消去", "xpack.securitySolution.console.builtInCommands.helpAbout": "使用可能なコマンドのリストを表示", "xpack.securitySolution.console.commandList.footerText": "上記のコマンドの詳細については、{helpOption}引数を使用してください。例:{cmdExample}", - "xpack.securitySolution.console.commandOutput.runInBackgroundButtonLabel": "バックグラウンドで実行", - "xpack.securitySolution.console.commandOutput.runInBackgroundMsg": "コマンド応答には時間がかかります。ここをクリックするとバックグラウンドで実行し、応答を受信したときに通知が表示されます", "xpack.securitySolution.console.commandUsage.atLeastOneOptionRequiredMessage": "注記:1つ以上のオプションを使用する必要があります", "xpack.securitySolution.console.commandUsage.inputUsage": "使用方法:", "xpack.securitySolution.console.commandUsage.optionsLabel": "オプション:", "xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce": "引数{argName}は一度だけ使用できます", - "xpack.securitySolution.console.commandValidation.cmdHelpTitle": "{cmdName}コマンド", "xpack.securitySolution.console.commandValidation.invalidArgValue": "無効な引数値:{argName}。{error}", "xpack.securitySolution.console.commandValidation.missingRequiredArg": "不足している必須の引数:{argName}", "xpack.securitySolution.console.commandValidation.mustHaveArgs": "不足している必須の引数:{requiredArgs}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 451a1412003918..dd87bbaa23fef3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23950,13 +23950,10 @@ "xpack.securitySolution.console.builtInCommands.clearAbout": "清除控制台缓冲区", "xpack.securitySolution.console.builtInCommands.helpAbout": "查看可用命令列表", "xpack.securitySolution.console.commandList.footerText": "有关上述命令的更多详情,请使用 {helpOption} 参数。示例:{cmdExample}", - "xpack.securitySolution.console.commandOutput.runInBackgroundButtonLabel": "在后台运行", - "xpack.securitySolution.console.commandOutput.runInBackgroundMsg": "命令响应花费的时间略长。单击此处以在后台运行,并在收到响应时发送通知", "xpack.securitySolution.console.commandUsage.atLeastOneOptionRequiredMessage": "注意:必须至少使用一个选项", "xpack.securitySolution.console.commandUsage.inputUsage": "用法:", "xpack.securitySolution.console.commandUsage.optionsLabel": "选项:", "xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce": "参数只能使用一次:{argName}", - "xpack.securitySolution.console.commandValidation.cmdHelpTitle": "{cmdName} 命令", "xpack.securitySolution.console.commandValidation.invalidArgValue": "无效的参数值:{argName}。{error}", "xpack.securitySolution.console.commandValidation.missingRequiredArg": "缺少所需参数:{argName}", "xpack.securitySolution.console.commandValidation.mustHaveArgs": "缺少所需参数:{requiredArgs}", From 39ae1d0aa05bed5abc88b73410999c4e08f7c095 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 6 May 2022 16:48:18 +0200 Subject: [PATCH 56/83] [Search] Share async strategy utils (#128358) --- .../strategies/common/async_utils.test.ts | 110 ++++++++++++++++++ .../search/strategies/common/async_utils.ts | 62 ++++++++++ .../eql_search/eql_search_strategy.ts | 7 +- .../strategies/ese_search/request_utils.ts | 31 +---- .../strategies/sql_search/request_utils.ts | 29 +---- 5 files changed, 188 insertions(+), 51 deletions(-) create mode 100644 src/plugins/data/server/search/strategies/common/async_utils.test.ts create mode 100644 src/plugins/data/server/search/strategies/common/async_utils.ts diff --git a/src/plugins/data/server/search/strategies/common/async_utils.test.ts b/src/plugins/data/server/search/strategies/common/async_utils.test.ts new file mode 100644 index 00000000000000..7c90a0fd4c124e --- /dev/null +++ b/src/plugins/data/server/search/strategies/common/async_utils.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getCommonDefaultAsyncSubmitParams, getCommonDefaultAsyncGetParams } from './async_utils'; +import moment from 'moment'; +import { SearchSessionsConfigSchema } from '../../../../config'; + +const getMockSearchSessionsConfig = ({ + enabled = true, + defaultExpiration = moment.duration(7, 'd'), +} = {}) => + ({ + enabled, + defaultExpiration, + } as SearchSessionsConfigSchema); + +describe('request utils', () => { + describe('getCommonDefaultAsyncSubmitParams', () => { + test('Uses `keep_alive` from default params if no `sessionId` is provided', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + }); + const params = getCommonDefaultAsyncSubmitParams(mockConfig, {}); + expect(params).toHaveProperty('keep_alive', '1m'); + }); + + test('Uses `keep_alive` from config if enabled', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + }); + const params = getCommonDefaultAsyncSubmitParams(mockConfig, { + sessionId: 'foo', + }); + expect(params).toHaveProperty('keep_alive', '259200000ms'); + }); + + test('Uses `keepAlive` of `1m` if disabled', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: false, + }); + const params = getCommonDefaultAsyncSubmitParams(mockConfig, { + sessionId: 'foo', + }); + expect(params).toHaveProperty('keep_alive', '1m'); + }); + + test('Uses `keep_on_completion` if enabled', async () => { + const mockConfig = getMockSearchSessionsConfig({}); + const params = getCommonDefaultAsyncSubmitParams(mockConfig, { + sessionId: 'foo', + }); + expect(params).toHaveProperty('keep_on_completion', true); + }); + + test('Does not use `keep_on_completion` if disabled', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: false, + }); + const params = getCommonDefaultAsyncSubmitParams(mockConfig, { + sessionId: 'foo', + }); + expect(params).toHaveProperty('keep_on_completion', false); + }); + }); + + describe('getCommonDefaultAsyncGetParams', () => { + test('Uses `wait_for_completion_timeout`', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: true, + }); + const params = getCommonDefaultAsyncGetParams(mockConfig, {}); + expect(params).toHaveProperty('wait_for_completion_timeout'); + }); + + test('Uses `keep_alive` if `sessionId` is not provided', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: true, + }); + const params = getCommonDefaultAsyncGetParams(mockConfig, {}); + expect(params).toHaveProperty('keep_alive', '1m'); + }); + + test('Has no `keep_alive` if `sessionId` is provided', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: true, + }); + const params = getCommonDefaultAsyncGetParams(mockConfig, { sessionId: 'foo' }); + expect(params).not.toHaveProperty('keep_alive'); + }); + + test('Uses `keep_alive` if `sessionId` is provided but sessions disabled', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: false, + }); + const params = getCommonDefaultAsyncGetParams(mockConfig, { sessionId: 'foo' }); + expect(params).toHaveProperty('keep_alive', '1m'); + }); + }); +}); diff --git a/src/plugins/data/server/search/strategies/common/async_utils.ts b/src/plugins/data/server/search/strategies/common/async_utils.ts new file mode 100644 index 00000000000000..46483ca3f3279c --- /dev/null +++ b/src/plugins/data/server/search/strategies/common/async_utils.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + AsyncSearchSubmitRequest, + AsyncSearchGetRequest, +} from '@elastic/elasticsearch/lib/api/types'; +import { SearchSessionsConfigSchema } from '../../../../config'; +import { ISearchOptions } from '../../../../common'; + +/** + @internal + */ +export function getCommonDefaultAsyncSubmitParams( + searchSessionsConfig: SearchSessionsConfigSchema | null, + options: ISearchOptions +): Pick< + AsyncSearchSubmitRequest, + 'keep_alive' | 'wait_for_completion_timeout' | 'keep_on_completion' +> { + const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; + + const keepAlive = useSearchSessions + ? `${searchSessionsConfig!.defaultExpiration.asMilliseconds()}ms` + : '1m'; + + return { + // Wait up to 100ms for the response to return + wait_for_completion_timeout: '100ms', + // If search sessions are used, store and get an async ID even for short running requests. + keep_on_completion: useSearchSessions, + // The initial keepalive is as defined in defaultExpiration if search sessions are used or 1m otherwise. + keep_alive: keepAlive, + }; +} + +/** + @internal + */ +export function getCommonDefaultAsyncGetParams( + searchSessionsConfig: SearchSessionsConfigSchema | null, + options: ISearchOptions +): Pick { + const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; + + return { + // Wait up to 100ms for the response to return + wait_for_completion_timeout: '100ms', + ...(useSearchSessions + ? // Don't change the expiration of search requests that are tracked in a search session + undefined + : { + // We still need to do polling for searches not within the context of a search session or when search session disabled + keep_alive: '1m', + }), + }; +} diff --git a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts index 33c6f387d65069..13b4295fb7c636 100644 --- a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts @@ -19,7 +19,8 @@ import { toEqlKibanaSearchResponse } from './response_utils'; import { EqlSearchResponse } from './types'; import { ISearchStrategy } from '../../types'; import { getDefaultSearchParams } from '../es_search'; -import { getDefaultAsyncGetParams, getIgnoreThrottled } from '../ese_search/request_utils'; +import { getIgnoreThrottled } from '../ese_search/request_utils'; +import { getCommonDefaultAsyncGetParams } from '../common/async_utils'; export const eqlSearchStrategyProvider = ( logger: Logger @@ -45,11 +46,11 @@ export const eqlSearchStrategyProvider = ( uiSettingsClient ); const params = id - ? getDefaultAsyncGetParams(null, options) + ? getCommonDefaultAsyncGetParams(null, options) : { ...(await getIgnoreThrottled(uiSettingsClient)), ...defaultParams, - ...getDefaultAsyncGetParams(null, options), + ...getCommonDefaultAsyncGetParams(null, options), ...request.params, }; const response = id diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts index ea850c80f90b3f..07f1c9d1ae9a54 100644 --- a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts +++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts @@ -12,6 +12,10 @@ import { AsyncSearchSubmitRequest } from '@elastic/elasticsearch/lib/api/types'; import { ISearchOptions, UI_SETTINGS } from '../../../../common'; import { getDefaultSearchParams } from '../es_search'; import { SearchSessionsConfigSchema } from '../../../../config'; +import { + getCommonDefaultAsyncGetParams, + getCommonDefaultAsyncSubmitParams, +} from '../common/async_utils'; /** * @internal @@ -43,23 +47,10 @@ export async function getDefaultAsyncSubmitParams( | 'keep_on_completion' > > { - const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; - - // TODO: searchSessionsConfig could be "null" if we are running without x-pack which happens only in tests. - // This can be cleaned up when we completely stop separating basic and oss - const keepAlive = useSearchSessions - ? `${searchSessionsConfig!.defaultExpiration.asMilliseconds()}ms` - : '1m'; - return { // TODO: adjust for partial results batched_reduce_size: 64, - // Wait up to 100ms for the response to return - wait_for_completion_timeout: '100ms', - // If search sessions are used, store and get an async ID even for short running requests. - keep_on_completion: useSearchSessions, - // The initial keepalive is as defined in defaultExpiration if search sessions are used or 1m otherwise. - keep_alive: keepAlive, + ...getCommonDefaultAsyncSubmitParams(searchSessionsConfig, options), ...(await getIgnoreThrottled(uiSettingsClient)), ...(await getDefaultSearchParams(uiSettingsClient)), // If search sessions are used, set the initial expiration time. @@ -73,17 +64,7 @@ export function getDefaultAsyncGetParams( searchSessionsConfig: SearchSessionsConfigSchema | null, options: ISearchOptions ): Pick { - const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; - return { - // Wait up to 100ms for the response to return - wait_for_completion_timeout: '100ms', - ...(useSearchSessions - ? // Don't change the expiration of search requests that are tracked in a search session - undefined - : { - // We still need to do polling for searches not within the context of a search session or when search session disabled - keep_alive: '1m', - }), + ...getCommonDefaultAsyncGetParams(searchSessionsConfig, options), }; } diff --git a/src/plugins/data/server/search/strategies/sql_search/request_utils.ts b/src/plugins/data/server/search/strategies/sql_search/request_utils.ts index d05b2710b07ea1..de8ced65d16c6c 100644 --- a/src/plugins/data/server/search/strategies/sql_search/request_utils.ts +++ b/src/plugins/data/server/search/strategies/sql_search/request_utils.ts @@ -9,6 +9,10 @@ import { SqlGetAsyncRequest, SqlQueryRequest } from '@elastic/elasticsearch/lib/api/types'; import { ISearchOptions } from '../../../../common'; import { SearchSessionsConfigSchema } from '../../../../config'; +import { + getCommonDefaultAsyncGetParams, + getCommonDefaultAsyncSubmitParams, +} from '../common/async_utils'; /** @internal @@ -17,19 +21,8 @@ export function getDefaultAsyncSubmitParams( searchSessionsConfig: SearchSessionsConfigSchema | null, options: ISearchOptions ): Pick { - const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; - - const keepAlive = useSearchSessions - ? `${searchSessionsConfig!.defaultExpiration.asMilliseconds()}ms` - : '1m'; - return { - // Wait up to 100ms for the response to return - wait_for_completion_timeout: '100ms', - // If search sessions are used, store and get an async ID even for short running requests. - keep_on_completion: useSearchSessions, - // The initial keepalive is as defined in defaultExpiration if search sessions are used or 1m otherwise. - keep_alive: keepAlive, + ...getCommonDefaultAsyncSubmitParams(searchSessionsConfig, options), }; } @@ -40,17 +33,7 @@ export function getDefaultAsyncGetParams( searchSessionsConfig: SearchSessionsConfigSchema | null, options: ISearchOptions ): Pick { - const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; - return { - // Wait up to 100ms for the response to return - wait_for_completion_timeout: '100ms', - ...(useSearchSessions - ? // Don't change the expiration of search requests that are tracked in a search session - undefined - : { - // We still need to do polling for searches not within the context of a search session or when search session disabled - keep_alive: '1m', - }), + ...getCommonDefaultAsyncGetParams(searchSessionsConfig, options), }; } From 3303999cec30fdb048d6633732d9942afb81016f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Fri, 6 May 2022 10:49:56 -0400 Subject: [PATCH 57/83] Display rule API key owner to users who can manage API keys (#131662) * Display rule owner for users who can manage API keys * Rename owner to API key owner * Make owner bold instead of a badge --- .../public/application/lib/capabilities.ts | 2 ++ .../components/rule_details.test.tsx | 23 +++++++++++++++- .../rule_details/components/rule_details.tsx | 27 ++++++++++++++++++- .../apps/triggers_actions_ui/details.ts | 3 +++ .../page_objects/rule_details.ts | 3 +++ 5 files changed, 56 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts index 74b8243519428b..bc4957b65b1bc5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts @@ -29,3 +29,5 @@ export function hasAllPrivilege(rule: InitialRule, ruleType?: RuleType): boolean export function hasReadPrivilege(rule: InitialRule, ruleType?: RuleType): boolean { return ruleType?.authorizedConsumers[rule.consumer]?.read ?? false; } +export const hasManageApiKeysCapability = (capabilities: Capabilities) => + capabilities?.management?.security?.api_keys; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx index fe17dde8c12828..7857eb172eedbb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx @@ -48,6 +48,7 @@ jest.mock('../../../lib/capabilities', () => ({ hasAllPrivilege: jest.fn(() => true), hasSaveRulesCapability: jest.fn(() => true), hasExecuteActionsCapability: jest.fn(() => true), + hasManageApiKeysCapability: jest.fn(() => true), })); const useKibanaMock = useKibana as jest.Mocked; const ruleTypeRegistry = ruleTypeRegistryMock.create(); @@ -100,6 +101,26 @@ describe('rule_details', () => { ).toBeTruthy(); }); + it('renders the API key owner badge when user can manage API keys', () => { + const rule = mockRule(); + expect( + shallow( + + ).find({rule.apiKeyOwner}) + ).toBeTruthy(); + }); + + it(`doesn't render the API key owner badge when user can't manage API keys`, () => { + const { hasManageApiKeysCapability } = jest.requireMock('../../../lib/capabilities'); + hasManageApiKeysCapability.mockReturnValueOnce(false); + const rule = mockRule(); + expect( + shallow() + .find({rule.apiKeyOwner}) + .exists() + ).toBeFalsy(); + }); + it('renders the rule error banner with error message, when rule has a license error', () => { const rule = mockRule({ enabled: true, @@ -871,7 +892,7 @@ function mockRule(overloads: Partial = {}): Rule { updatedBy: null, createdAt: new Date(), updatedAt: new Date(), - apiKeyOwner: null, + apiKeyOwner: 'bob', throttle: null, notifyWhen: null, muteAll: false, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index b3363159851d09..0389e6b0d9b302 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -27,7 +27,11 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { RuleExecutionStatusErrorReasons, parseDuration } from '@kbn/alerting-plugin/common'; -import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; +import { + hasAllPrivilege, + hasExecuteActionsCapability, + hasManageApiKeysCapability, +} from '../../../lib/capabilities'; import { getAlertingSectionBreadcrumb, getRuleDetailsBreadcrumb } from '../../../lib/breadcrumb'; import { getCurrentDocTitle } from '../../../lib/doc_title'; import { @@ -310,6 +314,27 @@ export const RuleDetails: React.FunctionComponent = ({ + {hasManageApiKeysCapability(capabilities) ? ( + + + + +

+ +

+
+
+ + + {rule.apiKeyOwner} + + +
+
+ ) : null} {uniqueActions && uniqueActions.length ? ( diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 1127a7423a3aad..56dfa17ef6268b 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -168,6 +168,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const ruleType = await pageObjects.ruleDetailsUI.getRuleType(); expect(ruleType).to.be(`Always Firing`); + const owner = await pageObjects.ruleDetailsUI.getAPIKeyOwner(); + expect(owner).to.be('elastic'); + const { connectorType } = await pageObjects.ruleDetailsUI.getActionsLabels(); expect(connectorType).to.be(`Slack`); }); diff --git a/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts index 01d7c24be2f416..cff396276eefd3 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts @@ -21,6 +21,9 @@ export function RuleDetailsPageProvider({ getService }: FtrProviderContext) { async getRuleType() { return await testSubjects.getVisibleText('ruleTypeLabel'); }, + async getAPIKeyOwner() { + return await testSubjects.getVisibleText('apiKeyOwnerLabel'); + }, async getActionsLabels() { return { connectorType: await testSubjects.getVisibleText('actionTypeLabel'), From d1b09c73abb8b6262d3756da4f1fb23585489faa Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 6 May 2022 07:59:51 -0700 Subject: [PATCH 58/83] Add openAPI specifications for cases endpoint (#131275) --- x-pack/plugins/cases/docs/openapi/README.md | 29 + .../plugins/cases/docs/openapi/bundled.json | 2122 +++++++++++++++++ .../plugins/cases/docs/openapi/bundled.yaml | 1811 ++++++++++++++ .../cases/docs/openapi/components/README.md | 7 + .../examples/create_case_request.yaml | 21 + .../examples/create_case_response.yaml | 42 + .../examples/update_case_request.yaml | 29 + .../examples/update_case_response.yaml | 60 + .../openapi/components/headers/kbn_xsrf.yaml | 5 + .../components/parameters/space_id.yaml | 7 + .../schemas/case_response_properties.yaml | 117 + .../components/schemas/closure_types.yaml | 5 + .../components/schemas/comment_types.yaml | 5 + .../schemas/connector_properties.yaml | 65 + .../components/schemas/connector_types.yaml | 9 + .../openapi/components/schemas/owners.yaml | 6 + .../openapi/components/schemas/status.yaml | 6 + .../cases/docs/openapi/entrypoint.yaml | 92 + .../cases/docs/openapi/paths/README.md | 10 + .../cases/docs/openapi/paths/api@cases.yaml | 161 ++ .../openapi/paths/s@{spaceid}@api@cases.yaml | 164 ++ 21 files changed, 4773 insertions(+) create mode 100644 x-pack/plugins/cases/docs/openapi/README.md create mode 100644 x-pack/plugins/cases/docs/openapi/bundled.json create mode 100644 x-pack/plugins/cases/docs/openapi/bundled.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/README.md create mode 100644 x-pack/plugins/cases/docs/openapi/components/examples/create_case_request.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/examples/update_case_request.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/headers/kbn_xsrf.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/parameters/space_id.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/schemas/closure_types.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/schemas/comment_types.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/schemas/connector_properties.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/schemas/connector_types.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/schemas/owners.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/components/schemas/status.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/entrypoint.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/paths/README.md create mode 100644 x-pack/plugins/cases/docs/openapi/paths/api@cases.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases.yaml diff --git a/x-pack/plugins/cases/docs/openapi/README.md b/x-pack/plugins/cases/docs/openapi/README.md new file mode 100644 index 00000000000000..1ff3e24c2e91f9 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/README.md @@ -0,0 +1,29 @@ +# OpenAPI (Experimental) + +The current self-contained spec file is [as JSON](https://raw.githubusercontent.com/elastic/kibana/master/x-pack/plugins/cases/common/openapi/bundled.json) or [as YAML](https://raw.githubusercontent.com/elastic/kibana/master/x-pack/plugins/cases/common/openapi/bundled.yaml) and can be used for online tools like those found at https://openapi.tools/. +This spec is experimental and may be incomplete or change later. + +A guide about the openApi specification can be found at [https://swagger.io/docs/specification/about/](https://swagger.io/docs/specification/about/). + +## The `openapi` folder + +* `entrypoint.yaml` is the overview file which pulls together all the paths and components. +* [Paths](paths/README.md): this defines each endpoint. A path can have one operation per http method. +* [Components](components/README.md): Reusable components + +## Tools + +It is possible to validate the docs before bundling them with the following +command in the `x-pack/plugins/cases/docs/openapi/` folder: + + ``` + npx swagger-cli validate entrypoint.yaml + ``` + +Then you can generate the `bundled` files by running the following commands: + + ``` + npx @redocly/openapi-cli bundle --ext yaml --output bundled.yaml entrypoint.yaml + npx @redocly/openapi-cli bundle --ext json --output bundled.json entrypoint.yaml + ``` + diff --git a/x-pack/plugins/cases/docs/openapi/bundled.json b/x-pack/plugins/cases/docs/openapi/bundled.json new file mode 100644 index 00000000000000..31feae3331b046 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/bundled.json @@ -0,0 +1,2122 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Cases", + "description": "OpenAPI schema for Cases endpoints", + "version": "0.1", + "contact": { + "name": "Cases Team" + }, + "license": { + "name": "Elastic License 2.0", + "url": "https://www.elastic.co/licensing/elastic-license" + } + }, + "tags": [ + { + "name": "cases", + "description": "Case APIs enable you to open and track issues." + }, + { + "name": "kibana", + "description": "Kibana APIs enable you to interact with Kibana features." + } + ], + "servers": [ + { + "url": "http://localhost:5601", + "description": "local" + } + ], + "paths": { + "/api/cases": { + "post": { + "description": "Creates a case. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're creating.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "description": { + "description": "The description for the case.", + "type": "string" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "description": "An object that contains the case settings.", + "type": "object", + "properties": { + "syncAlerts": { + "description": "Turns alert syncing on or off.", + "type": "boolean" + } + } + }, + "tags": { + "description": "The words and phrases that help categorize cases. It can be an empty array.", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "description": "A title for the case.", + "type": "string" + } + }, + "required": [ + "connector", + "description", + "owner", + "settings", + "tags", + "title" + ] + }, + "examples": { + "createCaseRequest": { + "$ref": "#/components/examples/create_case_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "closed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "comments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [] + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-05-13T09:16:17.416Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "ahunley@imf.usa.gov" + }, + "full_name": { + "type": "string", + "example": "Alan Hunley" + }, + "username": { + "type": "string", + "example": "ahunley" + } + } + }, + "description": { + "type": "string", + "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" + }, + "external_service": { + "type": "object", + "properties": { + "connector_id": { + "type": "string" + }, + "connector_name": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "external_title": { + "type": "string" + }, + "external_url": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time" + }, + "pushed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + } + } + }, + "id": { + "type": "string", + "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "type": "object", + "properties": { + "syncAlerts": { + "type": "boolean", + "example": true + } + } + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "phishing", + "social engineering", + "bubblegum" + ] + }, + "title": { + "type": "string", + "example": "This case will self-destruct in 5 seconds" + }, + "totalAlerts": { + "type": "integer", + "example": 0 + }, + "totalComment": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "version": { + "type": "string", + "example": "WzUzMiwxXQ==" + } + } + }, + "examples": { + "createCaseResponse": { + "$ref": "#/components/examples/create_case_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "delete": { + "description": "Deletes one or more cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're deleting.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "name": "ids", + "description": "The cases that you want to removed. To retrieve case IDs, use the find cases API. All non-ASCII characters must be URL encoded.", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "d4e7abb0-b462-11ec-9a8d-698504725a43" + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "patch": { + "description": "Updates one or more cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cases": { + "type": "array", + "items": { + "type": "object", + "properties": { + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "description": { + "description": "The description for the case.", + "type": "string" + }, + "id": { + "description": "The identifier for the case.", + "type": "string" + }, + "settings": { + "description": "An object that contains the case settings.", + "type": "object", + "properties": { + "syncAlerts": { + "description": "Turns alert syncing on or off.", + "type": "boolean" + } + } + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "description": "The words and phrases that help categorize cases.", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "description": "A title for the case.", + "type": "string" + }, + "version": { + "description": "The current version of the case.", + "type": "string" + } + }, + "required": [ + "id", + "version" + ] + } + } + } + }, + "examples": { + "updateCaseRequest": { + "$ref": "#/components/examples/update_case_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "closed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "comments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [] + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-05-13T09:16:17.416Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "ahunley@imf.usa.gov" + }, + "full_name": { + "type": "string", + "example": "Alan Hunley" + }, + "username": { + "type": "string", + "example": "ahunley" + } + } + }, + "description": { + "type": "string", + "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" + }, + "external_service": { + "type": "object", + "properties": { + "connector_id": { + "type": "string" + }, + "connector_name": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "external_title": { + "type": "string" + }, + "external_url": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time" + }, + "pushed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + } + } + }, + "id": { + "type": "string", + "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "type": "object", + "properties": { + "syncAlerts": { + "type": "boolean", + "example": true + } + } + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "phishing", + "social engineering", + "bubblegum" + ] + }, + "title": { + "type": "string", + "example": "This case will self-destruct in 5 seconds" + }, + "totalAlerts": { + "type": "integer", + "example": 0 + }, + "totalComment": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "version": { + "type": "string", + "example": "WzUzMiwxXQ==" + } + } + }, + "examples": { + "updateCaseResponse": { + "$ref": "#/components/examples/update_case_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/cases": { + "post": { + "description": "Creates a case. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're creating.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "description": { + "description": "The description for the case.", + "type": "string" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "description": "An object that contains the case settings.", + "type": "object", + "properties": { + "syncAlerts": { + "description": "Turns alert syncing on or off.", + "type": "boolean" + } + } + }, + "tags": { + "description": "The words and phrases that help categorize cases. It can be an empty array.", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "description": "A title for the case.", + "type": "string" + } + }, + "required": [ + "connector", + "description", + "owner", + "settings", + "tags", + "title" + ] + }, + "examples": { + "createCaseRequest": { + "$ref": "#/components/examples/create_case_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "closed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "comments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [] + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-05-13T09:16:17.416Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "ahunley@imf.usa.gov" + }, + "full_name": { + "type": "string", + "example": "Alan Hunley" + }, + "username": { + "type": "string", + "example": "ahunley" + } + } + }, + "description": { + "type": "string", + "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" + }, + "external_service": { + "type": "object", + "properties": { + "connector_id": { + "type": "string" + }, + "connector_name": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "external_title": { + "type": "string" + }, + "external_url": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time" + }, + "pushed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + } + } + }, + "id": { + "type": "string", + "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "type": "object", + "properties": { + "syncAlerts": { + "type": "boolean", + "example": true + } + } + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "phishing", + "social engineering", + "bubblegum" + ] + }, + "title": { + "type": "string", + "example": "This case will self-destruct in 5 seconds" + }, + "totalAlerts": { + "type": "integer", + "example": 0 + }, + "totalComment": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "version": { + "type": "string", + "example": "WzUzMiwxXQ==" + } + } + }, + "examples": { + "createCaseResponse": { + "$ref": "#/components/examples/create_case_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "delete": { + "description": "Deletes one or more cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're deleting.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "name": "ids", + "description": "The cases that you want to removed. All non-ASCII characters must be URL encoded.", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "d4e7abb0-b462-11ec-9a8d-698504725a43" + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "patch": { + "description": "Updates one or more cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cases": { + "type": "array", + "items": { + "type": "object", + "properties": { + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "description": { + "description": "The description for the case.", + "type": "string" + }, + "id": { + "description": "The identifier for the case.", + "type": "string" + }, + "settings": { + "description": "An object that contains the case settings.", + "type": "object", + "properties": { + "syncAlerts": { + "description": "Turns alert syncing on or off.", + "type": "boolean" + } + } + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "description": "The words and phrases that help categorize cases.", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "description": "A title for the case.", + "type": "string" + }, + "version": { + "description": "The current version of the case.", + "type": "string" + } + }, + "required": [ + "id", + "version" + ] + } + } + } + }, + "examples": { + "updateCaseRequest": { + "$ref": "#/components/examples/update_case_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "closed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "comments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [] + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-05-13T09:16:17.416Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "ahunley@imf.usa.gov" + }, + "full_name": { + "type": "string", + "example": "Alan Hunley" + }, + "username": { + "type": "string", + "example": "ahunley" + } + } + }, + "description": { + "type": "string", + "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" + }, + "external_service": { + "type": "object", + "properties": { + "connector_id": { + "type": "string" + }, + "connector_name": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "external_title": { + "type": "string" + }, + "external_url": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time" + }, + "pushed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + } + } + }, + "id": { + "type": "string", + "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "type": "object", + "properties": { + "syncAlerts": { + "type": "boolean", + "example": true + } + } + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "phishing", + "social engineering", + "bubblegum" + ] + }, + "title": { + "type": "string", + "example": "This case will self-destruct in 5 seconds" + }, + "totalAlerts": { + "type": "integer", + "example": 0 + }, + "totalComment": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "version": { + "type": "string", + "example": "WzUzMiwxXQ==" + } + } + }, + "examples": { + "updateCaseResponse": { + "$ref": "#/components/examples/update_case_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + } + }, + "components": { + "securitySchemes": { + "basicAuth": { + "type": "http", + "scheme": "basic" + }, + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "ApiKey" + } + }, + "parameters": { + "kbn_xsrf": { + "schema": { + "type": "string" + }, + "in": "header", + "name": "kbn-xsrf", + "required": true + }, + "space_id": { + "in": "path", + "name": "spaceId", + "description": "An identifier for the space.", + "required": true, + "schema": { + "type": "string", + "example": "default" + } + } + }, + "schemas": { + "connector_types": { + "type": "string", + "description": "The type of connector.", + "enum": [ + ".jira", + ".none", + ".resilient", + ".servicenow", + ".servicenow-sir", + ".swimlane" + ] + }, + "owners": { + "type": "string", + "description": "Owner apps", + "enum": [ + "cases", + "observability", + "securitySolution" + ] + }, + "status": { + "type": "string", + "description": "The status of the case.", + "enum": [ + "closed", + "in-progress", + "open" + ] + } + }, + "examples": { + "create_case_request": { + "summary": "Create a security case that uses a Jira connector.", + "value": { + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants.", + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering" + ], + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "priority": "High", + "parent": null + } + }, + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution" + } + }, + "create_case_response": { + "summary": "The create case API returns a JSON object that includes the user who created the case and the case identifier, version, and creation time.", + "value": { + "id": "66b9aa00-94fa-11ea-9f74-e7e108796192", + "version": "WzUzMiwxXQ==", + "comments": [], + "totalComment": 0, + "totalAlerts": 0, + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution", + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active", + "closed_at": null, + "closed_by": null, + "created_at": "2022-05-13T09:16:17.416Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": null, + "updated_by": null, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "parent": null, + "priority": "High" + } + }, + "external_service": null + } + }, + "update_case_request": { + "summary": "Update the case description, tags, and connector.", + "value": { + "cases": [ + { + "id": "a18b38a0-71b0-11ea-a0b2-c51ea50a58e2", + "version": "WzIzLDFd", + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "priority": null, + "parent": null + } + }, + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + } + } + ] + } + }, + "update_case_response": { + "summary": "This is an example response when the case description, tags, and connector were updated.", + "value": [ + { + "id": "66b9aa00-94fa-11ea-9f74-e7e108796192", + "version": "WzU0OCwxXQ==", + "comments": [], + "totalComment": 0, + "totalAlerts": 0, + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution", + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "closed_at": null, + "closed_by": null, + "created_at": "2022-05-13T09:16:17.416Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": "2022-05-13T09:48:33.043Z", + "updated_by": { + "email": "classified@hms.oo.gov.uk", + "full_name": "Classified", + "username": "M" + }, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "parent": null, + "priority": null + } + }, + "external_service": { + "external_title": "IS-4", + "pushed_by": { + "full_name": "Classified", + "email": "classified@hms.oo.gov.uk", + "username": "M" + }, + "external_url": "https://hms.atlassian.net/browse/IS-4", + "pushed_at": "2022-05-13T09:20:40.672Z", + "connector_id": "05da469f-1fde-4058-99a3-91e4807e2de8", + "external_id": "10003", + "connector_name": "Jira" + } + } + ] + } + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "apiKeyAuth": [] + } + ] +} \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/bundled.yaml b/x-pack/plugins/cases/docs/openapi/bundled.yaml new file mode 100644 index 00000000000000..afad92f489a74c --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/bundled.yaml @@ -0,0 +1,1811 @@ +openapi: 3.0.1 +info: + title: Cases + description: OpenAPI schema for Cases endpoints + version: '0.1' + contact: + name: Cases Team + license: + name: Elastic License 2.0 + url: https://www.elastic.co/licensing/elastic-license +tags: + - name: cases + description: Case APIs enable you to open and track issues. + - name: kibana + description: Kibana APIs enable you to interact with Kibana features. +servers: + - url: http://localhost:5601 + description: local +paths: + /api/cases: + post: + description: > + Creates a case. You must have all privileges for the **Cases** feature + in the **Management**, **Observability**, or **Security** section of the + Kibana feature privileges, depending on the owner of the case you're + creating. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM and + ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type is + sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM Resilient + connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for ServiceNow + SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow ITSM + connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + description: + description: The description for the case. + type: string + owner: + $ref: '#/components/schemas/owners' + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + tags: + description: >- + The words and phrases that help categorize cases. It can be + an empty array. + type: array + items: + type: string + title: + description: A title for the case. + type: string + required: + - connector + - description + - owner + - settings + - tags + - title + examples: + createCaseRequest: + $ref: '#/components/examples/create_case_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + closed_at: + type: string + format: date-time + nullable: true + example: null + closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + comments: + type: array + items: + type: string + example: [] + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-05-13T09:16:17.416Z' + created_by: + type: object + properties: + email: + type: string + example: ahunley@imf.usa.gov + full_name: + type: string + example: Alan Hunley + username: + type: string + example: ahunley + description: + type: string + example: >- + James Bond clicked on a highly suspicious email banner + advertising cheap holidays for underpaid civil servants. + Operation bubblegum is active. Repeat - operation + bubblegum is now active + external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + id: + type: string + example: 66b9aa00-94fa-11ea-9f74-e7e108796192 + owner: + $ref: '#/components/schemas/owners' + settings: + type: object + properties: + syncAlerts: + type: boolean + example: true + status: + $ref: '#/components/schemas/status' + tags: + type: array + items: + type: string + example: + - phishing + - social engineering + - bubblegum + title: + type: string + example: This case will self-destruct in 5 seconds + totalAlerts: + type: integer + example: 0 + totalComment: + type: integer + example: 0 + updated_at: + type: string + format: date-time + nullable: true + example: null + updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + version: + type: string + example: WzUzMiwxXQ== + examples: + createCaseResponse: + $ref: '#/components/examples/create_case_response' + servers: + - url: https://localhost:5601 + delete: + description: > + Deletes one or more cases. You must have all privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the cases you're deleting. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - name: ids + description: >- + The cases that you want to removed. To retrieve case IDs, use the + find cases API. All non-ASCII characters must be URL encoded. + in: query + required: true + schema: + type: string + example: d4e7abb0-b462-11ec-9a8d-698504725a43 + responses: + '204': + description: Indicates a successful call. + servers: + - url: https://localhost:5601 + patch: + description: > + Updates one or more cases. You must have all privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the case you're updating. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + cases: + type: array + items: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To + create a case without a connector, specify null. + If you want to omit any individual field, specify + null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow + ITSM and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: >- + The type of incident for IBM Resilient + connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue + type is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and + ServiceNow SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow + ITSM connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution + can be delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case + without a connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + description: + description: The description for the case. + type: string + id: + description: The identifier for the case. + type: string + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + status: + $ref: '#/components/schemas/status' + tags: + description: The words and phrases that help categorize cases. + type: array + items: + type: string + title: + description: A title for the case. + type: string + version: + description: The current version of the case. + type: string + required: + - id + - version + examples: + updateCaseRequest: + $ref: '#/components/examples/update_case_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + closed_at: + type: string + format: date-time + nullable: true + example: null + closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + comments: + type: array + items: + type: string + example: [] + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-05-13T09:16:17.416Z' + created_by: + type: object + properties: + email: + type: string + example: ahunley@imf.usa.gov + full_name: + type: string + example: Alan Hunley + username: + type: string + example: ahunley + description: + type: string + example: >- + James Bond clicked on a highly suspicious email banner + advertising cheap holidays for underpaid civil servants. + Operation bubblegum is active. Repeat - operation + bubblegum is now active + external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + id: + type: string + example: 66b9aa00-94fa-11ea-9f74-e7e108796192 + owner: + $ref: '#/components/schemas/owners' + settings: + type: object + properties: + syncAlerts: + type: boolean + example: true + status: + $ref: '#/components/schemas/status' + tags: + type: array + items: + type: string + example: + - phishing + - social engineering + - bubblegum + title: + type: string + example: This case will self-destruct in 5 seconds + totalAlerts: + type: integer + example: 0 + totalComment: + type: integer + example: 0 + updated_at: + type: string + format: date-time + nullable: true + example: null + updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + version: + type: string + example: WzUzMiwxXQ== + examples: + updateCaseResponse: + $ref: '#/components/examples/update_case_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/cases: + post: + description: > + Creates a case. You must have all privileges for the **Cases** feature + in the **Management**, **Observability**, or **Security** section of the + Kibana feature privileges, depending on the owner of the case you're + creating. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + requestBody: + content: + application/json: + schema: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM and + ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type is + sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM Resilient + connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for ServiceNow + SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow ITSM + connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + description: + description: The description for the case. + type: string + owner: + $ref: '#/components/schemas/owners' + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + tags: + description: >- + The words and phrases that help categorize cases. It can be + an empty array. + type: array + items: + type: string + title: + description: A title for the case. + type: string + required: + - connector + - description + - owner + - settings + - tags + - title + examples: + createCaseRequest: + $ref: '#/components/examples/create_case_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + closed_at: + type: string + format: date-time + nullable: true + example: null + closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + comments: + type: array + items: + type: string + example: [] + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-05-13T09:16:17.416Z' + created_by: + type: object + properties: + email: + type: string + example: ahunley@imf.usa.gov + full_name: + type: string + example: Alan Hunley + username: + type: string + example: ahunley + description: + type: string + example: >- + James Bond clicked on a highly suspicious email banner + advertising cheap holidays for underpaid civil servants. + Operation bubblegum is active. Repeat - operation + bubblegum is now active + external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + id: + type: string + example: 66b9aa00-94fa-11ea-9f74-e7e108796192 + owner: + $ref: '#/components/schemas/owners' + settings: + type: object + properties: + syncAlerts: + type: boolean + example: true + status: + $ref: '#/components/schemas/status' + tags: + type: array + items: + type: string + example: + - phishing + - social engineering + - bubblegum + title: + type: string + example: This case will self-destruct in 5 seconds + totalAlerts: + type: integer + example: 0 + totalComment: + type: integer + example: 0 + updated_at: + type: string + format: date-time + nullable: true + example: null + updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + version: + type: string + example: WzUzMiwxXQ== + examples: + createCaseResponse: + $ref: '#/components/examples/create_case_response' + servers: + - url: https://localhost:5601 + delete: + description: > + Deletes one or more cases. You must have all privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the cases you're deleting. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - name: ids + description: >- + The cases that you want to removed. All non-ASCII characters must be + URL encoded. + in: query + required: true + schema: + type: string + example: d4e7abb0-b462-11ec-9a8d-698504725a43 + responses: + '204': + description: Indicates a successful call. + servers: + - url: https://localhost:5601 + patch: + description: > + Updates one or more cases. You must have all privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the case you're updating. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + requestBody: + content: + application/json: + schema: + type: object + properties: + cases: + type: array + items: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To + create a case without a connector, specify null. + If you want to omit any individual field, specify + null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow + ITSM and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: >- + The type of incident for IBM Resilient + connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue + type is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and + ServiceNow SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow + ITSM connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution + can be delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case + without a connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + description: + description: The description for the case. + type: string + id: + description: The identifier for the case. + type: string + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + status: + $ref: '#/components/schemas/status' + tags: + description: The words and phrases that help categorize cases. + type: array + items: + type: string + title: + description: A title for the case. + type: string + version: + description: The current version of the case. + type: string + required: + - id + - version + examples: + updateCaseRequest: + $ref: '#/components/examples/update_case_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + closed_at: + type: string + format: date-time + nullable: true + example: null + closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + comments: + type: array + items: + type: string + example: [] + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-05-13T09:16:17.416Z' + created_by: + type: object + properties: + email: + type: string + example: ahunley@imf.usa.gov + full_name: + type: string + example: Alan Hunley + username: + type: string + example: ahunley + description: + type: string + example: >- + James Bond clicked on a highly suspicious email banner + advertising cheap holidays for underpaid civil servants. + Operation bubblegum is active. Repeat - operation + bubblegum is now active + external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + id: + type: string + example: 66b9aa00-94fa-11ea-9f74-e7e108796192 + owner: + $ref: '#/components/schemas/owners' + settings: + type: object + properties: + syncAlerts: + type: boolean + example: true + status: + $ref: '#/components/schemas/status' + tags: + type: array + items: + type: string + example: + - phishing + - social engineering + - bubblegum + title: + type: string + example: This case will self-destruct in 5 seconds + totalAlerts: + type: integer + example: 0 + totalComment: + type: integer + example: 0 + updated_at: + type: string + format: date-time + nullable: true + example: null + updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + version: + type: string + example: WzUzMiwxXQ== + examples: + updateCaseResponse: + $ref: '#/components/examples/update_case_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + apiKeyAuth: + type: apiKey + in: header + name: ApiKey + parameters: + kbn_xsrf: + schema: + type: string + in: header + name: kbn-xsrf + required: true + space_id: + in: path + name: spaceId + description: An identifier for the space. + required: true + schema: + type: string + example: default + schemas: + connector_types: + type: string + description: The type of connector. + enum: + - .jira + - .none + - .resilient + - .servicenow + - .servicenow-sir + - .swimlane + owners: + type: string + description: Owner apps + enum: + - cases + - observability + - securitySolution + status: + type: string + description: The status of the case. + enum: + - closed + - in-progress + - open + examples: + create_case_request: + summary: Create a security case that uses a Jira connector. + value: + description: >- + James Bond clicked on a highly suspicious email banner advertising + cheap holidays for underpaid civil servants. + title: This case will self-destruct in 5 seconds + tags: + - phishing + - social engineering + connector: + id: 131d4448-abe0-4789-939d-8ef60680b498 + name: My connector + type: .jira + fields: + issueType: '10006' + priority: High + parent: null + settings: + syncAlerts: true + owner: securitySolution + create_case_response: + summary: >- + The create case API returns a JSON object that includes the user who + created the case and the case identifier, version, and creation time. + value: + id: 66b9aa00-94fa-11ea-9f74-e7e108796192 + version: WzUzMiwxXQ== + comments: [] + totalComment: 0 + totalAlerts: 0 + title: This case will self-destruct in 5 seconds + tags: + - phishing + - social engineering + - bubblegum + settings: + syncAlerts: true + owner: securitySolution + description: >- + James Bond clicked on a highly suspicious email banner advertising + cheap holidays for underpaid civil servants. Operation bubblegum is + active. Repeat - operation bubblegum is now active + closed_at: null + closed_by: null + created_at: '2022-05-13T09:16:17.416Z' + created_by: + email: ahunley@imf.usa.gov + full_name: Alan Hunley + username: ahunley + status: open + updated_at: null + updated_by: null + connector: + id: 131d4448-abe0-4789-939d-8ef60680b498 + name: My connector + type: .jira + fields: + issueType: '10006' + parent: null + priority: High + external_service: null + update_case_request: + summary: Update the case description, tags, and connector. + value: + cases: + - id: a18b38a0-71b0-11ea-a0b2-c51ea50a58e2 + version: WzIzLDFd + connector: + id: 131d4448-abe0-4789-939d-8ef60680b498 + name: My connector + type: .jira + fields: + issueType: '10006' + priority: null + parent: null + description: >- + James Bond clicked on a highly suspicious email banner advertising + cheap holidays for underpaid civil servants. Operation bubblegum + is active. Repeat - operation bubblegum is now active! + tags: + - phishing + - social engineering + - bubblegum + settings: + syncAlerts: true + update_case_response: + summary: >- + This is an example response when the case description, tags, and + connector were updated. + value: + - id: 66b9aa00-94fa-11ea-9f74-e7e108796192 + version: WzU0OCwxXQ== + comments: [] + totalComment: 0 + totalAlerts: 0 + title: This case will self-destruct in 5 seconds + tags: + - phishing + - social engineering + - bubblegum + settings: + syncAlerts: true + owner: securitySolution + description: >- + James Bond clicked on a highly suspicious email banner advertising + cheap holidays for underpaid civil servants. Operation bubblegum is + active. Repeat - operation bubblegum is now active! + closed_at: null + closed_by: null + created_at: '2022-05-13T09:16:17.416Z' + created_by: + email: ahunley@imf.usa.gov + full_name: Alan Hunley + username: ahunley + status: open + updated_at: '2022-05-13T09:48:33.043Z' + updated_by: + email: classified@hms.oo.gov.uk + full_name: Classified + username: M + connector: + id: 131d4448-abe0-4789-939d-8ef60680b498 + name: My connector + type: .jira + fields: + issueType: '10006' + parent: null + priority: null + external_service: + external_title: IS-4 + pushed_by: + full_name: Classified + email: classified@hms.oo.gov.uk + username: M + external_url: https://hms.atlassian.net/browse/IS-4 + pushed_at: '2022-05-13T09:20:40.672Z' + connector_id: 05da469f-1fde-4058-99a3-91e4807e2de8 + external_id: '10003' + connector_name: Jira +security: + - basicAuth: [] + - apiKeyAuth: [] diff --git a/x-pack/plugins/cases/docs/openapi/components/README.md b/x-pack/plugins/cases/docs/openapi/components/README.md new file mode 100644 index 00000000000000..0841562a33150d --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/README.md @@ -0,0 +1,7 @@ +Reusable components +=========== + + - `examples` - reusable [Example objects](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#exampleObject) + - `headers` - reusable [Header objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#headerObject) + - `parameters` - reusable [Parameter objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject) + - `schemas` - reusable [Schema objects](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#schemaObject) diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/create_case_request.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_request.yaml new file mode 100644 index 00000000000000..0659ed18a85692 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_request.yaml @@ -0,0 +1,21 @@ +summary: Create a security case that uses a Jira connector. +value: + { + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants.", + "title": "This case will self-destruct in 5 seconds", + "tags": [ "phishing","social engineering"], + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "priority": "High", + "parent": null + } + }, + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution" + } diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml new file mode 100644 index 00000000000000..f9f2ce3d61beb9 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml @@ -0,0 +1,42 @@ +summary: The create case API returns a JSON object that includes the user who created the case and the case identifier, version, and creation time. +value: + { + "id": "66b9aa00-94fa-11ea-9f74-e7e108796192", + "version": "WzUzMiwxXQ==", + "comments": [], + "totalComment": 0, + "totalAlerts": 0, + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution", + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active", + "closed_at": null, + "closed_by": null, + "created_at": "2022-05-13T09:16:17.416Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": null, + "updated_by": null, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "parent": null, + "priority": "High" + } + }, + "external_service": null + } diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/update_case_request.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_request.yaml new file mode 100644 index 00000000000000..7ecb306cf0735f --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_request.yaml @@ -0,0 +1,29 @@ +summary: Update the case description, tags, and connector. +value: + { + "cases": [ + { + "id": "a18b38a0-71b0-11ea-a0b2-c51ea50a58e2", + "version": "WzIzLDFd", + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "priority": null, + "parent": null + } + }, + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + } + } + ] +} \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml new file mode 100644 index 00000000000000..a73191868c8ee9 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml @@ -0,0 +1,60 @@ +summary: This is an example response when the case description, tags, and connector were updated. +value: + [ + { + "id": "66b9aa00-94fa-11ea-9f74-e7e108796192", + "version": "WzU0OCwxXQ==", + "comments": [], + "totalComment": 0, + "totalAlerts": 0, + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution", + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "closed_at": null, + "closed_by": null, + "created_at": "2022-05-13T09:16:17.416Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": "2022-05-13T09:48:33.043Z", + "updated_by": { + "email": "classified@hms.oo.gov.uk", + "full_name": "Classified", + "username": "M" + }, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "parent": null, + "priority": null, + } + }, + "external_service": { + "external_title": "IS-4", + "pushed_by": { + "full_name": "Classified", + "email": "classified@hms.oo.gov.uk", + "username": "M" + }, + "external_url": "https://hms.atlassian.net/browse/IS-4", + "pushed_at": "2022-05-13T09:20:40.672Z", + "connector_id": "05da469f-1fde-4058-99a3-91e4807e2de8", + "external_id": "10003", + "connector_name": "Jira" + } + } + ] \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/headers/kbn_xsrf.yaml b/x-pack/plugins/cases/docs/openapi/components/headers/kbn_xsrf.yaml new file mode 100644 index 00000000000000..3d8dfae634e68d --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/headers/kbn_xsrf.yaml @@ -0,0 +1,5 @@ +schema: + type: string +in: header +name: kbn-xsrf +required: true diff --git a/x-pack/plugins/cases/docs/openapi/components/parameters/space_id.yaml b/x-pack/plugins/cases/docs/openapi/components/parameters/space_id.yaml new file mode 100644 index 00000000000000..0ff325b08a0821 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/parameters/space_id.yaml @@ -0,0 +1,7 @@ +in: path +name: spaceId +description: An identifier for the space. +required: true +schema: + type: string + example: default diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml new file mode 100644 index 00000000000000..780496f1591b42 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml @@ -0,0 +1,117 @@ +closed_at: + type: string + format: date-time + nullable: true + example: null +closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null +comments: + type: array + items: + type: string + example: [] +connector: + type: object + properties: + $ref: 'connector_properties.yaml' +created_at: + type: string + format: date-time + example: "2022-05-13T09:16:17.416Z" +created_by: + type: object + properties: + email: + type: string + example: "ahunley@imf.usa.gov" + full_name: + type: string + example: "Alan Hunley" + username: + type: string + example: "ahunley" +description: + type: string + example: "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" +external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null +id: + type: string + example: "66b9aa00-94fa-11ea-9f74-e7e108796192" +owner: + $ref: 'owners.yaml' +settings: + type: object + properties: + syncAlerts: + type: boolean + example: true +status: + $ref: 'status.yaml' +tags: + type: array + items: + type: string + example: ["phishing","social engineering","bubblegum"] +title: + type: string + example: "This case will self-destruct in 5 seconds" +totalAlerts: + type: integer + example: 0 +totalComment: + type: integer + example: 0 +updated_at: + type: string + format: date-time + nullable: true + example: null +updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null +version: + type: string + example: "WzUzMiwxXQ==" diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/closure_types.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/closure_types.yaml new file mode 100644 index 00000000000000..f09063d0db18f7 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/closure_types.yaml @@ -0,0 +1,5 @@ +type: string +description: Indicates whether a case is automatically closed when it is pushed to external systems (`close-by-pushing`) or not automatically closed (`close-by-user`). +enum: + - close-by-pushing + - close-by-user \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/comment_types.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/comment_types.yaml new file mode 100644 index 00000000000000..a6a86ae163b208 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/comment_types.yaml @@ -0,0 +1,5 @@ +type: string +description: The type of comment. +enum: + - alert + - user \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/connector_properties.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/connector_properties.yaml new file mode 100644 index 00000000000000..c2bc2ab7c887ab --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/connector_properties.yaml @@ -0,0 +1,65 @@ +fields: + description: An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors. + type: string + destIp: + description: A comma-separated list of destination IPs for ServiceNow SecOps connectors. + type: string + impact: + description: The effect an incident had on business for ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: A comma-separated list of malware hashes for ServiceNow SecOps connectors. + type: string + malwareUrl: + description: A comma-separated list of malware URLs for ServiceNow SecOps connectors. + type: string + parent: + description: The key of the parent issue, when the issue type is sub-task for Jira connectors. + type: string + priority: + description: The priority of the issue for Jira and ServiceNow SecOps connectors. + type: string + severity: + description: The severity of the incident for ServiceNow ITSM connectors. + type: string + severityCode: + description: The severity code of the incident for IBM Resilient connectors. + type: number + sourceIp: + description: A comma-separated list of source IPs for ServiceNow SecOps connectors. + type: string + subcategory: + description: The subcategory of the incident for ServiceNow ITSM connectors. + type: string + urgency: + description: The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type +id: + description: The identifier for the connector. To create a case without a connector, use `none`. + type: string +name: + description: The name of the connector. To create a case without a connector, use `none`. + type: string +type: + $ref: 'connector_types.yaml' \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/connector_types.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/connector_types.yaml new file mode 100644 index 00000000000000..24c1ec58808289 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/connector_types.yaml @@ -0,0 +1,9 @@ +type: string +description: The type of connector. +enum: + - .jira + - .none + - .resilient + - .servicenow + - .servicenow-sir + - .swimlane \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/owners.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/owners.yaml new file mode 100644 index 00000000000000..f39324a36e7028 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/owners.yaml @@ -0,0 +1,6 @@ +type: string +description: Owner apps +enum: + - cases + - observability + - securitySolution \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/status.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/status.yaml new file mode 100644 index 00000000000000..1fe2e342dd7765 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/status.yaml @@ -0,0 +1,6 @@ +type: string +description: The status of the case. +enum: + - closed + - in-progress + - open \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/entrypoint.yaml b/x-pack/plugins/cases/docs/openapi/entrypoint.yaml new file mode 100644 index 00000000000000..14155c156b0cca --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/entrypoint.yaml @@ -0,0 +1,92 @@ +openapi: 3.0.1 +info: + title: Cases + description: OpenAPI schema for Cases endpoints + version: '0.1' + contact: + name: Cases Team + license: + name: Elastic License 2.0 + url: https://www.elastic.co/licensing/elastic-license +tags: + - name: cases + description: Case APIs enable you to open and track issues. + - name: kibana + description: Kibana APIs enable you to interact with Kibana features. +servers: + - url: 'http://localhost:5601' + description: local +paths: + /api/cases: + $ref: paths/api@cases.yaml +# /api/cases/_find: +# $ref: paths/api@cases@_find.yaml +# '/api/cases/alerts/{alertId}': +# $ref: 'paths/api@cases@alerts@{alertid}.yaml' +# '/api/cases/configure': +# $ref: paths/api@cases@configure.yaml +# '/api/cases/configure/{configurationId}': +# $ref: paths/api@cases@configure@{configurationid}.yaml +# '/api/cases/configure/connectors/_find': +# $ref: paths/api@cases@configure@connectors@_find.yaml +# '/api/cases/reporters': +# $ref: 'paths/api@cases@reporters.yaml' +# '/api/cases/status': +# $ref: 'paths/api@cases@status.yaml' +# '/api/cases/tags': +# $ref: 'paths/api@cases@tags.yaml' +# '/api/cases/{caseId}': +# $ref: 'paths/api@cases@{caseid}.yaml' +# '/api/cases/{caseId}/alerts': +# $ref: 'paths/api@cases@{caseid}@alerts.yaml' +# '/api/cases/{caseId}/comments': +# $ref: 'paths/api@cases@{caseid}@comments.yaml' +# '/api/cases/{caseId}/comments/{commentId}': +# $ref: 'paths/api@cases@{caseid}@comments@{commentid}.yaml' +# '/api/cases/{caseId}/connector/{connectorId}/_push': +# $ref: 'paths/api@cases@{caseid}@connector@{connectorid}@_push.yaml' +# '/api/cases/{caseId}/user_actions': +# $ref: 'paths/api@cases@{caseid}@user_actions.yaml' + + '/s/{spaceId}/api/cases': + $ref: 'paths/s@{spaceid}@api@cases.yaml' + # '/s/{spaceId}/api/cases/_find': + # $ref: 'paths/s@{spaceid}@api@cases@_find.yaml' + # '/s/{spaceId}/api/cases/alerts/{alertId}': + # $ref: 'paths/s@{spaceid}@api@cases@alerts@{alertid}.yaml' + # '/s/{spaceId}/api/cases/configure': + # $ref: paths/s@{spaceid}@api@cases@configure.yaml + # '/s/{spaceId}/api/cases/configure/{configurationId}': + # $ref: paths/s@{spaceid}@api@cases@configure@{configurationid}.yaml + # '/s/{spaceId}/api/cases/configure/connectors/_find': + # $ref: paths/s@{spaceid}@api@cases@configure@connectors@_find.yaml + # '/s/{spaceId}/api/cases/reporters': + # $ref: 'paths/s@{spaceid}@api@cases@reporters.yaml' + # '/s/{spaceId}/api/cases/status': + # $ref: 'paths/s@{spaceid}@api@cases@status.yaml' + # '/s/{spaceId}/api/cases/tags': + # $ref: 'paths/s@{spaceid}@api@cases@tags.yaml' + # '/s/{spaceId}/api/cases/{caseId}': + # $ref: 'paths/s@{spaceid}@api@cases@{caseid}.yaml' + # '/s/{spaceId}/api/cases/{caseId}/alerts': + # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@alerts.yaml' + # '/s/{spaceId}/api/cases/{caseId}/comments': + # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments.yaml' + # '/s/{spaceId}/api/cases/{caseId}/comments/{commentId}': + # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml' + # '/s/{spaceId}/api/cases/{caseId}/connector/{connectorId}/_push': + # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@connector@{connectorid}@_push.yaml' + # '/s/{spaceId}/api/cases/{caseId}/user_actions': + # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@user_actions.yaml' +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + apiKeyAuth: + type: apiKey + in: header + name: ApiKey +security: + - basicAuth: [] + - apiKeyAuth: [] diff --git a/x-pack/plugins/cases/docs/openapi/paths/README.md b/x-pack/plugins/cases/docs/openapi/paths/README.md new file mode 100644 index 00000000000000..b7818c8474fc8a --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/README.md @@ -0,0 +1,10 @@ +Paths +===== + +Each path definition for which there is a specification exists within this folder. + +These files currently use the following conventions: + +* path separator token (e.g. `@`) is included in the file name +* path parameter (e.g. `{example}`) is included in the file name +* there is one file per path; each file can contain multiple operations diff --git a/x-pack/plugins/cases/docs/openapi/paths/api@cases.yaml b/x-pack/plugins/cases/docs/openapi/paths/api@cases.yaml new file mode 100644 index 00000000000000..c37bb3ecef6457 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/api@cases.yaml @@ -0,0 +1,161 @@ +post: + description: > + Creates a case. + You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're creating. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + $ref: '../components/schemas/connector_properties.yaml' + description: + description: The description for the case. + type: string + owner: + $ref: '../components/schemas/owners.yaml' + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + tags: + description: The words and phrases that help categorize cases. It can be an empty array. + type: array + items: + type: string + title: + description: A title for the case. + type: string + required: + - connector + - description + - owner + - settings + - tags + - title + examples: + createCaseRequest: + $ref: '../components/examples/create_case_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + $ref: '../components/schemas/case_response_properties.yaml' + examples: + createCaseResponse: + $ref: '../components/examples/create_case_response.yaml' + servers: + - url: https://localhost:5601 + +delete: + description: > + Deletes one or more cases. + You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're deleting. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - name: ids + description: The cases that you want to removed. To retrieve case IDs, use the find cases API. All non-ASCII characters must be URL encoded. + in: query + required: true + schema: + type: string + example: 'd4e7abb0-b462-11ec-9a8d-698504725a43' + responses: + '204': + description: Indicates a successful call. + servers: + - url: https://localhost:5601 + +patch: + description: > + Updates one or more cases. + You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + cases: + type: array + items: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + $ref: '../components/schemas/connector_properties.yaml' + description: + description: The description for the case. + type: string + id: + description: The identifier for the case. + type: string + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + status: + $ref: '../components/schemas/status.yaml' + tags: + description: The words and phrases that help categorize cases. + type: array + items: + type: string + title: + description: A title for the case. + type: string + version: + description: The current version of the case. + type: string + required: + - id + - version + examples: + updateCaseRequest: + $ref: '../components/examples/update_case_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + $ref: '../components/schemas/case_response_properties.yaml' + examples: + updateCaseResponse: + $ref: '../components/examples/update_case_response.yaml' + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases.yaml b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases.yaml new file mode 100644 index 00000000000000..c03ea64a535384 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases.yaml @@ -0,0 +1,164 @@ +post: + description: > + Creates a case. + You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're creating. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + requestBody: + content: + application/json: + schema: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + $ref: '../components/schemas/connector_properties.yaml' + description: + description: The description for the case. + type: string + owner: + $ref: '../components/schemas/owners.yaml' + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + tags: + description: The words and phrases that help categorize cases. It can be an empty array. + type: array + items: + type: string + title: + description: A title for the case. + type: string + required: + - connector + - description + - owner + - settings + - tags + - title + examples: + createCaseRequest: + $ref: '../components/examples/create_case_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + $ref: '../components/schemas/case_response_properties.yaml' + examples: + createCaseResponse: + $ref: '../components/examples/create_case_response.yaml' + servers: + - url: https://localhost:5601 + +delete: + description: > + Deletes one or more cases. + You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're deleting. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - name: ids + description: The cases that you want to removed. All non-ASCII characters must be URL encoded. + in: query + required: true + schema: + type: string + example: 'd4e7abb0-b462-11ec-9a8d-698504725a43' + responses: + '204': + description: Indicates a successful call. + servers: + - url: https://localhost:5601 + +patch: + description: > + Updates one or more cases. + You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + requestBody: + content: + application/json: + schema: + type: object + properties: + cases: + type: array + items: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + $ref: '../components/schemas/connector_properties.yaml' + description: + description: The description for the case. + type: string + id: + description: The identifier for the case. + type: string + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + status: + $ref: '../components/schemas/status.yaml' + tags: + description: The words and phrases that help categorize cases. + type: array + items: + type: string + title: + description: A title for the case. + type: string + version: + description: The current version of the case. + type: string + required: + - id + - version + examples: + updateCaseRequest: + $ref: '../components/examples/update_case_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + $ref: '../components/schemas/case_response_properties.yaml' + examples: + updateCaseResponse: + $ref: '../components/examples/update_case_response.yaml' + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 \ No newline at end of file From d2ea80a7a1fd2c3a7fb19bda6c875a3fb70f4999 Mon Sep 17 00:00:00 2001 From: mgiota Date: Fri, 6 May 2022 17:10:12 +0200 Subject: [PATCH 59/83] [Actionable Observability] Link to filtered rules page (#131629) * make stat counts clickable * unit tests for rule stats * rename test cases & add more cases * more test cases * use type for exporting types * add types * UI adjustments * add more test cases that check the href link * use const for the rules page link * use consts for classes * fix types in ConditionalWrap --- .../alerts/components/rule_stats/index.ts | 9 + .../components/rule_stats/rule_stats.test.tsx | 199 ++++++++++++++++++ .../components/rule_stats/rule_stats.tsx | 156 ++++++++++++++ .../alerts/components/rule_stats/types.ts | 16 ++ .../containers/alerts_page/alerts_page.tsx | 59 +----- 5 files changed, 383 insertions(+), 56 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/alerts/components/rule_stats/index.ts create mode 100644 x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.test.tsx create mode 100644 x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.tsx create mode 100644 x-pack/plugins/observability/public/pages/alerts/components/rule_stats/types.ts diff --git a/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/index.ts b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/index.ts new file mode 100644 index 00000000000000..b2b4e144952e06 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { renderRuleStats } from './rule_stats'; +export type { RuleStatsState } from './types'; diff --git a/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.test.tsx b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.test.tsx new file mode 100644 index 00000000000000..6f2edf5d0b1b6b --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.test.tsx @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderRuleStats } from './rule_stats'; +import { render, screen } from '@testing-library/react'; + +const RULES_PAGE_LINK = '/app/observability/alerts/rules'; +const STAT_CLASS = 'euiStat'; +const STAT_TITLE_PRIMARY_CLASS = 'euiStat__title--primary'; +const STAT_BUTTON_CLASS = 'euiButtonEmpty'; + +describe('Rule stats', () => { + test('renders all rule stats', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 0, + error: 0, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + expect(stats.length).toEqual(6); + }); + test('disabled stat is not clickable, when there are no disabled rules', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 0, + error: 0, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { findByText, container } = render(stats[4]); + const disabledElement = await findByText('Disabled'); + expect(disabledElement).toBeInTheDocument(); + expect(container.getElementsByClassName(STAT_CLASS).length).toBe(1); + expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(0); + expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(0); + }); + + test('disabled stat is clickable, when there are disabled rules', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 1, + muted: 0, + error: 0, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { container } = render(stats[4]); + expect(screen.getByText('Disabled').closest('a')).toHaveAttribute( + 'href', + `${RULES_PAGE_LINK}?_a=(lastResponse:!(),status:!(disabled))` + ); + + expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(1); + }); + + test('disabled stat count is link-colored, when there are disabled rules', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 1, + muted: 0, + error: 0, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { container } = render(stats[4]); + expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(1); + }); + + test('snoozed stat is not clickable, when there are no snoozed rules', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 0, + error: 0, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { findByText, container } = render(stats[3]); + const snoozedElement = await findByText('Snoozed'); + expect(snoozedElement).toBeInTheDocument(); + expect(container.getElementsByClassName(STAT_CLASS).length).toBe(1); + expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(0); + expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(0); + }); + + test('snoozed stat is clickable, when there are snoozed rules', () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 1, + error: 0, + snoozed: 1, + }, + RULES_PAGE_LINK, + false + ); + const { container } = render(stats[3]); + expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(1); + expect(screen.getByText('Snoozed').closest('a')).toHaveAttribute( + 'href', + `${RULES_PAGE_LINK}?_a=(lastResponse:!(),status:!(snoozed))` + ); + }); + + test('snoozed stat count is link-colored, when there are snoozed rules', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 1, + error: 0, + snoozed: 1, + }, + RULES_PAGE_LINK, + false + ); + const { container } = render(stats[3]); + expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(1); + }); + + test('errors stat is not clickable, when there are no error rules', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 0, + error: 0, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { findByText, container } = render(stats[2]); + const errorsElement = await findByText('Errors'); + expect(errorsElement).toBeInTheDocument(); + expect(container.getElementsByClassName(STAT_CLASS).length).toBe(1); + expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(0); + expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(0); + }); + + test('errors stat is clickable, when there are error rules', () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 0, + error: 2, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { container } = render(stats[2]); + expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(1); + expect(screen.getByText('Errors').closest('a')).toHaveAttribute( + 'href', + `${RULES_PAGE_LINK}?_a=(lastResponse:!(error),status:!())` + ); + }); + + test('errors stat count is link-colored, when there are error rules', () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 0, + error: 2, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { container } = render(stats[2]); + expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(1); + }); +}); diff --git a/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.tsx b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.tsx new file mode 100644 index 00000000000000..62c520c7b7442e --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiStat } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; + +interface RuleStatsState { + total: number; + disabled: number; + muted: number; + error: number; + snoozed: number; +} +type StatType = 'disabled' | 'snoozed' | 'error'; + +const Divider = euiStyled.div` + border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; + height: 100%; +`; + +const StyledStat = euiStyled(EuiStat)` + .euiText { + line-height: 1; + } +`; + +const ConditionalWrap = ({ + condition, + wrap, + children, +}: { + condition: boolean; + wrap: (wrappedChildren: React.ReactNode) => JSX.Element; + children: JSX.Element; +}): JSX.Element => (condition ? wrap(children) : children); + +export const renderRuleStats = ( + ruleStats: RuleStatsState, + manageRulesHref: string, + ruleStatsLoading: boolean +) => { + const createRuleStatsLink = (stats: RuleStatsState, statType: StatType) => { + const count = stats[statType]; + let statsLink = `${manageRulesHref}?_a=(lastResponse:!(),status:!())`; + if (count > 0) { + switch (statType) { + case 'error': + statsLink = `${manageRulesHref}?_a=(lastResponse:!(error),status:!())`; + break; + case 'snoozed': + case 'disabled': + statsLink = `${manageRulesHref}?_a=(lastResponse:!(),status:!(${statType}))`; + break; + default: + break; + } + } + return statsLink; + }; + + const disabledStatsComponent = ( + 0} + wrap={(wrappedChildren) => ( + + {wrappedChildren} + + )} + > + 0 ? 'primary' : ''} + titleSize="xs" + isLoading={ruleStatsLoading} + data-test-subj="statDisabled" + /> + + ); + + const snoozedStatsComponent = ( + 0} + wrap={(wrappedChildren) => ( + + {wrappedChildren} + + )} + > + 0 ? 'primary' : ''} + titleSize="xs" + isLoading={ruleStatsLoading} + data-test-subj="statMuted" + /> + + ); + + const errorStatsComponent = ( + 0} + wrap={(wrappedChildren) => ( + + {wrappedChildren} + + )} + > + 0 ? 'primary' : ''} + titleSize="xs" + isLoading={ruleStatsLoading} + data-test-subj="statErrors" + /> + + ); + return [ + , + disabledStatsComponent, + snoozedStatsComponent, + errorStatsComponent, + , + + {i18n.translate('xpack.observability.alerts.manageRulesButtonLabel', { + defaultMessage: 'Manage Rules', + })} + , + ].reverse(); +}; diff --git a/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/types.ts b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/types.ts new file mode 100644 index 00000000000000..87ff668ebf87f5 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/types.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface RuleStatsState { + total: number; + disabled: number; + muted: number; + error: number; + snoozed: number; +} + +export type StatType = 'disabled' | 'snoozed' | 'error'; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx index e99a3195d0f30d..2fe114771c3292 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx @@ -5,15 +5,13 @@ * 2.0. */ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { DataViewBase } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { ALERT_STATUS, AlertStatus } from '@kbn/rule-data-utils'; - -import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { loadRuleAggregations } from '@kbn/triggers-actions-ui-plugin/public'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields'; @@ -38,6 +36,7 @@ import { } from '../state_container'; import './styles.scss'; import { AlertsStatusFilter, AlertsDisclaimer, AlertsSearchBar } from '../../components'; +import { renderRuleStats } from '../../components/rule_stats'; import { ObservabilityAppServices } from '../../../../application/types'; import { OBSERVABILITY_RULE_TYPES } from '../../../rules/config'; @@ -57,11 +56,6 @@ export interface TopAlert { active: boolean; } -const Divider = euiStyled.div` - border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - height: 100%; -`; - const regExpEscape = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); const NO_INDEX_PATTERNS: DataViewBase[] = []; const BASE_ALERT_REGEX = new RegExp(`\\s*${regExpEscape(ALERT_STATUS)}\\s*:\\s*"(.*?|\\*?)"`); @@ -251,54 +245,7 @@ function AlertsPage() { ), - rightSideItems: [ - , - , - , - , - , - - {i18n.translate('xpack.observability.alerts.manageRulesButtonLabel', { - defaultMessage: 'Manage Rules', - })} - , - ].reverse(), + rightSideItems: renderRuleStats(ruleStats, manageRulesHref, ruleStatsLoading), }} > From 4a71a6e21017326f8b79a3a55dffc0f989e17dd9 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 6 May 2022 18:18:40 +0300 Subject: [PATCH 60/83] [Unified search] Fix uptime css problem (#131730) --- src/plugins/unified_search/public/search_bar/index.tsx | 1 + src/plugins/unified_search/public/search_bar/search_bar.tsx | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plugins/unified_search/public/search_bar/index.tsx b/src/plugins/unified_search/public/search_bar/index.tsx index f8c9de7ec7d87d..40421a50a5fe29 100644 --- a/src/plugins/unified_search/public/search_bar/index.tsx +++ b/src/plugins/unified_search/public/search_bar/index.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { injectI18n } from '@kbn/i18n-react'; import { withKibana } from '@kbn/kibana-react-plugin/public'; import type { SearchBarProps } from './search_bar'; +import '../index.scss'; const Fallback = () =>
; diff --git a/src/plugins/unified_search/public/search_bar/search_bar.tsx b/src/plugins/unified_search/public/search_bar/search_bar.tsx index ab59511ea6811a..a684e5ba928a83 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.tsx @@ -31,8 +31,6 @@ import QueryBarTopRow from '../query_string_input/query_bar_top_row'; import { FilterBar, FilterItems } from '../filter_bar'; import { searchBarStyles } from './search_bar.styles'; -import '../index.scss'; - export interface SearchBarInjectedDeps { kibana: KibanaReactContextValue; intl: InjectedIntl; From 984e212bb8217c10c011aa92c6d5470e5c133b4c Mon Sep 17 00:00:00 2001 From: najmiehsa <98463228+najmiehsa@users.noreply.github.com> Date: Fri, 6 May 2022 19:49:28 +0430 Subject: [PATCH 61/83] Adjust search session management page font size (#131291) Co-authored-by: najmieh --- .../public/search/session/sessions_mgmt/components/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/search/session/sessions_mgmt/components/index.tsx b/src/plugins/data/public/search/session/sessions_mgmt/components/index.tsx index c9ba988e1330b8..05eb03fd60ddfe 100644 --- a/src/plugins/data/public/search/session/sessions_mgmt/components/index.tsx +++ b/src/plugins/data/public/search/session/sessions_mgmt/components/index.tsx @@ -15,7 +15,7 @@ export { PopoverActionsMenu } from './actions'; export const TableText = ({ children, ...props }: EuiTextProps) => { return ( - + {children} ); From c8624cdd985598e6cd27a1801402e4ad12d343ea Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 6 May 2022 10:48:41 -0500 Subject: [PATCH 62/83] [ci] break out skip patterns so they can change without triggering CI (#131726) --- .../pipelines/pull_request/pipeline.js | 17 ++----------- .../pull_request/skippable_pr_matchers.js | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 .buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.js b/.buildkite/scripts/pipelines/pull_request/pipeline.js index 65742902943ecc..6a4610284e4009 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.js +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.js @@ -9,20 +9,7 @@ const execSync = require('child_process').execSync; const fs = require('fs'); const { areChangesSkippable, doAnyChangesMatch } = require('kibana-buildkite-library'); - -const SKIPPABLE_PATHS = [ - /^docs\//, - /^rfcs\//, - /^.ci\/.+\.yml$/, - /^.ci\/es-snapshots\//, - /^.ci\/pipeline-library\//, - /^.ci\/Jenkinsfile_[^\/]+$/, - /^\.github\//, - /\.md$/, - /^\.backportrc\.json$/, - /^nav-kibana-dev\.docnav\.json$/, - /^src\/dev\/prs\/kibana_qa_pr_list\.json$/, -]; +const { SKIPPABLE_PR_MATCHERS } = require('./skippable_pr_matchers'); const REQUIRED_PATHS = [ // this file is auto-generated and changes to it need to be validated with CI @@ -48,7 +35,7 @@ const uploadPipeline = (pipelineContent) => { (async () => { try { - const skippable = await areChangesSkippable(SKIPPABLE_PATHS, REQUIRED_PATHS); + const skippable = await areChangesSkippable(SKIPPABLE_PR_MATCHERS, REQUIRED_PATHS); if (skippable) { console.log('All changes in PR are skippable. Skipping CI.'); diff --git a/.buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js b/.buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js new file mode 100644 index 00000000000000..2a36e66e11cd62 --- /dev/null +++ b/.buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + SKIPPABLE_PR_MATCHERS: [ + /^docs\//, + /^rfcs\//, + /^.ci\/.+\.yml$/, + /^.ci\/es-snapshots\//, + /^.ci\/pipeline-library\//, + /^.ci\/Jenkinsfile_[^\/]+$/, + /^\.github\//, + /\.md$/, + /^\.backportrc\.json$/, + /^nav-kibana-dev\.docnav\.json$/, + /^src\/dev\/prs\/kibana_qa_pr_list\.json$/, + /^\.buildkite\/scripts\/pipelines\/pull_request\/skippable_pr_matchers\.js$/, + ], +}; From 799b257297e865da86acb5d875ab5498dedcfc33 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 6 May 2022 08:56:08 -0700 Subject: [PATCH 63/83] [DOCS] Updates deprecation text for legacy APIs (#131741) --- docs/api/actions-and-connectors/legacy/index.asciidoc | 2 +- docs/api/alerting/legacy/index.asciidoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/actions-and-connectors/legacy/index.asciidoc b/docs/api/actions-and-connectors/legacy/index.asciidoc index 859dd652de9844..66ecb2ed31119e 100644 --- a/docs/api/actions-and-connectors/legacy/index.asciidoc +++ b/docs/api/actions-and-connectors/legacy/index.asciidoc @@ -1,4 +1,4 @@ [[actions-and-connectors-legacy-apis]] === Deprecated 7.x APIs -These APIs are deprecated and will be removed as of 8.0. +These APIs are deprecated and will be removed in a future release. diff --git a/docs/api/alerting/legacy/index.asciidoc b/docs/api/alerting/legacy/index.asciidoc index cce2c378bdb581..48f37c06ff5436 100644 --- a/docs/api/alerting/legacy/index.asciidoc +++ b/docs/api/alerting/legacy/index.asciidoc @@ -1,7 +1,7 @@ [[alerts-api]] === Deprecated 7.x APIs -These APIs are deprecated and will be removed as of 8.0. +These APIs are deprecated and will be removed in a future release. include::create.asciidoc[leveloffset=+1] include::delete.asciidoc[leveloffset=+1] From 37a27384a5a752b13c4fa2e03cdf0408039d4854 Mon Sep 17 00:00:00 2001 From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> Date: Fri, 6 May 2022 18:13:08 +0200 Subject: [PATCH 64/83] Add cloud icon "ess-icon" at the end of the config keys in "alerting" documentation (#131735) --- docs/settings/alert-action-settings.asciidoc | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index aa5d9f53359b73..95003a08b7b095 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -187,37 +187,37 @@ For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`. [[alert-settings]] ==== Alerting settings -`xpack.alerting.maxEphemeralActionsPerAlert`:: +`xpack.alerting.maxEphemeralActionsPerAlert` {ess-icon}:: Sets the number of actions that will run ephemerally. To use this, enable ephemeral tasks in task manager first with <> -`xpack.alerting.cancelAlertsOnRuleTimeout`:: +`xpack.alerting.cancelAlertsOnRuleTimeout` {ess-icon}:: Specifies whether to skip writing alerts and scheduling actions if rule processing was cancelled due to a timeout. Default: `true`. This setting can be overridden by individual rule types. -`xpack.alerting.rules.minimumScheduleInterval.value`:: +`xpack.alerting.rules.minimumScheduleInterval.value` {ess-icon}:: Specifies the minimum schedule interval for rules. This minimum is applied to all rules created or updated after you set this value. The time is formatted as: + `[s,m,h,d]` + For example, `20m`, `24h`, `7d`. This duration cannot exceed `1d`. Default: `1m`. -`xpack.alerting.rules.minimumScheduleInterval.enforce`:: +`xpack.alerting.rules.minimumScheduleInterval.enforce` {ess-icon}:: Specifies the behavior when a new or changed rule has a schedule interval less than the value defined in `xpack.alerting.rules.minimumScheduleInterval.value`. If `false`, rules with schedules less than the interval will be created but warnings will be logged. If `true`, rules with schedules less than the interval cannot be created. Default: `false`. -`xpack.alerting.rules.run.actions.max`:: +`xpack.alerting.rules.run.actions.max` {ess-icon}:: Specifies the maximum number of actions that a rule can generate each time detection checks run. -`xpack.alerting.rules.run.timeout`:: +`xpack.alerting.rules.run.timeout` {ess-icon}:: Specifies the default timeout for tasks associated with all types of rules. The time is formatted as: + `[ms,s,m,h,d,w,M,Y]` + For example, `20m`, `24h`, `7d`, `1w`. Default: `5m`. -`xpack.alerting.rules.run.ruleTypeOverrides`:: +`xpack.alerting.rules.run.ruleTypeOverrides` {ess-icon}:: Overrides the configs under `xpack.alerting.rules.run` for the rule type with the given ID. List the rule identifier and its settings in an array of objects. + For example: @@ -230,7 +230,7 @@ xpack.alerting.rules.run: timeout: '15m' -- -`xpack.alerting.rules.run.actions.connectorTypeOverrides`:: +`xpack.alerting.rules.run.actions.connectorTypeOverrides` {ess-icon}:: Overrides the configs under `xpack.alerting.rules.run.actions` for the connector type with the given ID. List the connector type identifier and its settings in an array of objects. + For example: From 743cce0a65ff62357025875acf08bc97db66891b Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Fri, 6 May 2022 09:17:16 -0700 Subject: [PATCH 65/83] Sessions tab improvements (#131583) * session tab query modified query all events, not just entry leaders. solves a few problems wrt to query ability. default columns modified and display names provided for each * snapshot updated * readded test * Default sort set to process.entry_leader.start desc * sessions tab timeline id changed to cache bust localstorage for table column configs * missed a couple spots for session tab timeline id update Co-authored-by: mitodrummer --- .../common/types/timeline/index.ts | 2 +- .../__snapshots__/index.test.tsx.snap | 22 +++------ .../sessions_viewer/cell_renderer.tsx | 25 ---------- .../sessions_viewer/default_headers.ts | 47 +++++++++++------- .../components/sessions_viewer/index.test.tsx | 3 +- .../components/sessions_viewer/index.tsx | 19 +++---- .../sessions_viewer/translations.ts | 49 +++++++++++++++++++ .../timelines/common/types/timeline/index.ts | 2 +- .../timelines/public/store/t_grid/types.ts | 2 +- .../server/search_strategy/timeline/index.ts | 10 ++-- 10 files changed, 99 insertions(+), 82 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/sessions_viewer/cell_renderer.tsx diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index caeeaa0c17beeb..cb03788aa17baa 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -318,7 +318,7 @@ export enum TimelineId { usersPageExternalAlerts = 'users-page-external-alerts', hostsPageEvents = 'hosts-page-events', hostsPageExternalAlerts = 'hosts-page-external-alerts', - hostsPageSessions = 'hosts-page-sessions', + hostsPageSessions = 'hosts-page-sessions-v2', // the v2 is to cache bust localstorage settings as default columns were reworked. detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap index 32268e2f21e7fb..9d32d2c23b18b2 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap @@ -70,34 +70,28 @@ exports[`SessionsView renders correctly against snapshot 1`] = `
- hosts-page-sessions + hosts-page-sessions-v2
- process.start + Started
- process.end + Executable
- process.executable + User
- user.name + Interactive
- process.interactive + Hostname
- process.pid + Type
- host.hostname -
-
- process.entry_leader.entry_meta.type -
-
- process.entry_leader.entry_meta.source.ip + Source IP
diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/cell_renderer.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/cell_renderer.tsx deleted file mode 100644 index 088935b32ce34f..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/cell_renderer.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; -import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; -import { getEmptyValue } from '../empty_value'; -import { MAPPED_PROCESS_END_COLUMN } from './default_headers'; - -const hasEcsDataEndEventAction = (ecsData: CellValueElementProps['ecsData']) => { - return ecsData?.event?.action?.includes('end'); -}; - -export const CellRenderer: React.FC = (props: CellValueElementProps) => { - // We only want to render process.end for event.actions of type 'end' - if (props.columnId === MAPPED_PROCESS_END_COLUMN && !hasEcsDataEndEventAction(props.ecsData)) { - return <>{getEmptyValue()}; - } - - return ; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts index d73ab1b690f615..4c045e358e1d6b 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts @@ -10,50 +10,52 @@ import { defaultColumnHeaderType } from '../../../timelines/components/timeline/ import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; import { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; - -// Using @timestamp as an way of getting the end time of the process. (Currently endpoint doesn't populate process.end) -// @timestamp of an event.action with value of "end" is what we consider that to be the end time of the process -// Current action are: 'start', 'exec', 'end', so we might have up to three events per process. -export const MAPPED_PROCESS_END_COLUMN = '@timestamp'; +import { + COLUMN_SESSION_START, + COLUMN_EXECUTABLE, + COLUMN_ENTRY_USER, + COLUMN_INTERACTIVE, + COLUMN_HOST_NAME, + COLUMN_ENTRY_TYPE, + COLUMN_ENTRY_IP, +} from './translations'; export const sessionsHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: defaultColumnHeaderType, - id: 'process.start', + id: 'process.entry_leader.start', initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, + display: COLUMN_SESSION_START, }, { columnHeaderType: defaultColumnHeaderType, - id: MAPPED_PROCESS_END_COLUMN, - display: 'process.end', + id: 'process.entry_leader.executable', + display: COLUMN_EXECUTABLE, }, { columnHeaderType: defaultColumnHeaderType, - id: 'process.executable', + id: 'process.entry_leader.user.name', + display: COLUMN_ENTRY_USER, }, { columnHeaderType: defaultColumnHeaderType, - id: 'user.name', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'process.interactive', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'process.pid', + id: 'process.entry_leader.interactive', + display: COLUMN_INTERACTIVE, }, { columnHeaderType: defaultColumnHeaderType, id: 'host.hostname', + display: COLUMN_HOST_NAME, }, { columnHeaderType: defaultColumnHeaderType, id: 'process.entry_leader.entry_meta.type', + display: COLUMN_ENTRY_TYPE, }, { - columnHeaderType: defaultColumnHeaderType, id: 'process.entry_leader.entry_meta.source.ip', + columnHeaderType: defaultColumnHeaderType, + display: COLUMN_ENTRY_IP, }, ]; @@ -62,4 +64,11 @@ export const sessionsDefaultModel: SubsetTimelineModel = { columns: sessionsHeaders, defaultColumns: sessionsHeaders, excludedRowRendererIds: Object.values(RowRendererId), + sort: [ + { + columnId: 'process.entry_leader.start', + columnType: 'date', + sortDirection: 'desc', + }, + ], }; diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx index 043a2aa378427e..5280f298ba99e4 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx @@ -109,10 +109,11 @@ describe('SessionsView', () => { expect(wrapper.getByTestId(`${TEST_PREFIX}:startDate`)).toHaveTextContent(startDate); expect(wrapper.getByTestId(`${TEST_PREFIX}:endDate`)).toHaveTextContent(endDate); expect(wrapper.getByTestId(`${TEST_PREFIX}:timelineId`)).toHaveTextContent( - 'hosts-page-sessions' + 'hosts-page-sessions-v2' ); }); }); + it('passes in the right filters to TGrid', async () => { render( diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx index 6834553a5eee88..4d89b969e5c172 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx @@ -12,7 +12,7 @@ import { ESBoolQuery } from '../../../../common/typed_json'; import { StatefulEventsViewer } from '../events_viewer'; import { sessionsDefaultModel } from './default_headers'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; -import { CellRenderer } from './cell_renderer'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import * as i18n from './translations'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; @@ -24,15 +24,8 @@ export const defaultSessionsFilter: Required> = { bool: { filter: [ { - bool: { - should: [ - { - match: { - 'process.entry_leader.same_as_process': true, - }, - }, - ], - minimum_should_match: 1, + exists: { + field: 'process.entry_leader.entity_id', // to exclude any records which have no entry_leader.entity_id }, }, ], @@ -41,10 +34,10 @@ export const defaultSessionsFilter: Required> = { meta: { alias: null, disabled: false, - key: 'process.entry_leader.same_as_process', + key: 'process.entry_leader.entity_id', negate: false, params: {}, - type: 'boolean', + type: 'string', }, }; @@ -95,7 +88,7 @@ const SessionsViewComponent: React.FC = ({ entityType={entityType} id={timelineId} leadingControlColumns={leadingControlColumns} - renderCellValue={CellRenderer} + renderCellValue={DefaultCellRenderer} rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.default} start={startDate} diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts index 606ae2b46fc6a1..ea35892f3a2f96 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts @@ -20,3 +20,52 @@ export const SINGLE_COUNT_OF_SESSIONS = i18n.translate( defaultMessage: 'session', } ); + +export const COLUMN_SESSION_START = i18n.translate( + 'xpack.securitySolution.sessionsView.columnSessionStart', + { + defaultMessage: 'Started', + } +); + +export const COLUMN_EXECUTABLE = i18n.translate( + 'xpack.securitySolution.sessionsView.columnExecutable', + { + defaultMessage: 'Executable', + } +); + +export const COLUMN_ENTRY_USER = i18n.translate( + 'xpack.securitySolution.sessionsView.columnEntryUser', + { + defaultMessage: 'User', + } +); + +export const COLUMN_INTERACTIVE = i18n.translate( + 'xpack.securitySolution.sessionsView.columnInteractive', + { + defaultMessage: 'Interactive', + } +); + +export const COLUMN_HOST_NAME = i18n.translate( + 'xpack.securitySolution.sessionsView.columnHostName', + { + defaultMessage: 'Hostname', + } +); + +export const COLUMN_ENTRY_TYPE = i18n.translate( + 'xpack.securitySolution.sessionsView.columnEntryType', + { + defaultMessage: 'Type', + } +); + +export const COLUMN_ENTRY_IP = i18n.translate( + 'xpack.securitySolution.sessionsView.columnEntrySourceIp', + { + defaultMessage: 'Source IP', + } +); diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts index 867264fa815466..528c6e4293cf44 100644 --- a/x-pack/plugins/timelines/common/types/timeline/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -314,7 +314,7 @@ export enum TimelineId { usersPageExternalAlerts = 'users-page-external-alerts', hostsPageEvents = 'hosts-page-events', hostsPageExternalAlerts = 'hosts-page-external-alerts', - hostsPageSessions = 'hosts-page-sessions', + hostsPageSessions = 'hosts-page-sessions-v2', detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', diff --git a/x-pack/plugins/timelines/public/store/t_grid/types.ts b/x-pack/plugins/timelines/public/store/t_grid/types.ts index c4627b3accd716..8e0b7e995dbcd8 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/types.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/types.ts @@ -46,7 +46,7 @@ export enum TimelineId { usersPageExternalAlerts = 'users-page-external-alerts', hostsPageEvents = 'hosts-page-events', hostsPageExternalAlerts = 'hosts-page-external-alerts', - hostsPageSessions = 'hosts-page-sessions', + hostsPageSessions = 'hosts-page-sessions-v2', detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts index 980f19ac2950c8..d450daadf4689a 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts @@ -209,17 +209,13 @@ const timelineSessionsSearchStrategy = ({ }; const collapse = { - field: 'process.entity_id', - inner_hits: { - name: 'last_event', - size: 1, - sort: [{ '@timestamp': 'desc' }], - }, + field: 'process.entry_leader.entity_id', }; + const aggs = { total: { cardinality: { - field: 'process.entity_id', + field: 'process.entry_leader.entity_id', }, }, }; From e55f8c87bea0b498cf8bc226914ee7b1c842852c Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 6 May 2022 18:21:17 +0200 Subject: [PATCH 66/83] [Discover] Monospace font in Document Explorer (#131513) * [Discover] Monospace font in Document Explorer * [Discover] Update code style * [Discover] Update jest snapshot * [Discover] Update jest snapshot * [Discover] Increase the default width for time column * [Discover] Reduce font size for cell popover * [Discover] Update font size for cell popover and fix tests * [Discover] Update width to 210 --- .../components/discover_grid/constants.ts | 2 +- .../discover_grid/discover_grid.scss | 9 +++++ .../discover_grid_columns.test.tsx | 2 +- .../get_render_cell_value.test.tsx | 33 +++++++++++++------ .../discover_grid/get_render_cell_value.tsx | 13 ++++++-- 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/plugins/discover/public/components/discover_grid/constants.ts b/src/plugins/discover/public/components/discover_grid/constants.ts index d026607aef3730..f2f5a8e8bebc75 100644 --- a/src/plugins/discover/public/components/discover_grid/constants.ts +++ b/src/plugins/discover/public/components/discover_grid/constants.ts @@ -19,7 +19,7 @@ export const GRID_STYLE = { export const pageSizeArr = [25, 50, 100, 250]; export const defaultPageSize = 100; -export const defaultTimeColumnWidth = 190; +export const defaultTimeColumnWidth = 210; export const toolbarVisibility = { showColumnSelector: { allowHide: false, diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/components/discover_grid/discover_grid.scss index 0204433a5ba1c3..113bb609248500 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid.scss +++ b/src/plugins/discover/public/components/discover_grid/discover_grid.scss @@ -30,6 +30,15 @@ } } +.dscDiscoverGrid__cellValue { + font-family: $euiCodeFontFamily; +} + +.dscDiscoverGrid__cellPopoverValue { + font-family: $euiCodeFontFamily; + font-size: $euiFontSizeS; +} + .dscDiscoverGrid__footer { background-color: $euiColorLightShade; padding: $euiSize / 2 $euiSize; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx index a9116e616946f7..c98db31a97f7f8 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx @@ -207,7 +207,7 @@ describe('Discover grid columns', function () { /> , "id": "timestamp", - "initialWidth": 190, + "initialWidth": 210, "isSortable": true, "schema": "datetime", }, diff --git a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx index be4c69f1ced25c..53e5c23cb47d58 100644 --- a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx @@ -92,7 +92,9 @@ describe('Discover grid cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component.html()).toMatchInlineSnapshot(`"100"`); + expect(component.html()).toMatchInlineSnapshot( + `"100"` + ); }); it('renders bytes column correctly using _source when details is true', () => { @@ -115,7 +117,9 @@ describe('Discover grid cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component.html()).toMatchInlineSnapshot(`"100"`); + expect(component.html()).toMatchInlineSnapshot( + `"100"` + ); }); it('renders bytes column correctly using fields when details is true', () => { @@ -138,7 +142,9 @@ describe('Discover grid cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component.html()).toMatchInlineSnapshot(`"100"`); + expect(component.html()).toMatchInlineSnapshot( + `"100"` + ); }); it('renders _source column correctly', () => { @@ -163,7 +169,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` @@ -280,7 +286,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` @@ -359,7 +365,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` @@ -485,7 +491,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` @@ -527,7 +533,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` @@ -603,6 +609,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` ); - expect(component.html()).toMatchInlineSnapshot(`"-"`); + expect(component.html()).toMatchInlineSnapshot( + `"-"` + ); }); it('renders correctly when invalid column is given', () => { @@ -657,7 +666,9 @@ describe('Discover grid cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component.html()).toMatchInlineSnapshot(`"-"`); + expect(component.html()).toMatchInlineSnapshot( + `"-"` + ); }); it('renders unmapped fields correctly', () => { @@ -695,6 +706,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` -; + return -; } /** @@ -102,7 +105,11 @@ export const getRenderCellValueFn = : formatHit(row, dataView, fieldsToShow, maxEntries, fieldFormats); return ( - + {pairs.map(([key, value]) => ( {key} @@ -118,6 +125,7 @@ export const getRenderCellValueFn = return ( Date: Fri, 6 May 2022 18:23:04 +0200 Subject: [PATCH 67/83] [Cases] Add severity field to create case (#131626) --- .../cases/public/components/create/form.tsx | 5 ++ .../components/create/form_context.test.tsx | 43 +++++++++- .../public/components/create/form_context.tsx | 3 +- .../cases/public/components/create/mock.ts | 3 +- .../cases/public/components/create/schema.tsx | 4 + .../components/create/severity.test.tsx | 79 +++++++++++++++++++ .../public/components/create/severity.tsx | 50 ++++++++++++ 7 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/create/severity.test.tsx create mode 100644 x-pack/plugins/cases/public/components/create/severity.tsx diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index ac2729564b387b..50a3c69f2073e4 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -35,6 +35,7 @@ import { CreateCaseOwnerSelector } from './owner_selector'; import { useCasesContext } from '../cases_context/use_cases_context'; import { useAvailableCasesOwners } from '../app/use_available_owners'; import { CaseAttachments } from '../../types'; +import { Severity } from './severity'; interface ContainerProps { big?: boolean; @@ -88,6 +89,9 @@ export const CreateCaseFormFields: React.FC = React.m + + + {canShowCaseSolutionSelection && ( = React.m + ), }), diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index 634f518ae5ebd1..bfa4f391458da4 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -10,7 +10,7 @@ import { mount, ReactWrapper } from 'enzyme'; import { act, RenderResult, waitFor, within } from '@testing-library/react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { CommentType, ConnectorTypes } from '../../../common/api'; +import { CaseSeverity, CommentType, ConnectorTypes } from '../../../common/api'; import { useKibana } from '../../common/lib/kibana'; import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; import { usePostCase } from '../../containers/use_post_case'; @@ -182,6 +182,7 @@ describe('Create case', () => { ); expect(renderResult.getByTestId('caseTitle')).toBeTruthy(); + expect(renderResult.getByTestId('caseSeverity')).toBeTruthy(); expect(renderResult.getByTestId('caseDescription')).toBeTruthy(); expect(renderResult.getByTestId('caseTags')).toBeTruthy(); expect(renderResult.getByTestId('caseConnectors')).toBeTruthy(); @@ -208,6 +209,34 @@ describe('Create case', () => { }); }); + it('should post a case on submit click with the selected severity', async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const renderResult = mockedContext.render( + + + + + ); + + await fillFormReactTestingLib(renderResult); + + userEvent.click(renderResult.getByTestId('case-severity-selection')); + expect(renderResult.getByTestId('case-severity-selection-high')).toBeTruthy(); + userEvent.click(renderResult.getByTestId('case-severity-selection-high')); + + userEvent.click(renderResult.getByTestId('create-case-submit')); + await waitFor(() => { + expect(postCase).toBeCalledWith({ + ...sampleData, + severity: CaseSeverity.HIGH, + }); + }); + }); + it('does not submits the title when the length is longer than 64 characters', async () => { const longTitle = 'This is a title that should not be saved as it is longer than 64 characters.'; @@ -285,6 +314,18 @@ describe('Create case', () => { ); }); + it('should select LOW as the default severity', async () => { + const renderResult = mockedContext.render( + + + + + ); + expect(renderResult.getByTestId('caseSeverity')).toBeTruthy(); + // there should be 2 low elements. one for the options popover and one for the displayed one. + expect(renderResult.getAllByTestId('case-severity-selection-low').length).toBe(2); + }); + it('should select the default connector set in the configuration', async () => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index 4385053a8c8c02..a65e9f5960e9dd 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -14,7 +14,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service' import { useConnectors } from '../../containers/configure/use_connectors'; import { Case } from '../../containers/types'; -import { NONE_CONNECTOR_ID } from '../../../common/api'; +import { CaseSeverity, NONE_CONNECTOR_ID } from '../../../common/api'; import { UseCreateAttachments, useCreateAttachments, @@ -28,6 +28,7 @@ const initialCaseValue: FormProps = { description: '', tags: [], title: '', + severity: CaseSeverity.LOW, connectorId: NONE_CONNECTOR_ID, fields: null, syncAlerts: true, diff --git a/x-pack/plugins/cases/public/components/create/mock.ts b/x-pack/plugins/cases/public/components/create/mock.ts index 8ab515c79f67e1..38d57bf24781e9 100644 --- a/x-pack/plugins/cases/public/components/create/mock.ts +++ b/x-pack/plugins/cases/public/components/create/mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CasePostRequest, ConnectorTypes } from '../../../common/api'; +import { CasePostRequest, CaseSeverity, ConnectorTypes } from '../../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { choices } from '../connectors/mock'; @@ -13,6 +13,7 @@ export const sampleTags = ['coke', 'pepsi']; export const sampleData: CasePostRequest = { description: 'what a great description', tags: sampleTags, + severity: CaseSeverity.LOW, title: 'what a cool title', connector: { fields: null, diff --git a/x-pack/plugins/cases/public/components/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx index b7c363b2639982..d72b1cc523f0df 100644 --- a/x-pack/plugins/cases/public/components/create/schema.tsx +++ b/x-pack/plugins/cases/public/components/create/schema.tsx @@ -17,6 +17,7 @@ import { import * as i18n from './translations'; import { OptionalFieldLabel } from './optional_field_label'; +import { SEVERITY_TITLE } from '../severity/translations'; const { emptyField, maxLengthField } = fieldValidators; export const schemaTags = { @@ -83,6 +84,9 @@ export const schema: FormSchema = { ], }, tags: schemaTags, + severity: { + label: SEVERITY_TITLE, + }, connectorId: { type: FIELD_TYPES.SUPER_SELECT, label: i18n.CONNECTORS, diff --git a/x-pack/plugins/cases/public/components/create/severity.test.tsx b/x-pack/plugins/cases/public/components/create/severity.test.tsx new file mode 100644 index 00000000000000..d2434a37a43924 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/severity.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseSeverity } from '../../../common/api'; +import React from 'react'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; +import { Form, FormHook, useForm } from '../../common/shared_imports'; +import { Severity } from './severity'; +import { FormProps, schema } from './schema'; +import userEvent from '@testing-library/user-event'; +import { waitFor } from '@testing-library/dom'; + +let globalForm: FormHook; +const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm({ + defaultValue: { severity: CaseSeverity.LOW }, + schema: { + severity: schema.severity, + }, + }); + + globalForm = form; + + return {children}; +}; +describe('Severity form field', () => { + let appMockRender: AppMockRenderer; + beforeEach(() => { + appMockRender = createAppMockRenderer(); + }); + it('renders', () => { + const result = appMockRender.render( + + + + ); + expect(result.getByTestId('caseSeverity')).toBeTruthy(); + expect(result.getByTestId('case-severity-selection')).not.toHaveAttribute('disabled'); + }); + + // default to LOW in this test configuration + it('defaults to the correct value', () => { + const result = appMockRender.render( + + + + ); + expect(result.getByTestId('caseSeverity')).toBeTruthy(); + // two items. one for the popover one for the selected field + expect(result.getAllByTestId('case-severity-selection-low').length).toBe(2); + }); + + it('selects the correct value when changed', async () => { + const result = appMockRender.render( + + + + ); + expect(result.getByTestId('caseSeverity')).toBeTruthy(); + userEvent.click(result.getByTestId('case-severity-selection')); + userEvent.click(result.getByTestId('case-severity-selection-high')); + await waitFor(() => { + expect(globalForm.getFormData()).toEqual({ severity: 'high' }); + }); + }); + + it('disables when loading data', () => { + const result = appMockRender.render( + + + + ); + expect(result.getByTestId('case-severity-selection')).toHaveAttribute('disabled'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/severity.tsx b/x-pack/plugins/cases/public/components/create/severity.tsx new file mode 100644 index 00000000000000..730eab5d77ac6c --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/severity.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFormRow } from '@elastic/eui'; +import React, { memo } from 'react'; +import { CaseSeverity } from '../../../common/api'; +import { UseField, useFormContext, useFormData } from '../../common/shared_imports'; +import { SeveritySelector } from '../severity/selector'; +import { SEVERITY_TITLE } from '../severity/translations'; + +interface Props { + isLoading: boolean; +} + +const SeverityFieldFormComponent = ({ isLoading }: { isLoading: boolean }) => { + const { setFieldValue } = useFormContext(); + const [{ severity }] = useFormData({ watch: ['severity'] }); + const onSeverityChange = (newSeverity: CaseSeverity) => { + setFieldValue('severity', newSeverity); + }; + return ( + + + + ); +}; +SeverityFieldFormComponent.displayName = 'SeverityFieldForm'; + +const SeverityComponent: React.FC = ({ isLoading }) => ( + +); + +SeverityComponent.displayName = 'SeverityComponent'; + +export const Severity = memo(SeverityComponent); From 37443e94f26efba070b01fdf7f7bde911b3939e2 Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Fri, 6 May 2022 09:23:44 -0700 Subject: [PATCH 68/83] [RsponseOps] Fix flaky rules list test (#131567) * Add delay to make test less flaky * Addressed comments * Addressed comments --- .../apps/triggers_actions_ui/alerts_list.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 5f6c4501476bfe..a921256a091485 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -31,8 +31,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('rulesTab'); } - // FLAKY: https://github.com/elastic/kibana/issues/131535 - describe.skip('rules list', function () { + describe('rules list', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('rulesTab'); @@ -640,24 +639,34 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Select enabled await testSubjects.click('ruleStatusFilterButton'); await testSubjects.click('ruleStatusFilterOption-enabled'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(1); // Select disabled await testSubjects.click('ruleStatusFilterOption-enabled'); await testSubjects.click('ruleStatusFilterOption-disabled'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(1); // Select snoozed await testSubjects.click('ruleStatusFilterOption-disabled'); await testSubjects.click('ruleStatusFilterOption-snoozed'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(1); // Select disabled and snoozed await testSubjects.click('ruleStatusFilterOption-disabled'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(2); // Select all 3 await testSubjects.click('ruleStatusFilterOption-enabled'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(3); }); }); From cd689102faf08d5f21a2199fb8ea1b333e20e7a2 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 6 May 2022 19:12:05 +0200 Subject: [PATCH 69/83] [Synthetics] UI clean up (#131598) --- .../header/action_menu_content.test.tsx | 13 - .../common/header/action_menu_content.tsx | 5 - .../pages/synthetics_page_template.test.tsx | 4 +- .../common/pages/synthetics_page_template.tsx | 7 +- .../synthetics_alerts_flyout_wrapper.tsx | 33 -- .../toggle_alert_flyout_button.tsx | 31 -- .../synthetics_alerts_flyout_wrapper.tsx | 45 --- .../toggle_alert_flyout_button.test.tsx | 64 ---- .../alerts/toggle_alert_flyout_button.tsx | 164 --------- .../overview/alerts/translations.ts | 345 ------------------ .../overview/filter_group/labels.ts | 24 -- .../public/apps/synthetics/hooks/index.ts | 3 +- .../hooks/use_filter_update.test.ts | 32 -- .../synthetics/hooks/use_filter_update.ts | 76 ---- .../apps/synthetics/hooks/use_telemetry.ts | 2 +- .../public/apps/synthetics/routes.tsx | 2 +- .../apps/synthetics/state/index_status/api.ts | 2 +- .../public/apps/synthetics/synthetics_app.tsx | 4 +- .../synthetics/utils/testing/rtl_helpers.tsx | 2 +- .../hooks/use_breakpoints.test.ts | 0 .../synthetics => }/hooks/use_breakpoints.ts | 0 .../app/uptime_page_template.test.tsx | 4 +- .../app/uptime_page_template.tsx | 2 +- .../ping_timestamp/step_image_caption.tsx | 2 +- .../monitor_list/monitor_list.tsx | 2 +- .../pages/synthetics/checks_navigation.tsx | 2 +- .../utils/api_service/api_service.ts | 0 .../utils/api_service/index.ts | 0 .../utils/kibana_service/index.ts | 0 .../utils/kibana_service/kibana_service.ts | 0 30 files changed, 16 insertions(+), 854 deletions(-) delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/synthetics_alerts_flyout_wrapper.tsx delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/synthetics_alerts_flyout_wrapper.tsx delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.test.tsx delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.tsx delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/translations.ts delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/overview/filter_group/labels.ts delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.test.ts delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.ts rename x-pack/plugins/synthetics/public/{apps/synthetics => }/hooks/use_breakpoints.test.ts (100%) rename x-pack/plugins/synthetics/public/{apps/synthetics => }/hooks/use_breakpoints.ts (100%) rename x-pack/plugins/synthetics/public/{apps/synthetics => }/utils/api_service/api_service.ts (100%) rename x-pack/plugins/synthetics/public/{apps/synthetics => }/utils/api_service/index.ts (100%) rename x-pack/plugins/synthetics/public/{apps/synthetics => }/utils/kibana_service/index.ts (100%) rename x-pack/plugins/synthetics/public/{apps/synthetics => }/utils/kibana_service/kibana_service.ts (100%) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.test.tsx index 3137862025f156..0439eff39d88c9 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.test.tsx @@ -6,23 +6,10 @@ */ import React from 'react'; -import { fireEvent, waitFor } from '@testing-library/react'; import { render } from '../../../utils/testing/rtl_helpers'; import { ActionMenuContent } from './action_menu_content'; describe('ActionMenuContent', () => { - it('renders alerts dropdown', async () => { - const { getByLabelText, getByText } = render(); - - const alertsDropdown = getByLabelText('Open alerts and rules context menu'); - fireEvent.click(alertsDropdown); - - await waitFor(() => { - expect(getByText('Create rule')); - expect(getByText('Manage rules')); - }); - }); - it('renders settings link', () => { const { getByRole, getByText } = render(); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx index 6d3d83146a42c7..aaf41dc46bcaf5 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx @@ -14,12 +14,9 @@ import { createExploratoryViewUrl } from '@kbn/observability-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useSyntheticsSettingsContext } from '../../../contexts'; import { useGetUrlParams } from '../../../hooks'; -import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers/toggle_alert_flyout_button'; import { MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../../../common/constants'; import { stringifyUrlParams } from '../../../utils/url_params'; import { InspectorHeaderLink } from './inspector_header_link'; -// import { monitorStatusSelector } from '../../../state/selectors'; -// import { ManageMonitorsBtn } from './manage_monitors_btn'; const ADD_DATA_LABEL = i18n.translate('xpack.synthetics.addDataButtonLabel', { defaultMessage: 'Add data', @@ -93,8 +90,6 @@ export function ActionMenuContent(): React.ReactElement { /> - - {ANALYZE_MESSAGE}

}> { +jest.mock('../../../../../hooks/use_breakpoints', () => { const down = jest.fn().mockReturnValue(false); return { useBreakpoints: () => ({ down }), diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx index 0e6c5565b842e8..44b38236fc2a2b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx @@ -10,7 +10,6 @@ import styled from 'styled-components'; import { EuiPageHeaderProps, EuiPageTemplateProps } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useInspectorContext } from '@kbn/observability-plugin/public'; -import { CERTIFICATES_ROUTE, OVERVIEW_ROUTE } from '../../../../../../common/constants'; import { ClientPluginsStart } from '../../../../../plugin'; import { useNoDataConfig } from '../../../hooks/use_no_data_config'; import { EmptyStateLoading } from '../../overview/empty_state/empty_state_loading'; @@ -65,9 +64,7 @@ export const SyntheticsPageTemplateComponent: React.FC; } - const isMainRoute = path === OVERVIEW_ROUTE || path === CERTIFICATES_ROUTE; - - const showLoading = loading && isMainRoute && !data; + const showLoading = loading && !data; return ( <> @@ -75,7 +72,7 @@ export const SyntheticsPageTemplateComponent: React.FC {showLoading && } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/synthetics_alerts_flyout_wrapper.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/synthetics_alerts_flyout_wrapper.tsx deleted file mode 100644 index 7271e8cd2e998f..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/synthetics_alerts_flyout_wrapper.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { - selectAlertFlyoutVisibility, - selectAlertFlyoutType, - setAlertFlyoutVisible, -} from '../../../../state'; -import { SyntheticsAlertsFlyoutWrapperComponent } from '../synthetics_alerts_flyout_wrapper'; - -export const SyntheticsAlertsFlyoutWrapper: React.FC = () => { - const dispatch = useDispatch(); - const setAddFlyoutVisibility = (value: React.SetStateAction) => - // @ts-ignore the value here is a boolean, and it works with the action creator function - dispatch(setAlertFlyoutVisible(value)); - - const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility); - const alertTypeId = useSelector(selectAlertFlyoutType); - - return ( - - ); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx deleted file mode 100644 index 2fea38d99d0940..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { useDispatch } from 'react-redux'; -import { setAlertFlyoutType, setAlertFlyoutVisible } from '../../../../state'; -import { - ToggleAlertFlyoutButtonComponent, - ToggleAlertFlyoutButtonProps, -} from '../toggle_alert_flyout_button'; - -export const ToggleAlertFlyoutButton: React.FC = (props) => { - const dispatch = useDispatch(); - return ( - { - if (typeof value === 'string') { - dispatch(setAlertFlyoutType(value)); - dispatch(setAlertFlyoutVisible(true)); - } else { - dispatch(setAlertFlyoutVisible(value)); - } - }} - /> - ); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/synthetics_alerts_flyout_wrapper.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/synthetics_alerts_flyout_wrapper.tsx deleted file mode 100644 index 33c76176787cf3..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/synthetics_alerts_flyout_wrapper.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useMemo } from 'react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; - -interface Props { - alertFlyoutVisible: boolean; - alertTypeId?: string; - setAlertFlyoutVisibility: React.Dispatch>; -} - -interface KibanaDeps { - triggersActionsUi: TriggersAndActionsUIPublicPluginStart; -} - -export const SyntheticsAlertsFlyoutWrapperComponent = ({ - alertFlyoutVisible, - alertTypeId, - setAlertFlyoutVisibility, -}: Props) => { - const { triggersActionsUi } = useKibana().services; - const onCloseAlertFlyout = useCallback( - () => setAlertFlyoutVisibility(false), - [setAlertFlyoutVisibility] - ); - const AddAlertFlyout = useMemo( - () => - triggersActionsUi.getAddAlertFlyout({ - consumer: 'synthetics', - onClose: onCloseAlertFlyout, - ruleTypeId: alertTypeId, - canChangeTrigger: !alertTypeId, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [onCloseAlertFlyout, alertTypeId] - ); - - return <>{alertFlyoutVisible && AddAlertFlyout}; -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.test.tsx deleted file mode 100644 index b185d3897d243b..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import userEvent from '@testing-library/user-event'; -import { render, forNearestButton, makeSyntheticsPermissionsCore } from '../../../utils/testing'; -import { ToggleAlertFlyoutButtonComponent } from './toggle_alert_flyout_button'; -import { ToggleFlyoutTranslations } from './translations'; - -describe('ToggleAlertFlyoutButtonComponent', () => { - describe('when users have write access to synthetics', () => { - it('enables the button to create a rule', () => { - const { getByText } = render( - , - { core: makeSyntheticsPermissionsCore({ save: true }) } - ); - userEvent.click(getByText('Alerts and rules')); - expect( - forNearestButton(getByText)(ToggleFlyoutTranslations.openAlertContextPanelLabel) - ).toBeEnabled(); - }); - - it("does not contain a tooltip explaining why the user can't create alerts", async () => { - const { getByText, findByText } = render( - , - { core: makeSyntheticsPermissionsCore({ save: true }) } - ); - userEvent.click(getByText('Alerts and rules')); - userEvent.hover(getByText(ToggleFlyoutTranslations.openAlertContextPanelLabel)); - await expect(() => - findByText('You need read-write access to Synthetics to create alerts in this app.') - ).rejects.toEqual(expect.anything()); - }); - }); - - describe("when users don't have write access to uptime", () => { - it('disables the button to create a rule', () => { - const { getByText } = render( - , - { core: makeSyntheticsPermissionsCore({ save: false }) } - ); - userEvent.click(getByText('Alerts and rules')); - expect( - forNearestButton(getByText)(ToggleFlyoutTranslations.openAlertContextPanelLabel) - ).toBeDisabled(); - }); - - it("contains a tooltip explaining why users can't create rules", async () => { - const { getByText, findByText } = render( - , - { core: makeSyntheticsPermissionsCore({ save: false }) } - ); - userEvent.click(getByText('Alerts and rules')); - userEvent.hover(getByText(ToggleFlyoutTranslations.openAlertContextPanelLabel)); - expect( - await findByText('You need read-write access to Uptime to create alerts in this app.') - ).toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.tsx deleted file mode 100644 index f29fe0941ee825..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.tsx +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiHeaderLink, - EuiContextMenu, - EuiContextMenuPanelDescriptor, - EuiContextMenuPanelItemDescriptor, - EuiLink, - EuiPopover, -} from '@elastic/eui'; -import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { CLIENT_ALERT_TYPES } from '../../../../../../common/constants/alerts'; -import { ClientPluginsStart } from '../../../../../plugin'; - -import { ToggleFlyoutTranslations } from './translations'; - -interface ComponentProps { - setAlertFlyoutVisible: (value: boolean | string) => void; -} - -export interface ToggleAlertFlyoutButtonProps { - alertOptions?: string[]; -} - -type Props = ComponentProps & ToggleAlertFlyoutButtonProps; - -const ALERT_CONTEXT_MAIN_PANEL_ID = 0; -const ALERT_CONTEXT_SELECT_TYPE_PANEL_ID = 1; - -const noWritePermissionsTooltipContent = i18n.translate( - 'xpack.synthetics.alertDropdown.noWritePermissions', - { - defaultMessage: 'You need read-write access to Uptime to create alerts in this app.', - } -); - -export const ToggleAlertFlyoutButtonComponent: React.FC = ({ - alertOptions, - setAlertFlyoutVisible, -}) => { - const [isOpen, setIsOpen] = useState(false); - const kibana = useKibana(); - const { - services: { observability }, - } = useKibana(); - const manageRulesUrl = observability.useRulesLink(); - const hasUptimeWrite = kibana.services.application?.capabilities.uptime?.save ?? false; - - const monitorStatusAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = { - 'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel, - 'data-test-subj': 'xpack.synthetics.toggleAlertFlyout', - name: ToggleFlyoutTranslations.toggleMonitorStatusContent, - onClick: () => { - setAlertFlyoutVisible(CLIENT_ALERT_TYPES.MONITOR_STATUS); - setIsOpen(false); - }, - }; - - const tlsAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = { - 'aria-label': ToggleFlyoutTranslations.toggleTlsAriaLabel, - 'data-test-subj': 'xpack.synthetics.toggleTlsAlertFlyout', - name: ToggleFlyoutTranslations.toggleTlsContent, - onClick: () => { - setAlertFlyoutVisible(CLIENT_ALERT_TYPES.TLS); - setIsOpen(false); - }, - }; - - const managementContextItem: EuiContextMenuPanelItemDescriptor = { - 'aria-label': ToggleFlyoutTranslations.navigateToAlertingUIAriaLabel, - 'data-test-subj': 'xpack.synthetics.navigateToAlertingUi', - name: ( - - - - ), - icon: 'tableOfContents', - }; - - let selectionItems: EuiContextMenuPanelItemDescriptor[] = []; - if (!alertOptions) { - selectionItems = [monitorStatusAlertContextMenuItem, tlsAlertContextMenuItem]; - } else { - alertOptions.forEach((option) => { - if (option === CLIENT_ALERT_TYPES.MONITOR_STATUS) - selectionItems.push(monitorStatusAlertContextMenuItem); - else if (option === CLIENT_ALERT_TYPES.TLS) selectionItems.push(tlsAlertContextMenuItem); - }); - } - - if (selectionItems.length === 1) { - selectionItems[0].icon = 'bell'; - } - - let panels: EuiContextMenuPanelDescriptor[]; - if (selectionItems.length === 1) { - panels = [ - { - id: ALERT_CONTEXT_MAIN_PANEL_ID, - items: [...selectionItems, managementContextItem], - }, - ]; - } else { - panels = [ - { - id: ALERT_CONTEXT_MAIN_PANEL_ID, - items: [ - { - 'aria-label': ToggleFlyoutTranslations.openAlertContextPanelAriaLabel, - 'data-test-subj': 'xpack.synthetics.openAlertContextPanel', - name: ToggleFlyoutTranslations.openAlertContextPanelLabel, - icon: 'bell', - panel: ALERT_CONTEXT_SELECT_TYPE_PANEL_ID, - toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null, - disabled: !hasUptimeWrite, - }, - managementContextItem, - ], - }, - { - id: ALERT_CONTEXT_SELECT_TYPE_PANEL_ID, - title: ToggleFlyoutTranslations.toggleAlertFlyoutButtonLabel, - items: selectionItems, - }, - ]; - } - - return ( - setIsOpen(!isOpen)} - > - - - } - closePopover={() => setIsOpen(false)} - isOpen={isOpen} - ownFocus - panelPaddingSize="none" - > - - - ); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/translations.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/translations.ts deleted file mode 100644 index 0580528b6b38c8..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/translations.ts +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const SECONDS_TIME_RANGE = i18n.translate( - 'xpack.synthetics.alerts.timerangeUnitSelectable.secondsOption.ariaLabel', - { - defaultMessage: '"Seconds" time range select item', - } -); - -export const SECONDS = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.timerangeOption.seconds', - { - defaultMessage: 'seconds', - } -); - -export const MINUTES_TIME_RANGE = i18n.translate( - 'xpack.synthetics.alerts.timerangeUnitSelectable.minutesOption.ariaLabel', - { - defaultMessage: '"Minutes" time range select item', - } -); - -export const MINUTES = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.timerangeOption.minutes', - { - defaultMessage: 'minutes', - } -); - -export const HOURS_TIME_RANGE = i18n.translate( - 'xpack.synthetics.alerts.timerangeUnitSelectable.hoursOption.ariaLabel', - { - defaultMessage: '"Hours" time range select item', - } -); - -export const HOURS = i18n.translate('xpack.synthetics.alerts.monitorStatus.timerangeOption.hours', { - defaultMessage: 'hours', -}); - -export const DAYS_TIME_RANGE = i18n.translate( - 'xpack.synthetics.alerts.timerangeUnitSelectable.daysOption.ariaLabel', - { - defaultMessage: '"Days" time range select item', - } -); - -export const DAYS = i18n.translate('xpack.synthetics.alerts.monitorStatus.timerangeOption.days', { - defaultMessage: 'days', -}); - -export const WEEKS_TIME_RANGE = i18n.translate( - 'xpack.synthetics.alerts.timerangeUnitSelectable.weeksOption.ariaLabel', - { - defaultMessage: '"Weeks" time range select item', - } -); - -export const WEEKS = i18n.translate('xpack.synthetics.alerts.monitorStatus.timerangeOption.weeks', { - defaultMessage: 'weeks', -}); - -export const MONTHS_TIME_RANGE = i18n.translate( - 'xpack.synthetics.alerts.timerangeUnitSelectable.monthsOption.ariaLabel', - { - defaultMessage: '"Months" time range select item', - } -); - -export const MONTHS = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.timerangeOption.months', - { - defaultMessage: 'months', - } -); - -export const YEARS_TIME_RANGE = i18n.translate( - 'xpack.synthetics.alerts.timerangeUnitSelectable.yearsOption.ariaLabel', - { - defaultMessage: '"Years" time range select item', - } -); - -export const YEARS = i18n.translate('xpack.synthetics.alerts.monitorStatus.timerangeOption.years', { - defaultMessage: 'years', -}); - -export const ALERT_KUERY_BAR_ARIA = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.filterBar.ariaLabel', - { - defaultMessage: 'Input that allows filtering criteria for the monitor status alert', - } -); - -export const OPEN_THE_POPOVER_DOWN_COUNT = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.numTimesExpression.ariaLabel', - { - defaultMessage: 'Open the popover for down count input', - } -); - -export const ENTER_NUMBER_OF_DOWN_COUNTS = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.numTimesField.ariaLabel', - { - defaultMessage: 'Enter number of down counts required to trigger the alert', - } -); - -export const MATCHING_MONITORS_DOWN = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.numTimesExpression.matchingMonitors.description', - { - defaultMessage: 'matching monitors are down >', - } -); - -export const ANY_MONITOR_DOWN = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.numTimesExpression.anyMonitors.description', - { - defaultMessage: 'any monitor is down >', - } -); - -export const OPEN_THE_POPOVER_TIME_RANGE_VALUE = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.timerangeValueExpression.ariaLabel', - { - defaultMessage: 'Open the popover for time range value field', - } -); - -export const ENTER_NUMBER_OF_TIME_UNITS = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.timerangeValueField.ariaLabel', - { - defaultMessage: `Enter the number of time units for the alert's range`, - } -); - -export const ENTER_NUMBER_OF_TIME_UNITS_DESCRIPTION = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.timerangeValueField.expression', - { - defaultMessage: 'within', - } -); - -export const ENTER_NUMBER_OF_TIME_UNITS_VALUE = (value: number) => - i18n.translate('xpack.synthetics.alerts.monitorStatus.timerangeValueField.value', { - defaultMessage: 'last {value}', - values: { value }, - }); - -export const ENTER_AVAILABILITY_RANGE_ENABLED = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.availability.isEnabledCheckbox.label', - { - defaultMessage: 'Availability', - } -); - -export const ENTER_AVAILABILITY_RANGE_POPOVER_ARIA_LABEL = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.availability.timerangeValueField.popover.ariaLabel', - { - defaultMessage: 'Specify availability tracking time range', - } -); - -export const ENTER_AVAILABILITY_RANGE_UNITS_ARIA_LABEL = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.availability.timerangeValueField.ariaLabel', - { - defaultMessage: `Enter the number of units for the alert's availability check.`, - } -); - -export const ENTER_AVAILABILITY_RANGE_UNITS_DESCRIPTION = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.availability.timerangeValueField.expression', - { - defaultMessage: 'within the last', - } -); - -export const ENTER_AVAILABILITY_THRESHOLD_ARIA_LABEL = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.availability.threshold.ariaLabel', - { - defaultMessage: 'Specify availability thresholds for this alert', - } -); - -export const ENTER_AVAILABILITY_THRESHOLD_INPUT_ARIA_LABEL = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.availability.threshold.input.ariaLabel', - { - defaultMessage: 'Input an availability threshold to check for this alert', - } -); - -export const ENTER_AVAILABILITY_THRESHOLD_DESCRIPTION = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.availability.threshold.description', - { - defaultMessage: 'matching monitors are up in', - description: - 'This fragment explains that an alert will fire for monitors matching user-specified criteria', - } -); - -export const ENTER_ANY_AVAILABILITY_THRESHOLD_DESCRIPTION = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.availability.threshold.anyMonitorDescription', - { - defaultMessage: 'any monitor is up in', - description: - 'This fragment explains that an alert will fire for monitors matching user-specified criteria', - } -); - -export const ENTER_AVAILABILITY_THRESHOLD_VALUE = (value: string) => - i18n.translate('xpack.synthetics.alerts.monitorStatus.availability.threshold.value', { - defaultMessage: '< {value}% of checks', - description: - 'This fragment specifies criteria that will cause an alert to fire for uptime monitors', - values: { value }, - }); - -export const ENTER_AVAILABILITY_RANGE_SELECT_ARIA = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.availability.unit.selectable', - { - defaultMessage: 'Use this select to set the availability range units for this alert', - } -); - -export const ENTER_AVAILABILITY_RANGE_SELECT_HEADLINE = i18n.translate( - 'xpack.synthetics.alerts.monitorStatus.availability.unit.headline', - { - defaultMessage: 'Select time range unit', - } -); - -export const ADD_FILTER = i18n.translate('xpack.synthetics.alerts.monitorStatus.addFilter', { - defaultMessage: `Add filter`, -}); - -export const LOCATION = i18n.translate('xpack.synthetics.alerts.monitorStatus.addFilter.location', { - defaultMessage: `Location`, -}); - -export const TAG = i18n.translate('xpack.synthetics.alerts.monitorStatus.addFilter.tag', { - defaultMessage: `Tag`, -}); - -export const PORT = i18n.translate('xpack.synthetics.alerts.monitorStatus.addFilter.port', { - defaultMessage: `Port`, -}); - -export const TYPE = i18n.translate('xpack.synthetics.alerts.monitorStatus.addFilter.type', { - defaultMessage: `Type`, -}); - -export const TlsTranslations = { - criteriaAriaLabel: i18n.translate('xpack.synthetics.alerts.tls.criteriaExpression.ariaLabel', { - defaultMessage: - 'An expression displaying the criteria for monitor that are watched by this alert', - }), - criteriaDescription: i18n.translate( - 'xpack.synthetics.alerts.tls.criteriaExpression.description', - { - defaultMessage: 'when', - description: - 'The context of this `when` is in the conditional sense, like "when there are three cookies, eat them all".', - } - ), - criteriaValue: i18n.translate('xpack.synthetics.alerts.tls.criteriaExpression.value', { - defaultMessage: 'any monitor', - }), - expirationAriaLabel: i18n.translate( - 'xpack.synthetics.alerts.tls.expirationExpression.ariaLabel', - { - defaultMessage: - 'An expression displaying the threshold that will trigger the TLS alert for certificate expiration', - } - ), - expirationDescription: i18n.translate( - 'xpack.synthetics.alerts.tls.expirationExpression.description', - { - defaultMessage: 'has a certificate expiring within', - } - ), - expirationValue: (value?: number) => - i18n.translate('xpack.synthetics.alerts.tls.expirationExpression.value', { - defaultMessage: '{value} days', - values: { value }, - }), - ageAriaLabel: i18n.translate('xpack.synthetics.alerts.tls.ageExpression.ariaLabel', { - defaultMessage: - 'An expressing displaying the threshold that will trigger the TLS alert for old certificates', - }), - ageDescription: i18n.translate('xpack.synthetics.alerts.tls.ageExpression.description', { - defaultMessage: 'or older than', - }), - ageValue: (value?: number) => - i18n.translate('xpack.synthetics.alerts.tls.ageExpression.value', { - defaultMessage: '{value} days', - values: { value }, - }), -}; - -export const ToggleFlyoutTranslations = { - toggleButtonAriaLabel: i18n.translate('xpack.synthetics.alertsPopover.toggleButton.ariaLabel', { - defaultMessage: 'Open alerts and rules context menu', - }), - openAlertContextPanelAriaLabel: i18n.translate( - 'xpack.synthetics.openAlertContextPanel.ariaLabel', - { - defaultMessage: 'Open the rule context panel so you can choose a rule type', - } - ), - openAlertContextPanelLabel: i18n.translate('xpack.synthetics.openAlertContextPanel.label', { - defaultMessage: 'Create rule', - }), - toggleTlsAriaLabel: i18n.translate('xpack.synthetics.toggleTlsAlertButton.ariaLabel', { - defaultMessage: 'Open TLS rule flyout', - }), - toggleTlsContent: i18n.translate('xpack.synthetics.toggleTlsAlertButton.content', { - defaultMessage: 'TLS rule', - }), - toggleMonitorStatusAriaLabel: i18n.translate('xpack.synthetics.toggleAlertFlyout.ariaLabel', { - defaultMessage: 'Open add rule flyout', - }), - toggleMonitorStatusContent: i18n.translate('xpack.synthetics.toggleAlertButton.content', { - defaultMessage: 'Monitor status rule', - }), - navigateToAlertingUIAriaLabel: i18n.translate('xpack.synthetics.navigateToAlertingUi', { - defaultMessage: 'Leave Uptime and go to Alerting Management page', - }), - navigateToAlertingButtonContent: i18n.translate( - 'xpack.synthetics.navigateToAlertingButton.content', - { - defaultMessage: 'Manage rules', - } - ), - toggleAlertFlyoutButtonLabel: i18n.translate('xpack.synthetics.alerts.createRulesPanel.title', { - defaultMessage: 'Create rules', - }), -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/filter_group/labels.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/filter_group/labels.ts deleted file mode 100644 index e1cf5e20e14c31..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/filter_group/labels.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const filterLabels = { - LOCATION: i18n.translate('xpack.synthetics.filterBar.options.location.name', { - defaultMessage: 'Location', - }), - - PORT: i18n.translate('xpack.synthetics.filterBar.options.portLabel', { defaultMessage: 'Port' }), - - SCHEME: i18n.translate('xpack.synthetics.filterBar.options.schemeLabel', { - defaultMessage: 'Scheme', - }), - - TAG: i18n.translate('xpack.synthetics.filterBar.options.tagsLabel', { - defaultMessage: 'Tag', - }), -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts index a7df47d7a0f715..15079dc68823b8 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts @@ -6,9 +6,8 @@ */ export * from './use_url_params'; -export * from './use_filter_update'; export * from './use_breadcrumbs'; export * from './use_telemetry'; -export * from './use_breakpoints'; +export * from '../../../hooks/use_breakpoints'; export * from './use_service_allowed'; export * from './use_no_data_config'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.test.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.test.ts deleted file mode 100644 index da3a25a5fc9df6..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { addUpdatedField } from './use_filter_update'; - -describe('useFilterUpdate', () => { - describe('addUpdatedField', () => { - it('conditionally adds fields if they are new', () => { - const testVal = {}; - addUpdatedField('a val', 'newField', 'a new val', testVal); - expect(testVal).toEqual({ - newField: 'a new val', - }); - }); - - it('will add a field if the value is the same but not the default', () => { - const testVal = {}; - addUpdatedField('a val', 'newField', 'a val', testVal); - expect(testVal).toEqual({ newField: 'a val' }); - }); - - it(`won't add a field if the current value is empty`, () => { - const testVal = {}; - addUpdatedField('', 'newField', '', testVal); - expect(testVal).toEqual({}); - }); - }); -}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.ts deleted file mode 100644 index 5578230ab2cf0d..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect } from 'react'; -import { useUrlParams } from './use_url_params'; - -export const parseFiltersMap = (currentFilters: string): Map => { - try { - return new Map(JSON.parse(currentFilters)); - } catch { - return new Map(); - } -}; - -const getUpdateFilters = ( - filterKueries: Map, - fieldName: string, - values?: string[] -): string => { - // add new term to filter map, toggle it off if already present - const updatedFilterMap = new Map(filterKueries); - updatedFilterMap.set(fieldName, values ?? []); - updatedFilterMap.forEach((value, key) => { - if (typeof value !== 'undefined' && value.length === 0) { - updatedFilterMap.delete(key); - } - }); - - // store the new set of filters - const persistedFilters = Array.from(updatedFilterMap); - return persistedFilters.length === 0 ? '' : JSON.stringify(persistedFilters); -}; - -export function addUpdatedField( - current: string, - key: string, - updated: string, - objToUpdate: { [key: string]: string } -): void { - if (current !== updated || current !== '') { - objToUpdate[key] = updated; - } -} - -export const useFilterUpdate = ( - fieldName: string, - values: string[], - notValues: string[], - shouldUpdateUrl: boolean = true -) => { - const [getUrlParams, updateUrl] = useUrlParams(); - - const { filters, excludedFilters } = getUrlParams(); - - useEffect(() => { - const currentFiltersMap: Map = parseFiltersMap(filters); - const currentExclusionsMap: Map = parseFiltersMap(excludedFilters); - const newFiltersString = getUpdateFilters(currentFiltersMap, fieldName, values); - const newExclusionsString = getUpdateFilters(currentExclusionsMap, fieldName, notValues); - - const update: { [key: string]: string } = {}; - - addUpdatedField(filters, 'filters', newFiltersString, update); - addUpdatedField(excludedFilters, 'excludedFilters', newExclusionsString, update); - - if (shouldUpdateUrl && Object.keys(update).length > 0) { - // reset pagination whenever filters change - updateUrl({ ...update, pagination: '' }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fieldName, values, notValues]); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts index f97e4c4b2be097..64ecabaff5d5a5 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts @@ -7,7 +7,7 @@ import { useEffect } from 'react'; import { useGetUrlParams } from './use_url_params'; -import { apiService } from '../utils/api_service'; +import { apiService } from '../../../utils/api_service'; // import { API_URLS } from '../../../common/constants'; export enum SyntheticsPage { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index d20c390c84b599..7f04b3992885b8 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -23,7 +23,7 @@ import { OVERVIEW_ROUTE, } from '../../../common/constants'; import { MonitorManagementPage } from './components/monitor_management/monitor_management_page'; -import { apiService } from './utils/api_service'; +import { apiService } from '../../utils/api_service'; import { SyntheticsPage, useSyntheticsTelemetry } from './hooks/use_telemetry'; type RouteProps = { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/api.ts index f2d5e326ba2ab7..ba6ded899f9c44 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/api.ts @@ -7,7 +7,7 @@ import { API_URLS } from '../../../../../common/constants'; import { StatesIndexStatus, StatesIndexStatusType } from '../../../../../common/runtime_types'; -import { apiService } from '../../utils/api_service'; +import { apiService } from '../../../../utils/api_service'; export const fetchIndexStatus = async (): Promise => { return await apiService.get(API_URLS.INDEX_STATUS, undefined, StatesIndexStatusType); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx index 614f77ddff5d74..07fb3604abd42c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx @@ -17,7 +17,6 @@ import { } from '@kbn/kibana-react-plugin/public'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { InspectorContextProvider } from '@kbn/observability-plugin/public'; -import { SyntheticsAlertsFlyoutWrapper } from './components/overview/alerts/alerts_containers/synthetics_alerts_flyout_wrapper'; import { SyntheticsAppProps } from './contexts'; import { @@ -30,7 +29,7 @@ import { import { PageRouter } from './routes'; import { store, storage, setBasePath } from './state'; -import { kibanaService } from './utils/kibana_service'; +import { kibanaService } from '../../utils/kibana_service'; import { ActionMenu } from './components/common/header/action_menu'; const Application = (props: SyntheticsAppProps) => { @@ -99,7 +98,6 @@ const Application = (props: SyntheticsAppProps) => { application={core.application} > - diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx index 51c186c352a5be..71d86cc53a76b8 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx @@ -36,7 +36,7 @@ import { SyntheticsRefreshContextProvider, SyntheticsStartupPluginsContextProvider, } from '../../contexts'; -import { kibanaService } from '../kibana_service'; +import { kibanaService } from '../../../../utils/kibana_service'; type DeepPartial = { [P in keyof T]?: DeepPartial; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breakpoints.test.ts b/x-pack/plugins/synthetics/public/hooks/use_breakpoints.test.ts similarity index 100% rename from x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breakpoints.test.ts rename to x-pack/plugins/synthetics/public/hooks/use_breakpoints.test.ts diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breakpoints.ts b/x-pack/plugins/synthetics/public/hooks/use_breakpoints.ts similarity index 100% rename from x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breakpoints.ts rename to x-pack/plugins/synthetics/public/hooks/use_breakpoints.ts diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.test.tsx index 57ae3a6514505e..4efaf26a7ac115 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.test.tsx @@ -11,9 +11,9 @@ import 'jest-styled-components'; import { render } from '../lib/helper/rtl_helpers'; import { UptimePageTemplateComponent } from './uptime_page_template'; import { OVERVIEW_ROUTE } from '../../../common/constants'; -import { useBreakpoints } from '../../apps/synthetics/hooks/use_breakpoints'; +import { useBreakpoints } from '../../hooks/use_breakpoints'; -jest.mock('../../apps/synthetics/hooks/use_breakpoints', () => { +jest.mock('../../hooks/use_breakpoints', () => { const down = jest.fn().mockReturnValue(false); return { useBreakpoints: () => ({ down }), diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx index ade54e1e6f61a3..fa3ad7e0805e89 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx @@ -16,7 +16,7 @@ import { useNoDataConfig } from './use_no_data_config'; import { EmptyStateLoading } from '../components/overview/empty_state/empty_state_loading'; import { EmptyStateError } from '../components/overview/empty_state/empty_state_error'; import { useHasData } from '../components/overview/empty_state/use_has_data'; -import { useBreakpoints } from '../../apps/synthetics/hooks/use_breakpoints'; +import { useBreakpoints } from '../../hooks/use_breakpoints'; interface Props { path: string; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx index 0e4d03e3ce438c..73996c4e3a1b79 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx @@ -10,7 +10,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { ScreenshotRefImageData } from '../../../../../../../common/runtime_types'; -import { useBreakpoints } from '../../../../../../apps/synthetics/hooks/use_breakpoints'; +import { useBreakpoints } from '../../../../../../hooks/use_breakpoints'; import { nextAriaLabel, prevAriaLabel } from './translations'; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx index 0c1d56be587a41..4b9374e991e6bb 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx @@ -27,7 +27,7 @@ import { TCPSimpleFields, } from '../../../../../common/runtime_types'; import { UptimeSettingsContext } from '../../../contexts'; -import { useBreakpoints } from '../../../../apps/synthetics/hooks/use_breakpoints'; +import { useBreakpoints } from '../../../../hooks/use_breakpoints'; import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management'; import * as labels from '../../overview/monitor_list/translations'; import { Actions } from './actions'; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/checks_navigation.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/checks_navigation.tsx index c09da77a6f5595..f9d98b7b640c68 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/checks_navigation.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/checks_navigation.tsx @@ -12,7 +12,7 @@ import { useHistory } from 'react-router-dom'; import moment from 'moment'; import { SyntheticsJourneyApiResponse } from '../../../../common/runtime_types/ping'; import { getShortTimeStamp } from '../../components/overview/monitor_list/columns/monitor_status_column'; -import { useBreakpoints } from '../../../apps/synthetics/hooks/use_breakpoints'; +import { useBreakpoints } from '../../../hooks/use_breakpoints'; interface Props { timestamp: string; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/api_service/api_service.ts b/x-pack/plugins/synthetics/public/utils/api_service/api_service.ts similarity index 100% rename from x-pack/plugins/synthetics/public/apps/synthetics/utils/api_service/api_service.ts rename to x-pack/plugins/synthetics/public/utils/api_service/api_service.ts diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/api_service/index.ts b/x-pack/plugins/synthetics/public/utils/api_service/index.ts similarity index 100% rename from x-pack/plugins/synthetics/public/apps/synthetics/utils/api_service/index.ts rename to x-pack/plugins/synthetics/public/utils/api_service/index.ts diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/kibana_service/index.ts b/x-pack/plugins/synthetics/public/utils/kibana_service/index.ts similarity index 100% rename from x-pack/plugins/synthetics/public/apps/synthetics/utils/kibana_service/index.ts rename to x-pack/plugins/synthetics/public/utils/kibana_service/index.ts diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/kibana_service/kibana_service.ts b/x-pack/plugins/synthetics/public/utils/kibana_service/kibana_service.ts similarity index 100% rename from x-pack/plugins/synthetics/public/apps/synthetics/utils/kibana_service/kibana_service.ts rename to x-pack/plugins/synthetics/public/utils/kibana_service/kibana_service.ts From ee846edf070ba20ec82d9fce85917c46d2ab8b30 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 6 May 2022 13:24:24 -0500 Subject: [PATCH 70/83] [ci] bump kibana-buildkite-library (#131754) --- .buildkite/package-lock.json | 12 ++++++------ .buildkite/package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.buildkite/package-lock.json b/.buildkite/package-lock.json index 04e3c73fdd2f53..d848470b3ff68b 100644 --- a/.buildkite/package-lock.json +++ b/.buildkite/package-lock.json @@ -8,7 +8,7 @@ "name": "kibana-buildkite", "version": "1.0.0", "dependencies": { - "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#ef419d4f761dd256cb59bfab9b59f8b91029eb40" + "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#b9f6b423059cac7554a7402277f2ad3ecfe132a4" } }, "node_modules/@nodelib/fs.scandir": { @@ -355,8 +355,8 @@ }, "node_modules/kibana-buildkite-library": { "version": "1.0.0", - "resolved": "git+https://git@github.com/elastic/kibana-buildkite-library.git#ef419d4f761dd256cb59bfab9b59f8b91029eb40", - "integrity": "sha512-ou/Db/DAhyMeD0uQeLpJ/VfUCg0PGPssIYsr4gJZSTZoqxCDrMtE4nhtH8sXErHq0TaugdqergUhyhVHNBSJiA==", + "resolved": "git+https://git@github.com/elastic/kibana-buildkite-library.git#b9f6b423059cac7554a7402277f2ad3ecfe132a4", + "integrity": "sha512-HsSPeCrKwJKa+1urq/AzELmA1hsrwZjmOKzWzEYcQ63ZAnn8G3QWrGL0dNZQxIto3243EEs6Ne1pUVTMtEJr+Q==", "license": "MIT", "dependencies": { "@octokit/rest": "^18.10.0", @@ -801,9 +801,9 @@ } }, "kibana-buildkite-library": { - "version": "git+https://git@github.com/elastic/kibana-buildkite-library.git#ef419d4f761dd256cb59bfab9b59f8b91029eb40", - "integrity": "sha512-ou/Db/DAhyMeD0uQeLpJ/VfUCg0PGPssIYsr4gJZSTZoqxCDrMtE4nhtH8sXErHq0TaugdqergUhyhVHNBSJiA==", - "from": "kibana-buildkite-library@git+https://git@github.com/elastic/kibana-buildkite-library#ef419d4f761dd256cb59bfab9b59f8b91029eb40", + "version": "git+https://git@github.com/elastic/kibana-buildkite-library.git#b9f6b423059cac7554a7402277f2ad3ecfe132a4", + "integrity": "sha512-HsSPeCrKwJKa+1urq/AzELmA1hsrwZjmOKzWzEYcQ63ZAnn8G3QWrGL0dNZQxIto3243EEs6Ne1pUVTMtEJr+Q==", + "from": "kibana-buildkite-library@git+https://git@github.com/elastic/kibana-buildkite-library#b9f6b423059cac7554a7402277f2ad3ecfe132a4", "requires": { "@octokit/rest": "^18.10.0", "axios": "^0.21.4", diff --git a/.buildkite/package.json b/.buildkite/package.json index daff8bd5db7814..4e46ba6637027b 100644 --- a/.buildkite/package.json +++ b/.buildkite/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "private": true, "dependencies": { - "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#ef419d4f761dd256cb59bfab9b59f8b91029eb40" + "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#b9f6b423059cac7554a7402277f2ad3ecfe132a4" } } From a6f7c46786d15cf3fc792e548c96ea274edee523 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 6 May 2022 15:27:42 -0400 Subject: [PATCH 71/83] [Connector] Adding internal route for requesting ad-hoc ServiceNow access token (#131171) * Adding new OAuth fields to ServiceNow ExternalIncidentServiceConfigurationBase and ExternalIncidentServiceSecretConfiguration * Creating new function in ConnectorTokenClient for updating or replacing token * Update servicenow executors to get Oauth access tokens if configured. Still need to update unit tests for services * Creating wrapper function for createService to only create one axios instance * Fixing translation check error * Adding migration for adding isOAuth to service now connectors * Fixing unit tests * Fixing functional test * Not requiring privateKeyPassword * Fixing tests * Adding functional tests for connector creation * Adding functional tests * Fixing functional test * PR feedback * Adding route for requesting access token using OAuth credentials * Fixing test * Adding functional test * Fixing functional test * Fixing checks * Using existing private key * Refactoring get access token utilities to be more generic * Checking tokenurl against allowlist * Restricting access to users with ability to update connectors * Adding slashesDenotesHost parameter to url.parse * Removing ability to specify custom claims for jwt assertion * Verifying that token url contains hostname and uses https Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../actions/server/actions_client.mock.ts | 1 + .../actions/server/actions_client.test.ts | 310 ++++++++++- .../plugins/actions/server/actions_client.ts | 110 ++++ .../actions/server/actions_config.test.ts | 11 + .../plugins/actions/server/actions_config.ts | 2 +- .../lib/create_jwt_assertion.ts | 4 +- ...th_client_credentials_access_token.test.ts | 306 ++++++++++ ...t_oauth_client_credentials_access_token.ts | 99 ++++ .../lib/get_oauth_jwt_access_token.test.ts | 350 ++++++++++++ .../lib/get_oauth_jwt_access_token.ts | 109 ++++ .../lib/send_email.test.ts | 521 ++---------------- .../builtin_action_types/lib/send_email.ts | 53 +- .../servicenow/utils.test.ts | 225 +------- .../builtin_action_types/servicenow/utils.ts | 101 +--- x-pack/plugins/actions/server/plugin.test.ts | 2 +- x-pack/plugins/actions/server/plugin.ts | 11 +- .../routes/get_oauth_access_token.test.ts | 227 ++++++++ .../server/routes/get_oauth_access_token.ts | 81 +++ x-pack/plugins/actions/server/routes/index.ts | 21 +- .../actions_simulators/server/plugin.ts | 6 + .../server/servicenow_oauth_simulation.ts | 45 ++ .../oauth_access_token.ts | 170 ++++++ .../group2/tests/actions/index.ts | 1 + 23 files changed, 1948 insertions(+), 818 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts create mode 100644 x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts create mode 100644 x-pack/plugins/actions/server/routes/get_oauth_access_token.ts create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_oauth_simulation.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/oauth_access_token.ts diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 419babe97c0f47..246c8fa35fc15e 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -19,6 +19,7 @@ const createActionsClientMock = () => { update: jest.fn(), getAll: jest.fn(), getBulk: jest.fn(), + getOAuthAccessToken: jest.fn(), execute: jest.fn(), enqueueExecution: jest.fn(), ephemeralEnqueuedExecution: jest.fn(), diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index afee13b8c9bcab..787b4e450a9e06 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -18,11 +18,14 @@ import { actionsConfigMock } from './actions_config.mock'; import { getActionsConfigurationUtilities } from './actions_config'; import { licenseStateMock } from './lib/license_state.mock'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; -import { httpServerMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { + httpServerMock, + loggingSystemMock, + elasticsearchServiceMock, + savedObjectsClientMock, +} from '@kbn/core/server/mocks'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; - -import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; import { actionExecutorMock } from './lib/action_executor.mock'; import uuid from 'uuid'; import { ActionsAuthorization } from './authorization/actions_authorization'; @@ -37,6 +40,9 @@ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/s import { Logger } from '@kbn/core/server'; import { connectorTokenClientMock } from './builtin_action_types/lib/connector_token_client.mock'; import { inMemoryMetricsMock } from './monitoring/in_memory_metrics.mock'; +import { getOAuthJwtAccessToken } from './builtin_action_types/lib/get_oauth_jwt_access_token'; +import { getOAuthClientCredentialsAccessToken } from './builtin_action_types/lib/get_oauth_client_credentials_access_token'; +import { OAuthParams } from './routes/get_oauth_access_token'; jest.mock('@kbn/core/server/saved_objects/service/lib/utils', () => ({ SavedObjectsUtils: { @@ -60,6 +66,13 @@ jest.mock('./authorization/get_authorization_mode_by_source', () => { }; }); +jest.mock('./builtin_action_types/lib/get_oauth_jwt_access_token', () => ({ + getOAuthJwtAccessToken: jest.fn(), +})); +jest.mock('./builtin_action_types/lib/get_oauth_client_credentials_access_token', () => ({ + getOAuthClientCredentialsAccessToken: jest.fn(), +})); + const defaultKibanaIndex = '.kibana'; const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); @@ -73,6 +86,7 @@ const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const logger = loggingSystemMock.create().get() as jest.Mocked; const mockTaskManager = taskManagerMock.createSetup(); +const configurationUtilities = actionsConfigMock.create(); let actionsClient: ActionsClient; let mockedLicenseState: jest.Mocked; @@ -115,6 +129,10 @@ beforeEach(() => { usageCounter: mockUsageCounter, connectorTokenClient, }); + (getOAuthJwtAccessToken as jest.Mock).mockResolvedValue(`Bearer jwttokentokentoken`); + (getOAuthClientCredentialsAccessToken as jest.Mock).mockResolvedValue( + `Bearer clienttokentokentoken` + ); }); describe('create()', () => { @@ -1274,6 +1292,292 @@ describe('getBulk()', () => { }); }); +describe('getOAuthAccessToken()', () => { + function getOAuthAccessToken( + requestBody: OAuthParams + ): ReturnType { + actionsClient = new ActionsClient({ + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + actionExecutor, + executionEnqueuer, + ephemeralExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + preconfiguredActions: [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + isDeprecated: false, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + connectorTokenClient: connectorTokenClientMock.create(), + }); + return actionsClient.getOAuthAccessToken(requestBody, logger, configurationUtilities); + } + + describe('authorization', () => { + test('ensures user is authorised to get the type of action', async () => { + await getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update'); + }); + + test('throws when user is not authorised to create the type of action', async () => { + authorization.ensureAuthorized.mockRejectedValue(new Error(`Unauthorized to update actions`)); + + await expect( + getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to update actions]`); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update'); + }); + }); + + test('throws when tokenUrl is not using http or https', async () => { + await expect( + getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: 'ftp://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Token URL must use http or https]`); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update'); + }); + + test('throws when tokenUrl does not contain hostname', async () => { + await expect( + getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: '/path/to/myfile', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Token URL must contain hostname]`); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update'); + }); + + test('throws when tokenUrl is not in allowed hosts', async () => { + configurationUtilities.ensureUriAllowed.mockImplementationOnce(() => { + throw new Error('URI not allowed'); + }); + + await expect( + getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: URI not allowed]`); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update'); + expect(configurationUtilities.ensureUriAllowed).toHaveBeenCalledWith( + `https://testurl.service-now.com/oauth_token.do` + ); + }); + + test('calls getOAuthJwtAccessToken when type="jwt"', async () => { + const result = await getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }); + expect(result).toEqual({ + accessToken: 'Bearer jwttokentokentoken', + }); + expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalledWith({ + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + }); + expect(getOAuthClientCredentialsAccessToken).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + `Successfully retrieved access token using JWT OAuth with tokenUrl https://testurl.service-now.com/oauth_token.do and config {\"clientId\":\"abc\",\"jwtKeyId\":\"def\",\"userIdentifierValue\":\"userA\"}` + ); + }); + + test('calls getOAuthClientCredentialsAccessToken when type="client"', async () => { + const result = await getOAuthAccessToken({ + type: 'client', + options: { + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + scope: 'https://graph.microsoft.com/.default', + config: { + clientId: 'abc', + tenantId: 'def', + }, + secrets: { + clientSecret: 'iamasecret', + }, + }, + }); + expect(result).toEqual({ + accessToken: 'Bearer clienttokentokentoken', + }); + expect(getOAuthClientCredentialsAccessToken as jest.Mock).toHaveBeenCalledWith({ + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'abc', + tenantId: 'def', + }, + secrets: { + clientSecret: 'iamasecret', + }, + }, + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + oAuthScope: 'https://graph.microsoft.com/.default', + }); + expect(getOAuthJwtAccessToken).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + `Successfully retrieved access token using Client Credentials OAuth with tokenUrl https://login.microsoftonline.com/98765/oauth2/v2.0/token, scope https://graph.microsoft.com/.default and config {\"clientId\":\"abc\",\"tenantId\":\"def\"}` + ); + }); + + test('throws when getOAuthJwtAccessToken throws error', async () => { + (getOAuthJwtAccessToken as jest.Mock).mockRejectedValue(new Error(`Something went wrong!`)); + + await expect( + getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Failed to retrieve access token]`); + + expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + `Failed to retrieve access token using JWT OAuth with tokenUrl https://testurl.service-now.com/oauth_token.do and config {\"clientId\":\"abc\",\"jwtKeyId\":\"def\",\"userIdentifierValue\":\"userA\"} - Something went wrong!` + ); + }); + + test('throws when getOAuthClientCredentialsAccessToken throws error', async () => { + (getOAuthClientCredentialsAccessToken as jest.Mock).mockRejectedValue( + new Error(`Something went wrong!`) + ); + + await expect( + getOAuthAccessToken({ + type: 'client', + options: { + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + scope: 'https://graph.microsoft.com/.default', + config: { + clientId: 'abc', + tenantId: 'def', + }, + secrets: { + clientSecret: 'iamasecret', + }, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Failed to retrieve access token]`); + + expect(getOAuthClientCredentialsAccessToken as jest.Mock).toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + `Failed to retrieved access token using Client Credentials OAuth with tokenUrl https://login.microsoftonline.com/98765/oauth2/v2.0/token, scope https://graph.microsoft.com/.default and config {\"clientId\":\"abc\",\"tenantId\":\"def\"} - Something went wrong!` + ); + }); +}); + describe('delete()', () => { describe('authorization', () => { test('ensures user is authorised to delete actions', async () => { diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index dacf6de36bd37d..89156bb56b51a8 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -6,6 +6,7 @@ */ import Boom from '@hapi/boom'; +import url from 'url'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; @@ -18,6 +19,7 @@ import { SavedObject, KibanaRequest, SavedObjectsUtils, + Logger, } from '@kbn/core/server'; import { AuditLogger } from '@kbn/security-plugin/server'; import { RunNowResult } from '@kbn/task-manager-plugin/server'; @@ -46,6 +48,22 @@ import { import { connectorAuditEvent, ConnectorAuditAction } from './lib/audit_events'; import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption'; import { isConnectorDeprecated } from './lib/is_conector_deprecated'; +import { ActionsConfigurationUtilities } from './actions_config'; +import { + OAuthClientCredentialsParams, + OAuthJwtParams, + OAuthParams, +} from './routes/get_oauth_access_token'; +import { + getOAuthJwtAccessToken, + GetOAuthJwtConfig, + GetOAuthJwtSecrets, +} from './builtin_action_types/lib/get_oauth_jwt_access_token'; +import { + getOAuthClientCredentialsAccessToken, + GetOAuthClientCredentialsConfig, + GetOAuthClientCredentialsSecrets, +} from './builtin_action_types/lib/get_oauth_client_credentials_access_token'; // 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. @@ -448,6 +466,98 @@ export class ActionsClient { return actionResults; } + public async getOAuthAccessToken( + { type, options }: OAuthParams, + logger: Logger, + configurationUtilities: ActionsConfigurationUtilities + ) { + // Verify that user has edit access + await this.authorization.ensureAuthorized('update'); + + // Verify that token url is allowed by allowed hosts config + try { + configurationUtilities.ensureUriAllowed(options.tokenUrl); + } catch (err) { + throw Boom.badRequest(err.message); + } + + // Verify that token url contains a hostname and uses https + const parsedUrl = url.parse( + options.tokenUrl, + false /* parseQueryString */, + true /* slashesDenoteHost */ + ); + + if (!parsedUrl.hostname) { + throw Boom.badRequest(`Token URL must contain hostname`); + } + + if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') { + throw Boom.badRequest(`Token URL must use http or https`); + } + + let accessToken: string | null = null; + if (type === 'jwt') { + const tokenOpts = options as OAuthJwtParams; + + try { + accessToken = await getOAuthJwtAccessToken({ + logger, + configurationUtilities, + credentials: { + config: tokenOpts.config as GetOAuthJwtConfig, + secrets: tokenOpts.secrets as GetOAuthJwtSecrets, + }, + tokenUrl: tokenOpts.tokenUrl, + }); + + logger.debug( + `Successfully retrieved access token using JWT OAuth with tokenUrl ${ + tokenOpts.tokenUrl + } and config ${JSON.stringify(tokenOpts.config)}` + ); + } catch (err) { + logger.debug( + `Failed to retrieve access token using JWT OAuth with tokenUrl ${ + tokenOpts.tokenUrl + } and config ${JSON.stringify(tokenOpts.config)} - ${err.message}` + ); + throw Boom.badRequest(`Failed to retrieve access token`); + } + } else if (type === 'client') { + const tokenOpts = options as OAuthClientCredentialsParams; + try { + accessToken = await getOAuthClientCredentialsAccessToken({ + logger, + configurationUtilities, + credentials: { + config: tokenOpts.config as GetOAuthClientCredentialsConfig, + secrets: tokenOpts.secrets as GetOAuthClientCredentialsSecrets, + }, + tokenUrl: tokenOpts.tokenUrl, + oAuthScope: tokenOpts.scope, + }); + + logger.debug( + `Successfully retrieved access token using Client Credentials OAuth with tokenUrl ${ + tokenOpts.tokenUrl + }, scope ${tokenOpts.scope} and config ${JSON.stringify(tokenOpts.config)}` + ); + } catch (err) { + logger.debug( + `Failed to retrieved access token using Client Credentials OAuth with tokenUrl ${ + tokenOpts.tokenUrl + }, scope ${tokenOpts.scope} and config ${JSON.stringify(tokenOpts.config)} - ${ + err.message + }` + ); + throw Boom.badRequest(`Failed to retrieve access token`); + } + } + + return { accessToken }; + } + /** * Delete action */ diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index 470e6ce8cdc8e7..a6b68d907cb44c 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -129,6 +129,17 @@ describe('isUriAllowed', () => { ).toEqual(true); }); + test('returns true for network path references', () => { + const config: ActionsConfig = { + ...defaultActionsConfig, + allowedHosts: ['my-domain.com'], + enabledActionTypes: [], + }; + expect(getActionsConfigurationUtilities(config).isUriAllowed('//my-domain.com/foo')).toEqual( + true + ); + }); + test('throws when the hostname in the requested uri is not in the allowedHosts', () => { const config: ActionsConfig = defaultActionsConfig; expect( diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index 35e08bb5cfe669..49f1d1fd5445e4 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -76,7 +76,7 @@ function isAllowed({ allowedHosts }: ActionsConfig, hostname: string | null): bo function isHostnameAllowedInUri(config: ActionsConfig, uri: string): boolean { return pipe( - tryCatch(() => url.parse(uri)), + tryCatch(() => url.parse(uri, false /* parseQueryString */, true /* slashesDenoteHost */)), map((parsedUrl) => parsedUrl.hostname), mapNullable((hostname) => isAllowed(config, hostname)), getOrElse(() => false) diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts index b33a2d17ed9d84..9dde4790c152d3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts @@ -20,8 +20,7 @@ export function createJWTAssertion( logger: Logger, privateKey: string, privateKeyPassword: string | null, - reservedClaims: JWTClaims, - customClaims?: Record + reservedClaims: JWTClaims ): string { const { subject, audience, issuer, expireInMilliseconds, keyId } = reservedClaims; const iat = Math.floor(Date.now() / 1000); @@ -34,7 +33,6 @@ export function createJWTAssertion( iss: issuer, // issuer claim identifies the principal that issued the JWT iat, // issued at claim identifies the time at which the JWT was issued exp: iat + (expireInMilliseconds ?? 3600), // expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing - ...(customClaims ?? {}), }; try { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts new file mode 100644 index 00000000000000..2efa79cf09c488 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts @@ -0,0 +1,306 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Logger } from '@kbn/core/server'; +import { asyncForEach } from '@kbn/std'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { actionsConfigMock } from '../../actions_config.mock'; +import { connectorTokenClientMock } from './connector_token_client.mock'; +import { getOAuthClientCredentialsAccessToken } from './get_oauth_client_credentials_access_token'; +import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token'; + +jest.mock('./request_oauth_client_credentials_token', () => ({ + requestOAuthClientCredentialsToken: jest.fn(), +})); + +const logger = loggingSystemMock.create().get() as jest.Mocked; +const configurationUtilities = actionsConfigMock.create(); +const connectorTokenClient = connectorTokenClientMock.create(); + +describe('getOAuthClientCredentialsAccessToken', () => { + const getOAuthClientCredentialsAccessTokenOpts = { + connectorId: '123', + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + tenantId: 'tenantId', + }, + secrets: { + clientSecret: 'clientSecret', + }, + }, + oAuthScope: 'https://graph.microsoft.com/.default', + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + connectorTokenClient, + }; + + beforeEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + test('uses stored access token if it exists', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: { + id: '1', + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + createdAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 10000000000).toISOString(), + }, + }); + const accessToken = await getOAuthClientCredentialsAccessToken( + getOAuthClientCredentialsAccessTokenOpts + ); + + expect(accessToken).toEqual('testtokenvalue'); + expect(requestOAuthClientCredentialsToken as jest.Mock).not.toHaveBeenCalled(); + }); + + test('creates new assertion if stored access token does not exist', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthClientCredentialsAccessToken( + getOAuthClientCredentialsAccessTokenOpts + ); + + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(requestOAuthClientCredentialsToken as jest.Mock).toHaveBeenCalledWith( + 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + logger, + { + scope: 'https://graph.microsoft.com/.default', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + + configurationUtilities + ); + expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({ + connectorId: '123', + token: null, + newToken: 'access_token brandnewaccesstoken', + expiresInSec: 1000, + deleteExisting: false, + }); + }); + + test('creates new assertion if stored access token exists but is expired', async () => { + const createdAt = new Date().toISOString(); + const expiresAt = new Date(Date.now() - 100).toISOString(); + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: { + id: '1', + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + createdAt, + expiresAt, + }, + }); + (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthClientCredentialsAccessToken( + getOAuthClientCredentialsAccessTokenOpts + ); + + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(requestOAuthClientCredentialsToken as jest.Mock).toHaveBeenCalledWith( + 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + logger, + { + scope: 'https://graph.microsoft.com/.default', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + + configurationUtilities + ); + expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({ + connectorId: '123', + token: { + id: '1', + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + createdAt, + expiresAt, + }, + newToken: 'access_token brandnewaccesstoken', + expiresInSec: 1000, + deleteExisting: false, + }); + }); + + test('returns null and logs warning if any required fields are missing', async () => { + await asyncForEach(['clientId', 'tenantId'], async (configField: string) => { + const accessToken = await getOAuthClientCredentialsAccessToken({ + ...getOAuthClientCredentialsAccessTokenOpts, + credentials: { + config: { + ...getOAuthClientCredentialsAccessTokenOpts.credentials.config, + [configField]: null, + }, + secrets: getOAuthClientCredentialsAccessTokenOpts.credentials.secrets, + }, + }); + expect(accessToken).toBeNull(); + expect(logger.warn).toHaveBeenCalledWith( + `Missing required fields for requesting OAuth Client Credentials access token` + ); + }); + + await asyncForEach(['clientSecret'], async (secretsField: string) => { + const accessToken = await getOAuthClientCredentialsAccessToken({ + ...getOAuthClientCredentialsAccessTokenOpts, + credentials: { + config: getOAuthClientCredentialsAccessTokenOpts.credentials.config, + secrets: { + ...getOAuthClientCredentialsAccessTokenOpts.credentials.secrets, + [secretsField]: null, + }, + }, + }); + expect(accessToken).toBeNull(); + expect(logger.warn).toHaveBeenCalledWith( + `Missing required fields for requesting OAuth Client Credentials access token` + ); + }); + }); + + test('throws error if requestOAuthClientCredentialsToken throws error', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (requestOAuthClientCredentialsToken as jest.Mock).mockRejectedValueOnce( + new Error('requestOAuthClientCredentialsToken error!!') + ); + + await expect( + getOAuthClientCredentialsAccessToken(getOAuthClientCredentialsAccessTokenOpts) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"requestOAuthClientCredentialsToken error!!"`); + }); + + test('logs warning if connectorTokenClient.updateOrReplace throws error', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + connectorTokenClient.updateOrReplace.mockRejectedValueOnce(new Error('updateOrReplace error')); + + const accessToken = await getOAuthClientCredentialsAccessToken( + getOAuthClientCredentialsAccessTokenOpts + ); + + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(logger.warn).toHaveBeenCalledWith( + `Not able to update connector token for connectorId: 123 due to error: updateOrReplace error` + ); + }); + + test('gets access token if connectorId is not provided', async () => { + (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthClientCredentialsAccessToken({ + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + tenantId: 'tenantId', + }, + secrets: { + clientSecret: 'clientSecret', + }, + }, + oAuthScope: 'https://graph.microsoft.com/.default', + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + connectorTokenClient, + }); + + expect(connectorTokenClient.get).not.toHaveBeenCalled(); + expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled(); + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(requestOAuthClientCredentialsToken as jest.Mock).toHaveBeenCalledWith( + 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + logger, + { + scope: 'https://graph.microsoft.com/.default', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + + configurationUtilities + ); + }); + + test('gets access token if connectorTokenClient is not provided', async () => { + (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthClientCredentialsAccessToken({ + connectorId: '123', + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + tenantId: 'tenantId', + }, + secrets: { + clientSecret: 'clientSecret', + }, + }, + oAuthScope: 'https://graph.microsoft.com/.default', + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + }); + + expect(connectorTokenClient.get).not.toHaveBeenCalled(); + expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled(); + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(requestOAuthClientCredentialsToken as jest.Mock).toHaveBeenCalledWith( + 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + logger, + { + scope: 'https://graph.microsoft.com/.default', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + + configurationUtilities + ); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts new file mode 100644 index 00000000000000..803cce2db76681 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Logger } from '@kbn/core/server'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ConnectorToken, ConnectorTokenClientContract } from '../../types'; +import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token'; + +export interface GetOAuthClientCredentialsConfig { + clientId: string; + tenantId: string; +} + +export interface GetOAuthClientCredentialsSecrets { + clientSecret: string; +} + +interface GetOAuthClientCredentialsAccessTokenOpts { + connectorId?: string; + tokenUrl: string; + oAuthScope: string; + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; + credentials: { + config: GetOAuthClientCredentialsConfig; + secrets: GetOAuthClientCredentialsSecrets; + }; + connectorTokenClient?: ConnectorTokenClientContract; +} + +export const getOAuthClientCredentialsAccessToken = async ({ + connectorId, + logger, + tokenUrl, + oAuthScope, + configurationUtilities, + credentials, + connectorTokenClient, +}: GetOAuthClientCredentialsAccessTokenOpts) => { + const { clientId, tenantId } = credentials.config; + const { clientSecret } = credentials.secrets; + + if (!clientId || !clientSecret || !tenantId) { + logger.warn(`Missing required fields for requesting OAuth Client Credentials access token`); + return null; + } + + let accessToken: string; + let connectorToken: ConnectorToken | null = null; + let hasErrors: boolean = false; + + if (connectorId && connectorTokenClient) { + // Check if there is a token stored for this connector + const { connectorToken: token, hasErrors: errors } = await connectorTokenClient.get({ + connectorId, + }); + connectorToken = token; + hasErrors = errors; + } + + if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) { + // request access token with jwt assertion + const tokenResult = await requestOAuthClientCredentialsToken( + tokenUrl, + logger, + { + scope: oAuthScope, + clientId, + clientSecret, + }, + configurationUtilities + ); + accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`; + + // try to update connector_token SO + if (connectorId && connectorTokenClient) { + try { + await connectorTokenClient.updateOrReplace({ + connectorId, + token: connectorToken, + newToken: accessToken, + expiresInSec: tokenResult.expiresIn, + deleteExisting: hasErrors, + }); + } catch (err) { + logger.warn( + `Not able to update connector token for connectorId: ${connectorId} due to error: ${err.message}` + ); + } + } + } else { + // use existing valid token + accessToken = connectorToken.token; + } + return accessToken; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts new file mode 100644 index 00000000000000..b48456ddd2a8c5 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts @@ -0,0 +1,350 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Logger } from '@kbn/core/server'; +import { asyncForEach } from '@kbn/std'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { actionsConfigMock } from '../../actions_config.mock'; +import { connectorTokenClientMock } from './connector_token_client.mock'; +import { getOAuthJwtAccessToken } from './get_oauth_jwt_access_token'; +import { createJWTAssertion } from './create_jwt_assertion'; +import { requestOAuthJWTToken } from './request_oauth_jwt_token'; + +jest.mock('./create_jwt_assertion', () => ({ + createJWTAssertion: jest.fn(), +})); +jest.mock('./request_oauth_jwt_token', () => ({ + requestOAuthJWTToken: jest.fn(), +})); + +const logger = loggingSystemMock.create().get() as jest.Mocked; +const configurationUtilities = actionsConfigMock.create(); +const connectorTokenClient = connectorTokenClientMock.create(); + +describe('getOAuthJwtAccessToken', () => { + const getOAuthJwtAccessTokenOpts = { + connectorId: '123', + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + jwtKeyId: 'jwtKeyId', + userIdentifierValue: 'userIdentifierValue', + }, + secrets: { + clientSecret: 'clientSecret', + privateKey: 'privateKey', + privateKeyPassword: 'privateKeyPassword', + }, + }, + tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do', + connectorTokenClient, + }; + + beforeEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + test('uses stored access token if it exists', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: { + id: '1', + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + createdAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 10000000000).toISOString(), + }, + }); + const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts); + + expect(accessToken).toEqual('testtokenvalue'); + expect(createJWTAssertion as jest.Mock).not.toHaveBeenCalled(); + expect(requestOAuthJWTToken as jest.Mock).not.toHaveBeenCalled(); + }); + + test('creates new assertion if stored access token does not exist', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); + (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts); + + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith( + logger, + 'privateKey', + 'privateKeyPassword', + { + audience: 'clientId', + issuer: 'clientId', + subject: 'userIdentifierValue', + keyId: 'jwtKeyId', + } + ); + expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith( + 'https://dev23432523.service-now.com/oauth_token.do', + { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' }, + logger, + configurationUtilities + ); + expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({ + connectorId: '123', + token: null, + newToken: 'access_token brandnewaccesstoken', + expiresInSec: 1000, + deleteExisting: false, + }); + }); + + test('creates new assertion if stored access token exists but is expired', async () => { + const createdAt = new Date().toISOString(); + const expiresAt = new Date(Date.now() - 100).toISOString(); + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: { + id: '1', + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + createdAt, + expiresAt, + }, + }); + (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); + (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts); + + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith( + logger, + 'privateKey', + 'privateKeyPassword', + { + audience: 'clientId', + issuer: 'clientId', + subject: 'userIdentifierValue', + keyId: 'jwtKeyId', + } + ); + expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith( + 'https://dev23432523.service-now.com/oauth_token.do', + { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' }, + logger, + configurationUtilities + ); + expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({ + connectorId: '123', + token: { + id: '1', + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + createdAt, + expiresAt, + }, + newToken: 'access_token brandnewaccesstoken', + expiresInSec: 1000, + deleteExisting: false, + }); + }); + + test('returns null and logs warning if any required fields are missing', async () => { + await asyncForEach( + ['clientId', 'jwtKeyId', 'userIdentifierValue'], + async (configField: string) => { + const accessToken = await getOAuthJwtAccessToken({ + ...getOAuthJwtAccessTokenOpts, + credentials: { + config: { ...getOAuthJwtAccessTokenOpts.credentials.config, [configField]: null }, + secrets: getOAuthJwtAccessTokenOpts.credentials.secrets, + }, + }); + expect(accessToken).toBeNull(); + expect(logger.warn).toHaveBeenCalledWith( + `Missing required fields for requesting OAuth JWT access token` + ); + } + ); + + await asyncForEach(['clientSecret', 'privateKey'], async (secretsField: string) => { + const accessToken = await getOAuthJwtAccessToken({ + ...getOAuthJwtAccessTokenOpts, + credentials: { + config: getOAuthJwtAccessTokenOpts.credentials.config, + secrets: { ...getOAuthJwtAccessTokenOpts.credentials.secrets, [secretsField]: null }, + }, + }); + expect(accessToken).toBeNull(); + expect(logger.warn).toHaveBeenCalledWith( + `Missing required fields for requesting OAuth JWT access token` + ); + }); + }); + + test('throws error if createJWTAssertion throws error', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (createJWTAssertion as jest.Mock).mockImplementationOnce(() => { + throw new Error('createJWTAssertion error!!'); + }); + + await expect( + getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"createJWTAssertion error!!"`); + }); + + test('throws error if requestOAuthJWTToken throws error', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); + (requestOAuthJWTToken as jest.Mock).mockRejectedValueOnce( + new Error('requestOAuthJWTToken error!!') + ); + + await expect( + getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"requestOAuthJWTToken error!!"`); + }); + + test('logs warning if connectorTokenClient.updateOrReplace throws error', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); + (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + connectorTokenClient.updateOrReplace.mockRejectedValueOnce(new Error('updateOrReplace error')); + + const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts); + + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(logger.warn).toHaveBeenCalledWith( + `Not able to update connector token for connectorId: 123 due to error: updateOrReplace error` + ); + }); + + test('gets access token if connectorId is not provided', async () => { + (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); + (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthJwtAccessToken({ + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + jwtKeyId: 'jwtKeyId', + userIdentifierValue: 'userIdentifierValue', + }, + secrets: { + clientSecret: 'clientSecret', + privateKey: 'privateKey', + privateKeyPassword: 'privateKeyPassword', + }, + }, + tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do', + connectorTokenClient, + }); + + expect(connectorTokenClient.get).not.toHaveBeenCalled(); + expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled(); + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith( + logger, + 'privateKey', + 'privateKeyPassword', + { + audience: 'clientId', + issuer: 'clientId', + subject: 'userIdentifierValue', + keyId: 'jwtKeyId', + } + ); + expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith( + 'https://dev23432523.service-now.com/oauth_token.do', + { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' }, + logger, + configurationUtilities + ); + }); + + test('gets access token if connectorTokenClient is not provided', async () => { + (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); + (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthJwtAccessToken({ + connectorId: '123', + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + jwtKeyId: 'jwtKeyId', + userIdentifierValue: 'userIdentifierValue', + }, + secrets: { + clientSecret: 'clientSecret', + privateKey: 'privateKey', + privateKeyPassword: 'privateKeyPassword', + }, + }, + tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do', + }); + + expect(connectorTokenClient.get).not.toHaveBeenCalled(); + expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled(); + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith( + logger, + 'privateKey', + 'privateKeyPassword', + { + audience: 'clientId', + issuer: 'clientId', + subject: 'userIdentifierValue', + keyId: 'jwtKeyId', + } + ); + expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith( + 'https://dev23432523.service-now.com/oauth_token.do', + { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' }, + logger, + configurationUtilities + ); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts new file mode 100644 index 00000000000000..a4867d99556e7f --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Logger } from '@kbn/core/server'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ConnectorToken, ConnectorTokenClientContract } from '../../types'; +import { createJWTAssertion } from './create_jwt_assertion'; +import { requestOAuthJWTToken } from './request_oauth_jwt_token'; + +export interface GetOAuthJwtConfig { + clientId: string; + jwtKeyId: string; + userIdentifierValue: string; +} + +export interface GetOAuthJwtSecrets { + clientSecret: string; + privateKey: string; + privateKeyPassword: string | null; +} + +interface GetOAuthJwtAccessTokenOpts { + connectorId?: string; + tokenUrl: string; + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; + credentials: { + config: GetOAuthJwtConfig; + secrets: GetOAuthJwtSecrets; + }; + connectorTokenClient?: ConnectorTokenClientContract; +} + +export const getOAuthJwtAccessToken = async ({ + connectorId, + logger, + tokenUrl, + configurationUtilities, + credentials, + connectorTokenClient, +}: GetOAuthJwtAccessTokenOpts) => { + const { clientId, jwtKeyId, userIdentifierValue } = credentials.config; + const { clientSecret, privateKey, privateKeyPassword } = credentials.secrets; + + if (!clientId || !clientSecret || !jwtKeyId || !privateKey || !userIdentifierValue) { + logger.warn(`Missing required fields for requesting OAuth JWT access token`); + return null; + } + + let accessToken: string; + let connectorToken: ConnectorToken | null = null; + let hasErrors: boolean = false; + + if (connectorId && connectorTokenClient) { + // Check if there is a token stored for this connector + const { connectorToken: token, hasErrors: errors } = await connectorTokenClient.get({ + connectorId, + }); + connectorToken = token; + hasErrors = errors; + } + + if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) { + // generate a new assertion + const assertion = createJWTAssertion(logger, privateKey, privateKeyPassword, { + audience: clientId, + issuer: clientId, + subject: userIdentifierValue, + keyId: jwtKeyId, + }); + + // request access token with jwt assertion + const tokenResult = await requestOAuthJWTToken( + tokenUrl, + { + clientId, + clientSecret, + assertion, + }, + logger, + configurationUtilities + ); + accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`; + + // try to update connector_token SO + if (connectorId && connectorTokenClient) { + try { + await connectorTokenClient.updateOrReplace({ + connectorId, + token: connectorToken, + newToken: accessToken, + expiresInSec: tokenResult.expiresIn, + deleteExisting: hasErrors, + }); + } catch (err) { + logger.warn( + `Not able to update connector token for connectorId: ${connectorId} due to error: ${err.message}` + ); + } + } + } else { + // use existing valid token + accessToken = connectorToken.token; + } + return accessToken; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index 1d1c2c46cb0e49..fbf0d905416592 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -12,8 +12,8 @@ jest.mock('nodemailer', () => ({ jest.mock('./send_email_graph_api', () => ({ sendEmailGraphApi: jest.fn(), })); -jest.mock('./request_oauth_client_credentials_token', () => ({ - requestOAuthClientCredentialsToken: jest.fn(), +jest.mock('./get_oauth_client_credentials_access_token', () => ({ + getOAuthClientCredentialsAccessToken: jest.fn(), })); import { Logger } from '@kbn/core/server'; @@ -24,10 +24,9 @@ import { ProxySettings } from '../../types'; import { actionsConfigMock } from '../../actions_config.mock'; import { CustomHostSettings } from '../../config'; import { sendEmailGraphApi } from './send_email_graph_api'; -import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token'; +import { getOAuthClientCredentialsAccessToken } from './get_oauth_client_credentials_access_token'; import { ConnectorTokenClient } from './connector_token_client'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; -import { connectorTokenClientMock } from './connector_token_client.mock'; const createTransportMock = nodemailer.createTransport as jest.Mock; const sendMailMockResult = { result: 'does not matter' }; @@ -92,314 +91,38 @@ describe('send_email module', () => { test('uses OAuth 2.0 Client Credentials authentication for email using "exchange_server" service', async () => { const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock; - const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock; + const getOAuthClientCredentialsAccessTokenMock = + getOAuthClientCredentialsAccessToken as jest.Mock; const sendEmailOptions = getSendEmailOptions({ transport: { service: 'exchange_server', clientId: '123456', + tenantId: '98765', clientSecret: 'sdfhkdsjhfksdjfh', }, }); - requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({ - tokenType: 'Bearer', - accessToken: 'dfjsdfgdjhfgsjdf', - expiresIn: 123, - }); + getOAuthClientCredentialsAccessTokenMock.mockReturnValueOnce(`Bearer dfjsdfgdjhfgsjdf`); const date = new Date(); date.setDate(date.getDate() + 5); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'connector_token', - references: [], - attributes: { - connectorId: '123', - expiresAt: date.toISOString(), - tokenType: 'access_token', - token: '11111111', - }, - }); - sendEmailGraphApiMock.mockReturnValue({ status: 202, }); - unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ - total: 0, - saved_objects: [], - per_page: 500, - page: 1, - }); await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); - requestOAuthClientCredentialsTokenMock.mock.calls[0].pop(); - expect(requestOAuthClientCredentialsTokenMock.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://login.microsoftonline.com/undefined/oauth2/v2.0/token", - Object { - "context": Array [], - "debug": [MockFunction], - "error": [MockFunction], - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - Object { - "clientId": "123456", - "clientSecret": "sdfhkdsjhfksdjfh", - "scope": "https://graph.microsoft.com/.default", - }, - ] - `); - - delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; - sendEmailGraphApiMock.mock.calls[0].pop(); - expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "graphApiUrl": undefined, - "headers": Object { - "Authorization": "Bearer dfjsdfgdjhfgsjdf", - "Content-Type": "application/json", - }, - "messageHTML": "

a message

- ", - "options": Object { - "connectorId": "1", - "content": Object { - "message": "a message", - "subject": "a subject", - }, - "hasAuth": true, - "routing": Object { - "bcc": Array [], - "cc": Array [ - "bob@example.com", - "robert@example.com", - ], - "from": "fred@example.com", - "to": Array [ - "jim@example.com", - ], - }, - "transport": Object { - "clientId": "123456", - "clientSecret": "sdfhkdsjhfksdjfh", - "password": "changeme", - "service": "exchange_server", - "user": "elastic", - }, - }, - }, - Object { - "context": Array [], - "debug": [MockFunction], - "error": [MockFunction], - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - ] - `); - - expect(unsecuredSavedObjectsClient.create.mock.calls.length).toBe(1); - }); - - test('uses existing "access_token" from "connector_token" SO for authentication for email using "exchange_server" service', async () => { - const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock; - const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock; - const sendEmailOptions = getSendEmailOptions({ - transport: { - service: 'exchange_server', - clientId: '123456', - clientSecret: 'sdfhkdsjhfksdjfh', - }, - }); - requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({ - tokenType: 'Bearer', - accessToken: 'dfjsdfgdjhfgsjdf', - expiresIn: 123, - }); - - sendEmailGraphApiMock.mockReturnValue({ - status: 202, - }); - const date = new Date(); - date.setDate(date.getDate() + 5); - - unsecuredSavedObjectsClient.checkConflicts.mockResolvedValueOnce({ - errors: [], - }); - unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '1', - score: 1, - type: 'connector_token', - references: [], - attributes: { - connectorId: '123', - expiresAt: date.toISOString(), - tokenType: 'access_token', - }, - }, - ], - per_page: 500, - page: 1, - }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'connector_token', - references: [], - attributes: { - token: '11111111', + expect(getOAuthClientCredentialsAccessTokenMock).toHaveBeenCalledWith({ + configurationUtilities: sendEmailOptions.configurationUtilities, + connectorId: '1', + connectorTokenClient, + credentials: { + config: { clientId: '123456', tenantId: '98765' }, + secrets: { clientSecret: 'sdfhkdsjhfksdjfh' }, }, + logger: mockLogger, + oAuthScope: 'https://graph.microsoft.com/.default', + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', }); - await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); - expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(0); - - delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; - sendEmailGraphApiMock.mock.calls[0].pop(); - expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "graphApiUrl": undefined, - "headers": Object { - "Authorization": "11111111", - "Content-Type": "application/json", - }, - "messageHTML": "

a message

- ", - "options": Object { - "connectorId": "1", - "content": Object { - "message": "a message", - "subject": "a subject", - }, - "hasAuth": true, - "routing": Object { - "bcc": Array [], - "cc": Array [ - "bob@example.com", - "robert@example.com", - ], - "from": "fred@example.com", - "to": Array [ - "jim@example.com", - ], - }, - "transport": Object { - "clientId": "123456", - "clientSecret": "sdfhkdsjhfksdjfh", - "password": "changeme", - "service": "exchange_server", - "user": "elastic", - }, - }, - }, - Object { - "context": Array [], - "debug": [MockFunction], - "error": [MockFunction], - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - ] - `); - - expect(unsecuredSavedObjectsClient.create.mock.calls.length).toBe(0); - }); - - test('request the new token and update existing "access_token" when it is expired for "exchange_server" email service', async () => { - const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock; - const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock; - const sendEmailOptions = getSendEmailOptions({ - transport: { - service: 'exchange_server', - clientId: '123456', - clientSecret: 'sdfhkdsjhfksdjfh', - }, - }); - requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({ - tokenType: 'Bearer', - accessToken: 'dfjsdfgdjhfgsjdf', - expiresIn: 123, - }); - - sendEmailGraphApiMock.mockReturnValue({ - status: 202, - }); - const date = new Date(); - date.setDate(date.getDate() - 5); - - unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '1', - score: 1, - type: 'connector_token', - references: [], - attributes: { - connectorId: '123', - expiresAt: date.toISOString(), - tokenType: 'access_token', - }, - }, - ], - per_page: 500, - page: 1, - }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'connector_token', - references: [], - attributes: { - token: '11111111', - }, - }); - - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'connector_token', - references: [], - attributes: { - connectorId: '123', - expiresAt: date.toISOString(), - tokenType: 'access_token', - token: '11111111', - }, - }); - unsecuredSavedObjectsClient.checkConflicts.mockResolvedValueOnce({ - errors: [], - }); - - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'connector_token', - references: [], - attributes: { - connectorId: '123', - expiresAt: date.toISOString(), - tokenType: 'access_token', - token: '11111111', - }, - }); - - await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); - expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1); - delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; sendEmailGraphApiMock.mock.calls[0].pop(); expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` @@ -435,6 +158,7 @@ describe('send_email module', () => { "clientSecret": "sdfhkdsjhfksdjfh", "password": "changeme", "service": "exchange_server", + "tenantId": "98765", "user": "elastic", }, }, @@ -452,209 +176,42 @@ describe('send_email module', () => { }, ] `); - - expect(unsecuredSavedObjectsClient.create.mock.calls.length).toBe(1); }); - test('sending email for "exchange_server" wont fail if connectorTokenClient throw the errors, just log warning message', async () => { + test('throws error if null access token returned when using OAuth 2.0 Client Credentials authentication', async () => { const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock; - const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock; + const getOAuthClientCredentialsAccessTokenMock = + getOAuthClientCredentialsAccessToken as jest.Mock; const sendEmailOptions = getSendEmailOptions({ transport: { service: 'exchange_server', clientId: '123456', + tenantId: '98765', clientSecret: 'sdfhkdsjhfksdjfh', }, }); - requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({ - tokenType: 'Bearer', - accessToken: 'dfjsdfgdjhfgsjdf', - expiresIn: 123, - }); - - sendEmailGraphApiMock.mockReturnValue({ - status: 202, - }); - const date = new Date(); - date.setDate(date.getDate() + 5); - - unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ - total: 0, - saved_objects: [], - per_page: 500, - page: 1, - }); - unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); - - await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); - expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1); - expect(unsecuredSavedObjectsClient.create.mock.calls.length).toBe(1); - expect(mockLogger.warn.mock.calls[0]).toMatchObject([ - `Not able to update connector token for connectorId: 1 due to error: Fail`, - ]); + getOAuthClientCredentialsAccessTokenMock.mockReturnValueOnce(null); - delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; - sendEmailGraphApiMock.mock.calls[0].pop(); - expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "graphApiUrl": undefined, - "headers": Object { - "Authorization": "Bearer dfjsdfgdjhfgsjdf", - "Content-Type": "application/json", - }, - "messageHTML": "

a message

- ", - "options": Object { - "connectorId": "1", - "content": Object { - "message": "a message", - "subject": "a subject", - }, - "hasAuth": true, - "routing": Object { - "bcc": Array [], - "cc": Array [ - "bob@example.com", - "robert@example.com", - ], - "from": "fred@example.com", - "to": Array [ - "jim@example.com", - ], - }, - "transport": Object { - "clientId": "123456", - "clientSecret": "sdfhkdsjhfksdjfh", - "password": "changeme", - "service": "exchange_server", - "user": "elastic", - }, - }, - }, - Object { - "context": Array [], - "debug": [MockFunction], - "error": [MockFunction] { - "calls": Array [ - Array [ - "Failed to create connector_token for connectorId \\"1\\" and tokenType: \\"access_token\\". Error: Fail", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction] { - "calls": Array [ - Array [ - "Not able to update connector token for connectorId: 1 due to error: Fail", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - }, - ] - `); - }); + await expect(() => + sendEmail(mockLogger, sendEmailOptions, connectorTokenClient) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to retrieve access token for connectorId: 1"` + ); - test('delete duplication tokens if connectorTokenClient get method has the errors, like decription error', async () => { - const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock; - const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock; - const sendEmailOptions = getSendEmailOptions({ - transport: { - service: 'exchange_server', - clientId: '123456', - clientSecret: 'sdfhkdsjhfksdjfh', + expect(getOAuthClientCredentialsAccessTokenMock).toHaveBeenCalledWith({ + configurationUtilities: sendEmailOptions.configurationUtilities, + connectorId: '1', + connectorTokenClient, + credentials: { + config: { clientId: '123456', tenantId: '98765' }, + secrets: { clientSecret: 'sdfhkdsjhfksdjfh' }, }, + logger: mockLogger, + oAuthScope: 'https://graph.microsoft.com/.default', + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', }); - requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({ - tokenType: 'Bearer', - accessToken: 'dfjsdfgdjhfgsjdf', - expiresIn: 123, - }); - - sendEmailGraphApiMock.mockReturnValue({ - status: 202, - }); - const date = new Date(); - date.setDate(date.getDate() + 5); - - const connectorTokenClientM = connectorTokenClientMock.create(); - connectorTokenClientM.get.mockResolvedValueOnce({ - hasErrors: true, - connectorToken: null, - }); - - await sendEmail(mockLogger, sendEmailOptions, connectorTokenClientM); - expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1); - expect(connectorTokenClientM.updateOrReplace.mock.calls.length).toBe(1); - delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; - sendEmailGraphApiMock.mock.calls[0].pop(); - expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "graphApiUrl": undefined, - "headers": Object { - "Authorization": "Bearer dfjsdfgdjhfgsjdf", - "Content-Type": "application/json", - }, - "messageHTML": "

a message

- ", - "options": Object { - "connectorId": "1", - "content": Object { - "message": "a message", - "subject": "a subject", - }, - "hasAuth": true, - "routing": Object { - "bcc": Array [], - "cc": Array [ - "bob@example.com", - "robert@example.com", - ], - "from": "fred@example.com", - "to": Array [ - "jim@example.com", - ], - }, - "transport": Object { - "clientId": "123456", - "clientSecret": "sdfhkdsjhfksdjfh", - "password": "changeme", - "service": "exchange_server", - "user": "elastic", - }, - }, - }, - Object { - "context": Array [], - "debug": [MockFunction], - "error": [MockFunction], - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - ] - `); + expect(sendEmailGraphApiMock).not.toHaveBeenCalled(); }); test('handles unauthenticated email using not secure host/port', async () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index 983846adc71e0c..f2b059e51e0d6b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -14,9 +14,9 @@ import { ActionsConfigurationUtilities } from '../../actions_config'; import { CustomHostSettings } from '../../config'; import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options'; import { sendEmailGraphApi } from './send_email_graph_api'; -import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token'; import { ConnectorTokenClientContract, ProxySettings } from '../../types'; import { AdditionalEmailServices } from '../../../common'; +import { getOAuthClientCredentialsAccessToken } from './get_oauth_client_credentials_access_token'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; @@ -86,41 +86,28 @@ async function sendEmailWithExchange( const { transport, configurationUtilities, connectorId } = options; const { clientId, clientSecret, tenantId, oauthTokenUrl } = transport; - let accessToken: string; - - const { connectorToken, hasErrors } = await connectorTokenClient.get({ connectorId }); - if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) { - // request new access token for microsoft exchange online server with Graph API scope - const tokenResult = await requestOAuthClientCredentialsToken( - oauthTokenUrl ?? `${EXCHANGE_ONLINE_SERVER_HOST}/${tenantId}/oauth2/v2.0/token`, - logger, - { - scope: GRAPH_API_OAUTH_SCOPE, - clientId, - clientSecret, + const accessToken = await getOAuthClientCredentialsAccessToken({ + connectorId, + logger, + configurationUtilities, + credentials: { + config: { + clientId: clientId as string, + tenantId: tenantId as string, + }, + secrets: { + clientSecret: clientSecret as string, }, - configurationUtilities - ); - accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`; + }, + oAuthScope: GRAPH_API_OAUTH_SCOPE, + tokenUrl: oauthTokenUrl ?? `${EXCHANGE_ONLINE_SERVER_HOST}/${tenantId}/oauth2/v2.0/token`, + connectorTokenClient, + }); - // try to update connector_token SO - try { - await connectorTokenClient.updateOrReplace({ - connectorId, - token: connectorToken, - newToken: accessToken, - expiresInSec: tokenResult.expiresIn, - deleteExisting: hasErrors, - }); - } catch (err) { - logger.warn( - `Not able to update connector token for connectorId: ${connectorId} due to error: ${err.message}` - ); - } - } else { - // use existing valid token - accessToken = connectorToken.token; + if (!accessToken) { + throw new Error(`Unable to retrieve access token for connectorId: ${connectorId}`); } + const headers = { 'Content-Type': 'application/json', Authorization: accessToken, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts index dae4e59728a0ca..64a80977709e5d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts @@ -14,19 +14,14 @@ import { createServiceError, getPushedDate, throwIfSubActionIsNotSupported, - getAccessToken, getAxiosInstance, } from './utils'; import { connectorTokenClientMock } from '../lib/connector_token_client.mock'; import { actionsConfigMock } from '../../actions_config.mock'; -import { createJWTAssertion } from '../lib/create_jwt_assertion'; -import { requestOAuthJWTToken } from '../lib/request_oauth_jwt_token'; +import { getOAuthJwtAccessToken } from '../lib/get_oauth_jwt_access_token'; -jest.mock('../lib/create_jwt_assertion', () => ({ - createJWTAssertion: jest.fn(), -})); -jest.mock('../lib/request_oauth_jwt_token', () => ({ - requestOAuthJWTToken: jest.fn(), +jest.mock('../lib/get_oauth_jwt_access_token', () => ({ + getOAuthJwtAccessToken: jest.fn(), })); jest.mock('axios', () => ({ @@ -195,7 +190,7 @@ describe('utils', () => { }); }); - test('creates axios instance with interceptor when isOAuth is true and OAuth fields are defined', () => { + test('creates axios instance with interceptor when isOAuth is true and OAuth fields are defined', async () => { connectorTokenClient.get.mockResolvedValueOnce({ hasErrors: false, connectorToken: { @@ -235,206 +230,34 @@ describe('utils', () => { expect(createAxiosInstanceMock).toHaveBeenCalledTimes(1); expect(createAxiosInstanceMock).toHaveBeenCalledWith(); expect(axiosInstanceMock.interceptors.request.use).toHaveBeenCalledTimes(1); - }); - }); - describe('getAccessToken', () => { - const getAccessTokenOpts = { - connectorId: '123', - logger, - configurationUtilities, - credentials: { - config: { - apiUrl: 'https://servicenow', - usesTableApi: true, - isOAuth: true, - clientId: 'clientId', - jwtKeyId: 'jwtKeyId', - userIdentifierValue: 'userIdentifierValue', - }, - secrets: { - clientSecret: 'clientSecret', - privateKey: 'privateKey', - privateKeyPassword: 'privateKeyPassword', - username: null, - password: null, - }, - }, - snServiceUrl: 'https://dev23432523.service-now.com', - connectorTokenClient, - }; - beforeEach(() => { - jest.resetAllMocks(); - jest.clearAllMocks(); - }); + (getOAuthJwtAccessToken as jest.Mock).mockResolvedValueOnce('Bearer tokentokentoken'); - test('uses stored access token if it exists', async () => { - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: { - id: '1', - connectorId: '123', - tokenType: 'access_token', - token: 'testtokenvalue', - createdAt: new Date().toISOString(), - expiresAt: new Date(Date.now() + 10000000000).toISOString(), - }, - }); - const accessToken = await getAccessToken(getAccessTokenOpts); - - expect(accessToken).toEqual('testtokenvalue'); - expect(createJWTAssertion as jest.Mock).not.toHaveBeenCalled(); - expect(requestOAuthJWTToken as jest.Mock).not.toHaveBeenCalled(); - }); - - test('creates new assertion if stored access token does not exist', async () => { - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: null, - }); - (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); - (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ - tokenType: 'access_token', - accessToken: 'brandnewaccesstoken', - expiresIn: 1000, + const mockRequestCallback = (axiosInstanceMock.interceptors.request.use as jest.Mock).mock + .calls[0][0]; + expect(await mockRequestCallback({ headers: {} })).toEqual({ + headers: { Authorization: 'Bearer tokentokentoken' }, }); - const accessToken = await getAccessToken(getAccessTokenOpts); - - expect(accessToken).toEqual('access_token brandnewaccesstoken'); - expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith( - logger, - 'privateKey', - 'privateKeyPassword', - { - audience: 'clientId', - issuer: 'clientId', - subject: 'userIdentifierValue', - keyId: 'jwtKeyId', - } - ); - expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith( - 'https://dev23432523.service-now.com/oauth_token.do', - { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' }, - logger, - configurationUtilities - ); - expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({ + expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalledWith({ connectorId: '123', - token: null, - newToken: 'access_token brandnewaccesstoken', - expiresInSec: 1000, - deleteExisting: false, - }); - }); - - test('creates new assertion if stored access token exists but is expired', async () => { - const createdAt = new Date().toISOString(); - const expiresAt = new Date(Date.now() - 100).toISOString(); - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: { - id: '1', - connectorId: '123', - tokenType: 'access_token', - token: 'testtokenvalue', - createdAt, - expiresAt, - }, - }); - (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); - (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ - tokenType: 'access_token', - accessToken: 'brandnewaccesstoken', - expiresIn: 1000, - }); - - const accessToken = await getAccessToken(getAccessTokenOpts); - - expect(accessToken).toEqual('access_token brandnewaccesstoken'); - expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith( - logger, - 'privateKey', - 'privateKeyPassword', - { - audience: 'clientId', - issuer: 'clientId', - subject: 'userIdentifierValue', - keyId: 'jwtKeyId', - } - ); - expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith( - 'https://dev23432523.service-now.com/oauth_token.do', - { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' }, logger, - configurationUtilities - ); - expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({ - connectorId: '123', - token: { - id: '1', - connectorId: '123', - tokenType: 'access_token', - token: 'testtokenvalue', - createdAt, - expiresAt, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + jwtKeyId: 'jwtKeyId', + userIdentifierValue: 'userIdentifierValue', + }, + secrets: { + clientSecret: 'clientSecret', + privateKey: 'privateKey', + privateKeyPassword: null, + }, }, - newToken: 'access_token brandnewaccesstoken', - expiresInSec: 1000, - deleteExisting: false, - }); - }); - - test('throws error if createJWTAssertion throws error', async () => { - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: null, - }); - (createJWTAssertion as jest.Mock).mockImplementationOnce(() => { - throw new Error('createJWTAssertion error!!'); - }); - - await expect(getAccessToken(getAccessTokenOpts)).rejects.toThrowErrorMatchingInlineSnapshot( - `"createJWTAssertion error!!"` - ); - }); - - test('throws error if requestOAuthJWTToken throws error', async () => { - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: null, - }); - (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); - (requestOAuthJWTToken as jest.Mock).mockRejectedValueOnce( - new Error('requestOAuthJWTToken error!!') - ); - - await expect(getAccessToken(getAccessTokenOpts)).rejects.toThrowErrorMatchingInlineSnapshot( - `"requestOAuthJWTToken error!!"` - ); - }); - - test('logs warning if connectorTokenClient.updateOrReplace throws error', async () => { - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: null, - }); - (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); - (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ - tokenType: 'access_token', - accessToken: 'brandnewaccesstoken', - expiresIn: 1000, + tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do', + connectorTokenClient, }); - connectorTokenClient.updateOrReplace.mockRejectedValueOnce( - new Error('updateOrReplace error') - ); - - const accessToken = await getAccessToken(getAccessTokenOpts); - - expect(accessToken).toEqual('access_token brandnewaccesstoken'); - expect(logger.warn).toHaveBeenCalledWith( - `Not able to update ServiceNow connector token for connectorId: 123 due to error: updateOrReplace error` - ); }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts index 84d6741398bceb..538967269b1eae 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts @@ -21,8 +21,7 @@ import { addTimeZoneToDate, getErrorMessage } from '../lib/axios_utils'; import * as i18n from './translations'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { ConnectorTokenClientContract } from '../../types'; -import { createJWTAssertion } from '../lib/create_jwt_assertion'; -import { requestOAuthJWTToken } from '../lib/request_oauth_jwt_token'; +import { getOAuthJwtAccessToken } from '../lib/get_oauth_jwt_access_token'; export const prepareIncident = (useOldApi: boolean, incident: PartialIncident): PartialIncident => useOldApi @@ -83,13 +82,13 @@ export const throwIfSubActionIsNotSupported = ({ } }; -export interface GetAccessTokenAndAxiosInstanceOpts { - connectorId: string; +export interface GetAxiosInstanceOpts { + connectorId?: string; logger: Logger; configurationUtilities: ActionsConfigurationUtilities; credentials: ExternalServiceCredentials; snServiceUrl: string; - connectorTokenClient: ConnectorTokenClientContract; + connectorTokenClient?: ConnectorTokenClientContract; } export const getAxiosInstance = ({ @@ -99,7 +98,7 @@ export const getAxiosInstance = ({ credentials, snServiceUrl, connectorTokenClient, -}: GetAccessTokenAndAxiosInstanceOpts): AxiosInstance => { +}: GetAxiosInstanceOpts): AxiosInstance => { const { config, secrets } = credentials; const { isOAuth } = config as ServiceNowPublicConfigurationType; const { username, password } = secrets as ServiceNowSecretConfigurationType; @@ -114,15 +113,25 @@ export const getAxiosInstance = ({ axiosInstance = axios.create(); axiosInstance.interceptors.request.use( async (axiosConfig: AxiosRequestConfig) => { - const accessToken = await getAccessToken({ + const accessToken = await getOAuthJwtAccessToken({ connectorId, logger, configurationUtilities, credentials: { - config: config as ServiceNowPublicConfigurationType, - secrets, + config: { + clientId: config.clientId as string, + jwtKeyId: config.jwtKeyId as string, + userIdentifierValue: config.userIdentifierValue as string, + }, + secrets: { + clientSecret: secrets.clientSecret as string, + privateKey: secrets.privateKey as string, + privateKeyPassword: secrets.privateKeyPassword + ? (secrets.privateKeyPassword as string) + : null, + }, }, - snServiceUrl, + tokenUrl: `${snServiceUrl}/oauth_token.do`, connectorTokenClient, }); axiosConfig.headers.Authorization = accessToken; @@ -136,75 +145,3 @@ export const getAxiosInstance = ({ return axiosInstance; }; - -export const getAccessToken = async ({ - connectorId, - logger, - configurationUtilities, - credentials, - snServiceUrl, - connectorTokenClient, -}: GetAccessTokenAndAxiosInstanceOpts) => { - const { isOAuth, clientId, jwtKeyId, userIdentifierValue } = - credentials.config as ServiceNowPublicConfigurationType; - const { clientSecret, privateKey, privateKeyPassword } = - credentials.secrets as ServiceNowSecretConfigurationType; - - let accessToken: string; - - // Check if there is a token stored for this connector - const { connectorToken, hasErrors } = await connectorTokenClient.get({ connectorId }); - - if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) { - // generate a new assertion - if ( - !isOAuth || - !clientId || - !clientSecret || - !jwtKeyId || - !privateKey || - !userIdentifierValue - ) { - return null; - } - - const assertion = createJWTAssertion(logger, privateKey, privateKeyPassword, { - audience: clientId, - issuer: clientId, - subject: userIdentifierValue, - keyId: jwtKeyId, - }); - - // request access token with jwt assertion - const tokenResult = await requestOAuthJWTToken( - `${snServiceUrl}/oauth_token.do`, - { - clientId, - clientSecret, - assertion, - }, - logger, - configurationUtilities - ); - accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`; - - // try to update connector_token SO - try { - await connectorTokenClient.updateOrReplace({ - connectorId, - token: connectorToken, - newToken: accessToken, - expiresInSec: tokenResult.expiresIn, - deleteExisting: hasErrors, - }); - } catch (err) { - logger.warn( - `Not able to update ServiceNow connector token for connectorId: ${connectorId} due to error: ${err.message}` - ); - } - } else { - // use existing valid token - accessToken = connectorToken.token; - } - return accessToken; -}; diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index d89a3c96b01b94..3f3895ec5b69f7 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -109,7 +109,7 @@ describe('Actions Plugin', () => { httpServerMock.createKibanaRequest(), httpServerMock.createResponseFactory() )) as unknown as ActionsApiRequestHandlerContext; - actionsContextHandler!.getActionsClient(); + expect(actionsContextHandler!.getActionsClient()).toBeDefined(); }); it('should throw error when ESO plugin is missing encryption key', async () => { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 1fad2a6189693c..c097b94a859503 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -311,12 +311,13 @@ export class ActionsPlugin implements Plugin(), - this.licenseState, + defineRoutes({ + router: core.http.createRouter(), + licenseState: this.licenseState, + logger: this.logger, actionsConfigUtils, - this.usageCounter - ); + usageCounter: this.usageCounter, + }); // Cleanup failed execution task definition if (this.actionsConfig.cleanupFailedExecutionsTask.enabled) { diff --git a/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts b/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts new file mode 100644 index 00000000000000..888e87dbdf1f4f --- /dev/null +++ b/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts @@ -0,0 +1,227 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOAuthAccessToken } from './get_oauth_access_token'; +import { Logger } from '@kbn/core/server'; +import { httpServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { actionsConfigMock } from '../actions_config.mock'; +import { actionsClientMock } from '../actions_client.mock'; + +jest.mock('./verify_access_and_context', () => ({ + verifyAccessAndContext: jest.fn(), +})); + +const logger = loggingSystemMock.create().get() as jest.Mocked; +const configurationUtilities = actionsConfigMock.create(); + +beforeEach(() => { + jest.resetAllMocks(); + (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); +}); + +describe('getOAuthAccessToken', () => { + it('returns jwt access token for given jwt oauth config', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getOAuthAccessToken(router, licenseState, logger, configurationUtilities); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector/_oauth_access_token"`); + + const actionsClient = actionsClientMock.create(); + actionsClient.getOAuthAccessToken.mockResolvedValueOnce({ + accessToken: 'Bearer jwttokentokentoken', + }); + + const requestBody = { + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }; + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + body: requestBody, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "accessToken": "Bearer jwttokentokentoken", + }, + } + `); + + expect(actionsClient.getOAuthAccessToken).toHaveBeenCalledTimes(1); + expect(actionsClient.getOAuthAccessToken.mock.calls[0]).toEqual([ + requestBody, + logger, + configurationUtilities, + ]); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + accessToken: 'Bearer jwttokentokentoken', + }, + }); + }); + + it('returns client credentials access token for given client credentials oauth config', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getOAuthAccessToken(router, licenseState, logger, configurationUtilities); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector/_oauth_access_token"`); + + const actionsClient = actionsClientMock.create(); + actionsClient.getOAuthAccessToken.mockResolvedValueOnce({ + accessToken: 'Bearer clienttokentokentoken', + }); + + const requestBody = { + type: 'client', + options: { + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + scope: 'https://graph.microsoft.com/.default', + config: { + clientId: 'abc', + tenantId: 'def', + }, + secrets: { + clientSecret: 'iamasecret', + }, + }, + }; + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + body: requestBody, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "accessToken": "Bearer clienttokentokentoken", + }, + } + `); + + expect(actionsClient.getOAuthAccessToken).toHaveBeenCalledTimes(1); + expect(actionsClient.getOAuthAccessToken.mock.calls[0]).toEqual([ + requestBody, + logger, + configurationUtilities, + ]); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + accessToken: 'Bearer clienttokentokentoken', + }, + }); + }); + + it('ensures the license allows getting servicenow access token', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getOAuthAccessToken(router, licenseState, logger, configurationUtilities); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector/_oauth_access_token"`); + + const [context, req, res] = mockHandlerArguments( + {}, + { + body: { + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); + }); + + it('ensures the license check prevents getting service now access token', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => { + throw new Error('OMG'); + }); + + getOAuthAccessToken(router, licenseState, logger, configurationUtilities); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector/_oauth_access_token"`); + + const [context, req, res] = mockHandlerArguments( + {}, + { + body: { + type: 'client', + options: { + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + scope: 'https://graph.microsoft.com/.default', + config: { + clientId: 'abc', + tenantId: 'def', + }, + secrets: { + clientSecret: 'iamasecret', + }, + }, + }, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/get_oauth_access_token.ts b/x-pack/plugins/actions/server/routes/get_oauth_access_token.ts new file mode 100644 index 00000000000000..e1b612d321bcd9 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/get_oauth_access_token.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { IRouter, Logger } from '@kbn/core/server'; +import { ILicenseState } from '../lib'; +import { INTERNAL_BASE_ACTION_API_PATH } from '../../common'; +import { ActionsRequestHandlerContext } from '../types'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { ActionsConfigurationUtilities } from '../actions_config'; + +const oauthJwtBodySchema = schema.object({ + tokenUrl: schema.string(), + config: schema.object({ + clientId: schema.string(), + jwtKeyId: schema.string(), + userIdentifierValue: schema.string(), + }), + secrets: schema.object({ + clientSecret: schema.string(), + privateKey: schema.string(), + privateKeyPassword: schema.maybe(schema.string()), + }), +}); + +export type OAuthJwtParams = TypeOf; + +const oauthClientCredentialsBodySchema = schema.object({ + tokenUrl: schema.string(), + scope: schema.string(), + config: schema.object({ + clientId: schema.string(), + tenantId: schema.string(), + }), + secrets: schema.object({ + clientSecret: schema.string(), + }), +}); + +export type OAuthClientCredentialsParams = TypeOf; + +const bodySchema = schema.object({ + type: schema.oneOf([schema.literal('jwt'), schema.literal('client')]), + options: schema.conditional( + schema.siblingRef('type'), + schema.literal('jwt'), + oauthJwtBodySchema, + oauthClientCredentialsBodySchema + ), +}); + +export type OAuthParams = TypeOf; + +export const getOAuthAccessToken = ( + router: IRouter, + licenseState: ILicenseState, + logger: Logger, + configurationUtilities: ActionsConfigurationUtilities +) => { + router.post( + { + path: `${INTERNAL_BASE_ACTION_API_PATH}/connector/_oauth_access_token`, + validate: { + body: bodySchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const actionsClient = (await context.actions).getActionsClient(); + + return res.ok({ + body: await actionsClient.getOAuthAccessToken(req.body, logger, configurationUtilities), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts index ab90141ae1c80f..2822aa36689000 100644 --- a/x-pack/plugins/actions/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IRouter } from '@kbn/core/server'; +import { IRouter, Logger } from '@kbn/core/server'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { ILicenseState } from '../lib'; import { ActionsRequestHandlerContext } from '../types'; @@ -17,15 +17,21 @@ import { getAllActionRoute } from './get_all'; import { connectorTypesRoute } from './connector_types'; import { updateActionRoute } from './update'; import { getWellKnownEmailServiceRoute } from './get_well_known_email_service'; +import { getOAuthAccessToken } from './get_oauth_access_token'; import { defineLegacyRoutes } from './legacy'; import { ActionsConfigurationUtilities } from '../actions_config'; -export function defineRoutes( - router: IRouter, - licenseState: ILicenseState, - actionsConfigUtils: ActionsConfigurationUtilities, - usageCounter?: UsageCounter -) { +export interface RouteOptions { + router: IRouter; + licenseState: ILicenseState; + logger: Logger; + actionsConfigUtils: ActionsConfigurationUtilities; + usageCounter?: UsageCounter; +} + +export function defineRoutes(opts: RouteOptions) { + const { router, licenseState, logger, actionsConfigUtils, usageCounter } = opts; + defineLegacyRoutes(router, licenseState, usageCounter); createActionRoute(router, licenseState); @@ -36,5 +42,6 @@ export function defineRoutes( connectorTypesRoute(router, licenseState); executeActionRoute(router, licenseState); + getOAuthAccessToken(router, licenseState, logger, actionsConfigUtils); getWellKnownEmailServiceRoute(router, licenseState); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index 4525768a0fb420..82516bf4a417d7 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -15,6 +15,7 @@ import { ActionType } from '@kbn/actions-plugin/server'; import { initPlugin as initPagerduty } from './pagerduty_simulation'; import { initPlugin as initSwimlane } from './swimlane_simulation'; import { initPlugin as initServiceNow } from './servicenow_simulation'; +import { initPlugin as initServiceNowOAuth } from './servicenow_oauth_simulation'; import { initPlugin as initJira } from './jira_simulation'; import { initPlugin as initResilient } from './resilient_simulation'; import { initPlugin as initSlack } from './slack_simulation'; @@ -49,6 +50,7 @@ export function getAllExternalServiceSimulatorPaths(): string[] { allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.RESILIENT}/rest/orgs/201/incidents`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.MS_EXCHANGE}/users/test@/sendMail`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.MS_EXCHANGE}/1234567/oauth2/v2.0/token`); + allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/oauth_token.do`); return allPaths; } @@ -129,6 +131,10 @@ export class FixturePlugin implements Plugin, + res: KibanaResponseFactory + ): Promise> { + return jsonResponse(res, 200, { + access_token: 'tokentokentoken', + expires_in: 3660, + token_type: 'Bearer', + }); + } + ); +} + +function jsonResponse( + res: KibanaResponseFactory, + code: number, + object: Record = {} +) { + return res.custom>({ body: object, statusCode: code }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/oauth_access_token.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/oauth_access_token.ts new file mode 100644 index 00000000000000..6053f78ea76a49 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/oauth_access_token.ts @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import fs from 'fs'; +import expect from '@kbn/expect'; +import { promisify } from 'util'; +import httpProxy from 'http-proxy'; +import { KBN_KEY_PATH } from '@kbn/dev-utils'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default function oAuthAccessTokenTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + + describe('get oauth access token', () => { + let servicenowSimulatorURL: string = ''; + let proxyServer: httpProxy | undefined; + let testPrivateKey: string; + const configService = getService('config'); + + // need to wait for kibanaServer to settle ... + before(async () => { + testPrivateKey = await promisify(fs.readFile)(KBN_KEY_PATH, 'utf8'); + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => {} + ); + }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); + + it('should return 200 when requesting a JWT access token with OAuth credentials', async () => { + const { body: accessToken } = await supertest + .post('/internal/actions/connector/_oauth_access_token') + .set('kbn-xsrf', 'foo') + .send({ + type: 'jwt', + options: { + tokenUrl: `${servicenowSimulatorURL}/oauth_token.do`, + config: { + clientId: 'abc', + userIdentifierValue: 'elastic', + jwtKeyId: 'def', + }, + secrets: { + clientSecret: 'xyz', + privateKey: testPrivateKey, + }, + }, + }) + .expect(200); + + expect(accessToken).to.eql({ accessToken: 'Bearer tokentokentoken' }); + }); + + it('should return 200 when requesting a Client Credentials access token with OAuth credentials', async () => { + const { body: accessToken } = await supertest + .post('/internal/actions/connector/_oauth_access_token') + .set('kbn-xsrf', 'foo') + .send({ + type: 'client', + options: { + tokenUrl: `${kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.MS_EXCHANGE) + )}/1234567/oauth2/v2.0/token`, + scope: 'https://graph.microsoft.com/.default', + config: { + clientId: 'abc', + tenantId: '98765', + }, + secrets: { + clientSecret: 'xyz', + }, + }, + }) + .expect(200); + + expect(accessToken).to.eql({ accessToken: 'Bearer asdadasd' }); + }); + + it('should return 400 when given incorrect options for requesting Client Credentials access token with OAuth credentials', async () => { + await supertest + .post('/internal/actions/connector/_oauth_access_token') + .set('kbn-xsrf', 'foo') + .send({ + type: 'client', + options: { + tokenUrl: `${servicenowSimulatorURL}/oauth_token.do`, + config: { + clientId: 'abc', + userIdentifierValue: 'elastic', + jwtKeyId: 'def', + }, + secrets: { + clientSecret: 'xyz', + privateKey: testPrivateKey, + }, + }, + }) + .expect(400); + }); + + it('should return 400 when given incorrect options for requesting JWT access token with OAuth credentials', async () => { + await supertest + .post('/internal/actions/connector/_oauth_access_token') + .set('kbn-xsrf', 'foo') + .send({ + type: 'jwt', + options: { + tokenUrl: `${kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.MS_EXCHANGE) + )}/1234567/oauth2/v2.0/token`, + scope: 'https://graph.microsoft.com/.default', + config: { + clientId: 'abc', + tenantId: '98765', + }, + secrets: { + clientSecret: 'xyz', + }, + }, + }) + .expect(400); + }); + + it('should return 400 when token url not included in allowlist', async () => { + const { body } = await supertest + .post('/internal/actions/connector/_oauth_access_token') + .set('kbn-xsrf', 'foo') + .send({ + type: 'jwt', + options: { + tokenUrl: `https://servicenow.nonexistent.com/oauth_token.do`, + config: { + clientId: 'abc', + userIdentifierValue: 'elastic', + jwtKeyId: 'def', + }, + secrets: { + clientSecret: 'xyz', + privateKey: testPrivateKey, + }, + }, + }); + + expect(body.statusCode).to.equal(400); + expect(body.message).to.equal( + `target url "https://servicenow.nonexistent.com/oauth_token.do" is not added to the Kibana config xpack.actions.allowedHosts` + ); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts index 6d1ecdbee566c8..9c1b6a4fd8299c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts @@ -25,6 +25,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./builtin_action_types/pagerduty')); loadTestFile(require.resolve('./builtin_action_types/swimlane')); loadTestFile(require.resolve('./builtin_action_types/server_log')); + loadTestFile(require.resolve('./builtin_action_types/oauth_access_token')); loadTestFile(require.resolve('./builtin_action_types/servicenow_itsm')); loadTestFile(require.resolve('./builtin_action_types/servicenow_sir')); loadTestFile(require.resolve('./builtin_action_types/servicenow_itom')); From fadd817114df4de8e2f327da89b780800d648978 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Fri, 6 May 2022 12:44:38 -0700 Subject: [PATCH 72/83] [Screenshotting] instrument for benchmark tests using new EventLogger class (#130356) * use an EventLogger throughout a screenshotting flow * unique id for each pipeline flow * fix open_url logging * add comments * add unit test * fix getTimeRangeEnd * improve logging of thrown errors * log the number of pixels using zoom * use elementPositionAndAttributes for logging * fix tests * replace multiple methods for logging spans with single log method * fix test * fix sessionId not showing in error logs * prettify message * more logging improvements * add specific error logging around get screenshots * function level comments * error handling around getting render errors * ensure original logger.error always still called * fix error logs not having the right logging context * more error logging around pdfMaker * more error logging around re-position elements * fix test * fix error re-throw after logging * use apm to capture the error * simplify eventLogger api * single startTransaction method * shortcut methods for screenshot/pdf event log Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/screenshotting/common/index.ts | 2 + .../server/formats/pdf/index.ts | 14 +- .../server/formats/pdf/pdf_maker/index.ts | 62 ++-- .../server/formats/pdf/pdf_maker/tracker.ts | 52 --- .../plugins/screenshotting/server/plugin.ts | 4 +- .../__snapshots__/index.test.ts.snap | 4 +- .../screenshots/event_logger/index.test.ts | 261 +++++++++++++++ .../server/screenshots/event_logger/index.ts | 315 ++++++++++++++++++ .../get_element_position_data.test.ts | 14 +- .../screenshots/get_element_position_data.ts | 21 +- .../screenshots/get_number_of_items.test.ts | 17 +- .../server/screenshots/get_number_of_items.ts | 28 +- .../screenshots/get_render_errors.test.ts | 15 +- .../server/screenshots/get_render_errors.ts | 70 ++-- .../screenshots/get_screenshots.test.ts | 17 +- .../server/screenshots/get_screenshots.ts | 47 ++- .../server/screenshots/get_time_range.test.ts | 17 +- .../server/screenshots/get_time_range.ts | 20 +- .../server/screenshots/index.ts | 51 ++- .../server/screenshots/inject_css.ts | 24 +- .../server/screenshots/observable.test.ts | 17 +- .../server/screenshots/observable.ts | 66 ++-- .../server/screenshots/open_url.ts | 26 +- .../index.test.ts} | 2 +- .../{semaphore.ts => semaphore/index.ts} | 0 .../server/screenshots/wait_for_render.ts | 19 +- .../screenshots/wait_for_visualizations.ts | 25 +- 27 files changed, 914 insertions(+), 296 deletions(-) delete mode 100644 x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/tracker.ts create mode 100644 x-pack/plugins/screenshotting/server/screenshots/event_logger/index.test.ts create mode 100644 x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts rename x-pack/plugins/screenshotting/server/screenshots/{semaphore.test.ts => semaphore/index.test.ts} (98%) rename x-pack/plugins/screenshotting/server/screenshots/{semaphore.ts => semaphore/index.ts} (100%) diff --git a/x-pack/plugins/screenshotting/common/index.ts b/x-pack/plugins/screenshotting/common/index.ts index b6b9034cb81896..7570477a1c1c92 100644 --- a/x-pack/plugins/screenshotting/common/index.ts +++ b/x-pack/plugins/screenshotting/common/index.ts @@ -14,3 +14,5 @@ export { SCREENSHOTTING_EXPRESSION, SCREENSHOTTING_EXPRESSION_INPUT, } from './expression'; + +export const PLUGIN_ID = 'screenshotting'; diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts index 7a2453b2a426b8..ce28c53bb5f888 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { groupBy } from 'lodash'; import type { Values } from '@kbn/utility-types'; -import type { Logger, PackageInfo } from '@kbn/core/server'; +import { groupBy } from 'lodash'; +import type { PackageInfo } from '@kbn/core/server'; import type { LayoutParams } from '../../../common'; import { LayoutTypes } from '../../../common'; import type { Layout } from '../../layouts'; -import type { CaptureOptions, CaptureResult, CaptureMetrics } from '../../screenshots'; +import type { CaptureMetrics, CaptureOptions, CaptureResult } from '../../screenshots'; +import { EventLogger, Transactions } from '../../screenshots/event_logger'; import { pngsToPdf } from './pdf_maker'; /** @@ -92,7 +93,7 @@ function getTimeRange(results: CaptureResult['results']) { } export async function toPdf( - logger: Logger, + eventLogger: EventLogger, packageInfo: PackageInfo, layout: Layout, { logo, title }: PdfScreenshotOptions, @@ -106,7 +107,7 @@ export async function toPdf( layout, logo, packageInfo, - logger, + eventLogger, }); return { @@ -119,7 +120,8 @@ export async function toPdf( renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []), }; } catch (error) { - logger.error(`Could not generate the PDF buffer!`); + eventLogger.kbnLogger.error(`Could not generate the PDF buffer!`); + eventLogger.error(error, Transactions.PDF); throw error; } diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts index be69ec4c5e1419..280b9173c79209 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts @@ -5,17 +5,17 @@ * 2.0. */ -import type { Logger, PackageInfo } from '@kbn/core/server'; -import { PdfMaker } from './pdfmaker'; +import type { PackageInfo } from '@kbn/core/server'; import type { Layout } from '../../../layouts'; -import { getTracker } from './tracker'; import type { CaptureResult } from '../../../screenshots'; +import { Actions, EventLogger, Transactions } from '../../../screenshots/event_logger'; +import { PdfMaker } from './pdfmaker'; interface PngsToPdfArgs { results: CaptureResult['results']; layout: Layout; packageInfo: PackageInfo; - logger: Logger; + eventLogger: EventLogger; logo?: string; title?: string; } @@ -26,37 +26,43 @@ export async function pngsToPdf({ logo, title, packageInfo, - logger, + eventLogger, }: PngsToPdfArgs): Promise<{ buffer: Buffer; pages: number }> { - const pdfMaker = new PdfMaker(layout, logo, packageInfo, logger); - const tracker = getTracker(); - if (title) { - pdfMaker.setTitle(title); - } - results.forEach((result) => { - result.screenshots.forEach((png) => { - tracker.startAddImage(); - pdfMaker.addImage(png.data, { - title: png.title ?? undefined, - description: png.description ?? undefined, - }); - tracker.endAddImage(); - }); - }); + const { kbnLogger } = eventLogger; + const transactionEnd = eventLogger.startTransaction(Transactions.PDF); let buffer: Uint8Array | null = null; + let pdfMaker: PdfMaker | null = null; try { - tracker.startCompile(); + pdfMaker = new PdfMaker(layout, logo, packageInfo, kbnLogger); + if (title) { + pdfMaker.setTitle(title); + } + results.forEach((result) => { + result.screenshots.forEach((png) => { + const spanEnd = eventLogger.logPdfEvent( + 'add image to PDF file', + Actions.ADD_IMAGE, + 'output' + ); + pdfMaker?.addImage(png.data, { + title: png.title ?? undefined, + description: png.description ?? undefined, + }); + spanEnd(); + }); + }); + + const spanEnd = eventLogger.logPdfEvent('compile PDF file', Actions.COMPILE, 'output'); buffer = await pdfMaker.generate(); - tracker.endCompile(); + spanEnd(); const byteLength = buffer?.byteLength ?? 0; - logger.debug(`PDF buffer byte length: ${byteLength}`); - tracker.setByteLength(byteLength); - } catch (err) { - throw err; - } finally { - tracker.end(); + transactionEnd({ labels: { byte_length_pdf: byteLength, pdf_pages: pdfMaker.getPageCount() } }); + } catch (error) { + kbnLogger.error(error); + eventLogger.error(error, Actions.COMPILE); + throw error; } return { buffer: Buffer.from(buffer.buffer), pages: pdfMaker.getPageCount() }; diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/tracker.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/tracker.ts deleted file mode 100644 index 49576a03d18a39..00000000000000 --- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/tracker.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import apm from 'elastic-apm-node'; - -interface PdfTracker { - setByteLength: (byteLength: number) => void; - startAddImage: () => void; - endAddImage: () => void; - startCompile: () => void; - endCompile: () => void; - end: () => void; -} - -const TRANSACTION_TYPE = 'reporting'; // TODO: Find out whether we can rename to "screenshotting"; -const SPANTYPE_OUTPUT = 'output'; - -interface ApmSpan { - end: () => void; -} - -export function getTracker(): PdfTracker { - const apmTrans = apm.startTransaction('generate-pdf', TRANSACTION_TYPE); - - let apmAddImage: ApmSpan | null = null; - let apmCompilePdf: ApmSpan | null = null; - - return { - startAddImage() { - apmAddImage = apmTrans?.startSpan('add-pdf-image', SPANTYPE_OUTPUT) || null; - }, - endAddImage() { - apmAddImage?.end(); - }, - startCompile() { - apmCompilePdf = apmTrans?.startSpan('compile-pdf', SPANTYPE_OUTPUT) || null; - }, - endCompile() { - apmCompilePdf?.end(); - }, - setByteLength(byteLength: number) { - apmTrans?.setLabel('byte-length', byteLength, false); - }, - end() { - apmTrans?.end(); - }, - }; -} diff --git a/x-pack/plugins/screenshotting/server/plugin.ts b/x-pack/plugins/screenshotting/server/plugin.ts index 27da8b3430e6d0..144b88a2c1c75e 100755 --- a/x-pack/plugins/screenshotting/server/plugin.ts +++ b/x-pack/plugins/screenshotting/server/plugin.ts @@ -85,11 +85,9 @@ export class ScreenshottingPlugin implements Plugin { const browserDriverFactory = await this.browserDriverFactory; - const logger = this.logger.get('screenshot'); - return new Screenshots( browserDriverFactory, - logger, + this.logger, this.packageInfo, http, this.config, diff --git a/x-pack/plugins/screenshotting/server/screenshots/__snapshots__/index.test.ts.snap b/x-pack/plugins/screenshotting/server/screenshots/__snapshots__/index.test.ts.snap index c0971c9b95763f..1b3826ce9980d6 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/__snapshots__/index.test.ts.snap +++ b/x-pack/plugins/screenshotting/server/screenshots/__snapshots__/index.test.ts.snap @@ -20,7 +20,7 @@ Array [ }, }, ], - "error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Mock error!], + "error": [Error: The "wait for elements" phase encountered an error: Error: Mock error!], "renderErrors": undefined, "screenshots": Array [ Object { @@ -63,7 +63,7 @@ Array [ }, }, ], - "error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Mock error!], + "error": [Error: The "wait for elements" phase encountered an error: Error: Mock error!], "renderErrors": undefined, "screenshots": Array [ Object { diff --git a/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.test.ts b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.test.ts new file mode 100644 index 00000000000000..3a20c404ff4979 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.test.ts @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { Actions, EventLogger, ScreenshottingAction, Transactions } from '.'; +import { ElementPosition } from '../get_element_position_data'; +import { ConfigType } from '../../config'; + +jest.mock('uuid', () => ({ + v4: () => 'NEW_UUID', +})); + +type EventLoggerArgs = [message: string, meta: ScreenshottingAction]; +describe('Event Logger', () => { + let eventLogger: EventLogger; + let config: ConfigType; + let logSpy: jest.SpyInstance; + + beforeEach(() => { + const testDate = moment(new Date('2021-04-12T16:00:00.000Z')); + let delaySeconds = 1; + + jest.spyOn(global.Date, 'now').mockImplementation(() => { + return testDate.add(delaySeconds++, 'seconds').valueOf(); + }); + + const logger = loggingSystemMock.createLogger(); + config = { capture: { zoom: 2 } } as ConfigType; + eventLogger = new EventLogger(logger, config); + + logSpy = jest.spyOn(logger, 'debug') as jest.SpyInstance; + }); + + it('creates logs for the events and includes durations and event payload data', () => { + const screenshottingEnd = eventLogger.startTransaction(Transactions.SCREENSHOTTING); + const openUrlEnd = eventLogger.logScreenshottingEvent( + 'open the url to the Kibana application', + Actions.OPEN_URL, + 'wait' + ); + openUrlEnd(); + const getElementPositionsEnd = eventLogger.logScreenshottingEvent( + 'scan the page to find the boundaries of visualization elements', + Actions.GET_ELEMENT_POSITION_DATA, + 'wait' + ); + getElementPositionsEnd(); + screenshottingEnd({ + labels: { + cpu: 12, + cpu_percentage: 0, + memory: 450789, + memory_mb: 449, + byte_length: 14000, + }, + }); + + const pdfEnd = eventLogger.startTransaction(Transactions.PDF); + const addImageEnd = eventLogger.logPdfEvent( + 'add image to the PDF file', + Actions.ADD_IMAGE, + 'output' + ); + addImageEnd(); + pdfEnd({ labels: { pdf_pages: 1, byte_length_pdf: 6666 } }); + + const logs = logSpy.mock.calls.map(([message, data]) => ({ + message, + duration: data?.event?.duration, + screenshotting: data?.kibana?.screenshotting, + })); + + expect(logs.length).toBe(10); + expect(logs).toMatchInlineSnapshot(` + Array [ + Object { + "duration": undefined, + "message": "starting: screenshot-pipeline", + "screenshotting": Object { + "action": "screenshot-pipeline-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": undefined, + "message": "starting: open the url to the Kibana application", + "screenshotting": Object { + "action": "open-url-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": 3000, + "message": "completed: open the url to the Kibana application", + "screenshotting": Object { + "action": "open-url-complete", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": undefined, + "message": "starting: scan the page to find the boundaries of visualization elements", + "screenshotting": Object { + "action": "get-element-position-data-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": 5000, + "message": "completed: scan the page to find the boundaries of visualization elements", + "screenshotting": Object { + "action": "get-element-position-data-complete", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": 20000, + "message": "completed: screenshot-pipeline", + "screenshotting": Object { + "action": "screenshot-pipeline-complete", + "byte_length": 14000, + "cpu": 12, + "cpu_percentage": 0, + "memory": 450789, + "memory_mb": 449, + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": undefined, + "message": "starting: generate-pdf", + "screenshotting": Object { + "action": "generate-pdf-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": undefined, + "message": "starting: add image to the PDF file", + "screenshotting": Object { + "action": "add-pdf-image-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": 9000, + "message": "completed: add image to the PDF file", + "screenshotting": Object { + "action": "add-pdf-image-complete", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": 27000, + "message": "completed: generate-pdf", + "screenshotting": Object { + "action": "generate-pdf-complete", + "byte_length_pdf": 6666, + "pdf_pages": 1, + "session_id": "NEW_UUID", + }, + }, + ] + `); + }); + + it('logs the number of pixels', () => { + const elementPosition = { + boundingClientRect: { width: 1350, height: 2000 }, + scroll: {}, + } as ElementPosition; + const endScreenshot = eventLogger.logScreenshottingEvent( + 'screenshot capture test', + Actions.GET_SCREENSHOT, + 'read', + eventLogger.getPixelsFromElementPosition(elementPosition) + ); + endScreenshot({ byte_length: 4444 }); + + const logData = logSpy.mock.calls.map(([message, data]) => ({ + message, + duration: data.event?.duration, + screenshotting: data.kibana.screenshotting, + })); + + expect(logData).toMatchInlineSnapshot(` + Array [ + Object { + "duration": undefined, + "message": "starting: screenshot capture test", + "screenshotting": Object { + "action": "get-screenshots-start", + "pixels": 10800000, + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": 2000, + "message": "completed: screenshot capture test", + "screenshotting": Object { + "action": "get-screenshots-complete", + "byte_length": 4444, + "pixels": 10800000, + "session_id": "NEW_UUID", + }, + }, + ] + `); + }); + + it('creates helpful error logs', () => { + eventLogger.startTransaction(Transactions.SCREENSHOTTING); + eventLogger.logScreenshottingEvent('opening the url', Actions.OPEN_URL, 'wait'); + eventLogger.error(new Error('Something erroneous happened'), Transactions.SCREENSHOTTING); + + const logData = logSpy.mock.calls.map(([message, data]) => ({ + message, + error: data.error, + screenshotting: data.kibana.screenshotting, + })); + + expect(logData).toMatchInlineSnapshot(` + Array [ + Object { + "error": undefined, + "message": "starting: screenshot-pipeline", + "screenshotting": Object { + "action": "screenshot-pipeline-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "error": undefined, + "message": "starting: opening the url", + "screenshotting": Object { + "action": "open-url-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "error": Object { + "code": undefined, + "message": "Something erroneous happened", + "stack_trace": undefined, + "type": undefined, + }, + "message": "Error: Something erroneous happened", + "screenshotting": Object { + "action": "screenshot-pipeline-error", + "session_id": "NEW_UUID", + }, + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts new file mode 100644 index 00000000000000..033fb24c80685b --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts @@ -0,0 +1,315 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, LogMeta } from '@kbn/core/server'; +import apm from 'elastic-apm-node'; +import uuid from 'uuid'; +import { CaptureResult } from '..'; +import { PLUGIN_ID } from '../../../common'; +import { ConfigType } from '../../config'; +import { ElementPosition } from '../get_element_position_data'; +import { Screenshot } from '../get_screenshots'; + +export enum Actions { + OPEN_URL = 'open-url', + GET_ELEMENT_POSITION_DATA = 'get-element-position-data', + GET_NUMBER_OF_ITEMS = 'get-number-of-items', + GET_RENDER_ERRORS = 'get-render-errors', + GET_TIMERANGE = 'get-timerange', + INJECT_CSS = 'inject-css', + REPOSITION = 'position-elements', + WAIT_RENDER = 'wait-for-render', + WAIT_VISUALIZATIONS = 'wait-for-visualizations', + GET_SCREENSHOT = 'get-screenshots', + ADD_IMAGE = 'add-pdf-image', + COMPILE = 'compile-pdf', +} + +export enum Transactions { + SCREENSHOTTING = 'screenshot-pipeline', + PDF = 'generate-pdf', +} + +export type SpanTypes = 'setup' | 'read' | 'wait' | 'correction' | 'output'; + +export interface ScreenshottingAction extends LogMeta { + event?: { + duration?: number; // number of nanoseconds from begin to end of an event + provider: typeof PLUGIN_ID; + }; + + message: string; + kibana: { + screenshotting: { + action: Actions | Transactions; + session_id: string; + + // chromium stats + cpu?: number; + cpu_percentage?: number; + memory?: number; + memory_mb?: number; + + // screenshotting stats + items_count?: number; + pixels?: number; + byte_length?: number; + element_positions?: number; + render_errors?: number; + + // pdf stats + byte_length_pdf?: number; + pdf_pages?: number; + }; + }; +} + +interface ErrorAction { + message: string; + code?: string; + stack_trace?: string; + type?: string; +} + +type SimpleEvent = Omit; + +type LogAdapter = ( + message: string, + suffix: 'start' | 'complete' | 'error', + event: Partial, + startTime?: Date | undefined +) => void; + +type Labels = Record; +type TransactionEndFn = (args: { labels: Partial }) => void; +type LogEndFn = (metricData?: Partial) => void; + +function fillLogData( + message: string, + event: Partial, + suffix: 'start' | 'complete' | 'error', + sessionId: string, + duration: number | undefined +) { + let newMessage = message; + if (suffix !== 'error') { + newMessage = `${suffix === 'start' ? 'starting' : 'completed'}: ${message}`; + } + + let interpretedAction: string; + if (suffix === 'error') { + interpretedAction = event.action + '-error'; + } else { + interpretedAction = event.action + `-${suffix}`; + } + + const logData: ScreenshottingAction = { + message: newMessage, + kibana: { + screenshotting: { + ...event, + action: interpretedAction as Actions, + session_id: sessionId, + }, + }, + event: { duration, provider: PLUGIN_ID }, + }; + return logData; +} + +function logAdapter(logger: Logger, sessionId: string) { + const log: LogAdapter = (message, suffix, event, startTime) => { + let duration: number | undefined; + if (startTime != null) { + const start = startTime.valueOf(); + duration = new Date(Date.now()).valueOf() - start.valueOf(); + } + + const logData = fillLogData(message, event, suffix, sessionId, duration); + logger.debug(logData.message, logData); + }; + return log; +} + +/** + * A class to use internal state properties to log timing between actions in the screenshotting pipeline + */ +export class EventLogger { + private spans = new Map(); + private transactions: Record = { + 'screenshot-pipeline': null, + 'generate-pdf': null, + }; + + private sessionId: string; // identifier to track all logs from one screenshotting flow + private logEvent: LogAdapter; + private timings: Partial> = {}; + + constructor(private readonly logger: Logger, private readonly config: ConfigType) { + this.sessionId = uuid.v4(); + this.logEvent = logAdapter(logger.get('events'), this.sessionId); + } + + private startTiming(a: Actions | Transactions) { + this.timings[a] = new Date(Date.now()); + } + + /** + * @returns Logger - original logger + */ + public get kbnLogger() { + return this.logger; + } + + /** + * General method for logging the beginning of any of this plugin's pipeline + * + * @returns {ScreenshottingEndFn} + */ + public startTransaction( + action: Transactions.SCREENSHOTTING | Transactions.PDF + ): TransactionEndFn { + this.transactions[action] = apm.startTransaction(action, PLUGIN_ID); + const transaction = this.transactions[action]; + + this.startTiming(action); + this.logEvent(action, 'start', { action }); + + return ({ labels }) => { + Object.entries(labels).forEach(([label]) => { + const labelField = label as keyof SimpleEvent; + const labelValue = labels[labelField]; + transaction?.setLabel(label, labelValue, false); + }); + + transaction?.end(); + + this.logEvent(action, 'complete', { ...labels, action }, this.timings[action]); + }; + } + + /** + * General event logging function + * + * @param {string} message + * @param {Actions} action - action type for kibana.screenshotting.action + * @param {TransactionType} transaction - name of the internal APM transaction in which to associate the span + * @param {SpanTypes} type - identifier of the span type + * @param {metricsPre} type - optional metrics to add to the "start" log of the event + * @returns {LogEndFn} - function to log the end of the span + */ + public log( + message: string, + action: Actions, + type: SpanTypes, + metricsPre: Partial = {}, + transaction: Transactions + ): LogEndFn { + const txn = this.transactions[transaction]; + const span = txn?.startSpan(action, type); + + this.spans.set(action, span); + this.startTiming(action); + this.logEvent(message, 'start', { ...metricsPre, action }); + + return (metricData = {}) => { + span?.end(); + this.logEvent( + message, + 'complete', + { ...metricsPre, ...metricData, action }, + this.timings[action] + ); + }; + } + + /** + * Logging helper for screenshotting events + */ + public logScreenshottingEvent( + message: string, + action: Actions, + type: SpanTypes, + metricsPre: Partial = {} + ) { + return this.log(message, action, type, metricsPre, Transactions.SCREENSHOTTING); + } + + /** + * Logging helper for screenshotting events + */ + public logPdfEvent( + message: string, + action: Actions, + type: SpanTypes, + metricsPre: Partial = {} + ) { + return this.log(message, action, type, metricsPre, Transactions.PDF); + } + + /** + * Helper function to calculate the byte length of a set of captured PNG images + */ + public getByteLengthFromCaptureResults( + results: CaptureResult['results'] + ): Pick { + const totalByteLength = results.reduce( + (totals, { screenshots }) => + totals + + screenshots.reduce( + (byteLength: number, screenshot: Screenshot) => byteLength + screenshot.data.byteLength, + 0 + ), + 0 + ); + return { byte_length: totalByteLength }; + } + + /** + * Helper function to create the "metricPre" data needed to log the start + * of a screenshot capture event. + */ + public getPixelsFromElementPosition( + elementPosition: ElementPosition + ): Pick { + const { width, height } = elementPosition.boundingClientRect; + const zoom = this.config.capture.zoom; + const pixels = width * zoom * (height * zoom); + return { pixels }; + } + + /** + * General error logger + * + * @param {ErrorAction} error: The error object that was caught + * @param {Actions} action: The screenshotting action type + * @returns void + */ + public error(error: ErrorAction | string, action: Actions | Transactions) { + const isError = typeof error === 'object'; + const message = `Error: ${isError ? error.message : error}`; + + const errorData = { + ...fillLogData( + message, + { action }, + 'error', + this.sessionId, + undefined // + ), + error: { + message: isError ? error.message : error, + code: isError ? error.code : undefined, + stack_trace: isError ? error.stack_trace : undefined, + type: isError ? error.type : undefined, + }, + }; + + this.logger.get('events').debug(message, errorData); + apm.captureError(error as Error | string); + } +} diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts index 915b036acf22ef..f3a76ca79d85f6 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts @@ -5,20 +5,24 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { createMockBrowserDriver } from '../browsers/mock'; +import { ConfigType } from '../config'; import { createMockLayout } from '../layouts/mock'; +import { EventLogger } from './event_logger'; import { getElementPositionAndAttributes } from './get_element_position_data'; describe('getElementPositionAndAttributes', () => { - const logger = {} as jest.Mocked; let browser: ReturnType; let layout: ReturnType; + let eventLogger: EventLogger; + let config = {} as ConfigType; beforeEach(async () => { browser = createMockBrowserDriver(); layout = createMockLayout(); - + config = { capture: { zoom: 2 } } as ConfigType; + eventLogger = new EventLogger(loggingSystemMock.createLogger(), config); browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); // @see https://github.com/jsdom/jsdom/issues/653 @@ -59,7 +63,7 @@ describe('getElementPositionAndAttributes', () => { /> `; - await expect(getElementPositionAndAttributes(browser, logger, layout)).resolves + await expect(getElementPositionAndAttributes(browser, eventLogger, layout)).resolves .toMatchInlineSnapshot(` Array [ Object { @@ -103,6 +107,6 @@ describe('getElementPositionAndAttributes', () => { }); it('should return null when there are no elements matching', async () => { - await expect(getElementPositionAndAttributes(browser, logger, layout)).resolves.toBeNull(); + await expect(getElementPositionAndAttributes(browser, eventLogger, layout)).resolves.toBeNull(); }); }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts index e3235c6d232532..5018701ce24116 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts @@ -5,11 +5,10 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; +import { Actions, EventLogger } from './event_logger'; export interface AttributesMap { [key: string]: string | null; @@ -36,10 +35,17 @@ export interface ElementsPositionAndAttribute { export const getElementPositionAndAttributes = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, layout: Layout ): Promise => { - const span = apm.startSpan('get_element_position_data', 'read'); + const { kbnLogger } = eventLogger; + + const spanEnd = eventLogger.logScreenshottingEvent( + 'get element position data', + Actions.GET_ELEMENT_POSITION_DATA, + 'read' + ); + const { screenshot: screenshotSelector } = layout.selectors; // data-shared-items-container let elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null; try { @@ -77,7 +83,7 @@ export const getElementPositionAndAttributes = async ( args: [screenshotSelector, { title: 'data-title', description: 'data-description' }], }, { context: CONTEXT_ELEMENTATTRIBUTES }, - logger + kbnLogger ); if (!elementsPositionAndAttributes?.length) { @@ -86,10 +92,13 @@ export const getElementPositionAndAttributes = async ( ); } } catch (err) { + kbnLogger.error(err); + eventLogger.error(err, Actions.GET_ELEMENT_POSITION_DATA); elementsPositionAndAttributes = null; + // no throw } - span?.end(); + spanEnd({ element_positions: elementsPositionAndAttributes?.length }); return elementsPositionAndAttributes; }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts index 34b8291eb03dad..a7c4f27065bcfa 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts @@ -5,22 +5,25 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { createMockBrowserDriver } from '../browsers/mock'; +import { ConfigType } from '../config'; import { createMockLayout } from '../layouts/mock'; +import { EventLogger } from './event_logger'; import { getNumberOfItems } from './get_number_of_items'; describe('getNumberOfItems', () => { const timeout = 10; let browser: ReturnType; let layout: ReturnType; - let logger: jest.Mocked; + let eventLogger: EventLogger; + let config = {} as ConfigType; beforeEach(async () => { browser = createMockBrowserDriver(); layout = createMockLayout(); - logger = { debug: jest.fn() } as unknown as jest.Mocked; - + config = { capture: { zoom: 2 } } as ConfigType; + eventLogger = new EventLogger(loggingSystemMock.createLogger(), config); browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); }); @@ -33,7 +36,7 @@ describe('getNumberOfItems', () => {
`; - await expect(getNumberOfItems(browser, logger, timeout, layout)).resolves.toBe(10); + await expect(getNumberOfItems(browser, eventLogger, timeout, layout)).resolves.toBe(10); }); it('should determine the number of items by selector ', async () => { @@ -43,7 +46,7 @@ describe('getNumberOfItems', () => { `; - await expect(getNumberOfItems(browser, logger, timeout, layout)).resolves.toBe(3); + await expect(getNumberOfItems(browser, eventLogger, timeout, layout)).resolves.toBe(3); }); it('should fall back to the selector when the attribute is empty', async () => { @@ -53,6 +56,6 @@ describe('getNumberOfItems', () => { `; - await expect(getNumberOfItems(browser, logger, timeout, layout)).resolves.toBe(2); + await expect(getNumberOfItems(browser, eventLogger, timeout, layout)).resolves.toBe(2); }); }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts index 9dab001e4730d2..0e4da2fe5cf6a4 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts @@ -5,24 +5,27 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; +import { Actions, EventLogger } from './event_logger'; export const getNumberOfItems = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, timeout: number, layout: Layout ): Promise => { - const span = apm.startSpan('get_number_of_items', 'read'); + const { kbnLogger } = eventLogger; + const spanEnd = eventLogger.logScreenshottingEvent( + 'get the number of visualization items on the page', + Actions.GET_NUMBER_OF_ITEMS, + 'read' + ); + const { renderComplete: renderCompleteSelector, itemsCountAttribute } = layout.selectors; let itemsCount: number; - logger.debug('waiting for elements or items count attribute; or not found to interrupt'); - try { // the dashboard is using the `itemsCountAttribute` attribute to let us // know how many items to expect since gridster incrementally adds panels @@ -31,7 +34,7 @@ export const getNumberOfItems = async ( `${renderCompleteSelector},[${itemsCountAttribute}]`, { timeout }, { context: CONTEXT_READMETADATA }, - logger + kbnLogger ); // returns the value of the `itemsCountAttribute` if it's there, otherwise @@ -52,16 +55,15 @@ export const getNumberOfItems = async ( args: [renderCompleteSelector, itemsCountAttribute], }, { context: CONTEXT_GETNUMBEROFITEMS }, - logger + kbnLogger ); } catch (error) { - logger.error(error); - throw new Error( - `An error occurred when trying to read the page for visualization panel info: ${error.message}` - ); + kbnLogger.error(error); + eventLogger.error(error, Actions.GET_NUMBER_OF_ITEMS); + throw error; } - span?.end(); + spanEnd({ items_count: itemsCount }); return itemsCount; }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts index a475e3c614c156..ece25b37725c8c 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts @@ -5,21 +5,24 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { createMockBrowserDriver } from '../browsers/mock'; +import { ConfigType } from '../config'; import { createMockLayout } from '../layouts/mock'; +import { EventLogger } from './event_logger'; import { getRenderErrors } from './get_render_errors'; describe('getRenderErrors', () => { let browser: ReturnType; let layout: ReturnType; - let logger: jest.Mocked; + let eventLogger: EventLogger; + let config = {} as ConfigType; beforeEach(async () => { browser = createMockBrowserDriver(); layout = createMockLayout(); - logger = { debug: jest.fn(), warn: jest.fn() } as unknown as jest.Mocked; - + config = { capture: { zoom: 2 } } as ConfigType; + eventLogger = new EventLogger(loggingSystemMock.createLogger(), config); browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); }); @@ -35,7 +38,7 @@ describe('getRenderErrors', () => {
`; - await expect(getRenderErrors(browser, logger, layout)).resolves.toEqual([ + await expect(getRenderErrors(browser, eventLogger, layout)).resolves.toEqual([ 'a test error', 'a test error', 'a test error', @@ -48,6 +51,6 @@ describe('getRenderErrors', () => { `; - await expect(getRenderErrors(browser, logger, layout)).resolves.toEqual(undefined); + await expect(getRenderErrors(browser, eventLogger, layout)).resolves.toEqual(undefined); }); }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts index e8beb189112105..44b92ceddbc8de 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts @@ -5,45 +5,59 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import type { Layout } from '../layouts'; import { CONTEXT_GETRENDERERRORS } from './constants'; +import { Actions, EventLogger } from './event_logger'; export const getRenderErrors = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, layout: Layout ): Promise => { - const span = apm.startSpan('get_render_errors', 'read'); - logger.debug('reading render errors'); - const errorsFound: undefined | string[] = await browser.evaluate( - { - fn: (errorSelector, errorAttribute) => { - const visualizations: Element[] = Array.from(document.querySelectorAll(errorSelector)); - const errors: string[] = []; - - visualizations.forEach((visualization) => { - const errorMessage = visualization.getAttribute(errorAttribute); - if (errorMessage) { - errors.push(errorMessage); - } - }); - - return errors.length ? errors : undefined; - }, - args: [layout.selectors.renderError, layout.selectors.renderErrorAttribute], - }, - { context: CONTEXT_GETRENDERERRORS }, - logger + const { kbnLogger } = eventLogger; + + const spanEnd = eventLogger.logScreenshottingEvent( + 'look for render errors', + Actions.GET_RENDER_ERRORS, + 'read' ); - span?.end(); - if (errorsFound?.length) { - logger.warn( - `Found ${errorsFound.length} error messages. See report object for more information.` + let errorsFound: undefined | string[]; + try { + errorsFound = await browser.evaluate( + { + fn: (errorSelector, errorAttribute) => { + const visualizations: Element[] = Array.from(document.querySelectorAll(errorSelector)); + const errors: string[] = []; + + visualizations.forEach((visualization) => { + const errorMessage = visualization.getAttribute(errorAttribute); + if (errorMessage) { + errors.push(errorMessage); + } + }); + + return errors.length ? errors : undefined; + }, + args: [layout.selectors.renderError, layout.selectors.renderErrorAttribute], + }, + { context: CONTEXT_GETRENDERERRORS }, + kbnLogger ); + + const renderErrors = errorsFound?.length; + if (renderErrors) { + kbnLogger.warn( + `Found ${renderErrors} error messages. See report object for more information.` + ); + } + + spanEnd({ render_errors: renderErrors }); + } catch (error) { + kbnLogger.error(error); + eventLogger.error(error, Actions.GET_RENDER_ERRORS); + throw error; } return errorsFound; diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts index 1f104b9bf2d80e..c2342280aea202 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts @@ -5,8 +5,10 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { createMockBrowserDriver } from '../browsers/mock'; +import { ConfigType } from '../config'; +import { EventLogger } from './event_logger'; import { getScreenshots } from './get_screenshots'; describe('getScreenshots', () => { @@ -27,12 +29,13 @@ describe('getScreenshots', () => { }, ]; let browser: ReturnType; - let logger: jest.Mocked; + let eventLogger: EventLogger; + let config = {} as ConfigType; beforeEach(async () => { browser = createMockBrowserDriver(); - logger = { info: jest.fn() } as unknown as jest.Mocked; - + config = { capture: { zoom: 2 } } as ConfigType; + eventLogger = new EventLogger(loggingSystemMock.createLogger(), config); browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); }); @@ -41,7 +44,7 @@ describe('getScreenshots', () => { }); it('should return screenshots', async () => { - await expect(getScreenshots(browser, logger, elementsPositionAndAttributes)).resolves + await expect(getScreenshots(browser, eventLogger, elementsPositionAndAttributes)).resolves .toMatchInlineSnapshot(` Array [ Object { @@ -87,7 +90,7 @@ describe('getScreenshots', () => { }); it('should forward elements positions', async () => { - await getScreenshots(browser, logger, elementsPositionAndAttributes); + await getScreenshots(browser, eventLogger, elementsPositionAndAttributes); expect(browser.screenshot).toHaveBeenCalledTimes(2); expect(browser.screenshot).toHaveBeenNthCalledWith( @@ -104,7 +107,7 @@ describe('getScreenshots', () => { browser.screenshot.mockResolvedValue(Buffer.from('')); await expect( - getScreenshots(browser, logger, elementsPositionAndAttributes) + getScreenshots(browser, eventLogger, elementsPositionAndAttributes) ).rejects.toBeInstanceOf(Error); }); }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts index 53829b098ee8cf..f157649bbb8488 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts @@ -5,9 +5,8 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; +import { Actions, EventLogger } from './event_logger'; import type { ElementsPositionAndAttribute } from './get_element_position_data'; export interface Screenshot { @@ -29,33 +28,45 @@ export interface Screenshot { export const getScreenshots = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, elementsPositionAndAttributes: ElementsPositionAndAttribute[] ): Promise => { - logger.info(`taking screenshots`); + const { kbnLogger } = eventLogger; + kbnLogger.info(`taking screenshots`); const screenshots: Screenshot[] = []; - for (let i = 0; i < elementsPositionAndAttributes.length; i++) { - const span = apm.startSpan('get_screenshots', 'read'); - const item = elementsPositionAndAttributes[i]; + try { + for (let i = 0; i < elementsPositionAndAttributes.length; i++) { + const item = elementsPositionAndAttributes[i]; + const endScreenshot = eventLogger.logScreenshottingEvent( + 'screenshot capture', + Actions.GET_SCREENSHOT, + 'read', + eventLogger.getPixelsFromElementPosition(item.position) + ); - const data = await browser.screenshot(item.position); + const data = await browser.screenshot(item.position); - if (!data?.byteLength) { - throw new Error(`Failure in getScreenshots! Screenshot data is void`); - } + if (!data?.byteLength) { + throw new Error(`Failure in getScreenshots! Screenshot data is void`); + } - screenshots.push({ - data, - title: item.attributes.title, - description: item.attributes.description, - }); + screenshots.push({ + data, + title: item.attributes.title, + description: item.attributes.description, + }); - span?.end(); + endScreenshot({ byte_length: data.byteLength }); + } + } catch (error) { + kbnLogger.error(error); + eventLogger.error(error, Actions.GET_SCREENSHOT); + throw error; } - logger.info(`screenshots taken: ${screenshots.length}`); + kbnLogger.info(`screenshots taken: ${screenshots.length}`); return screenshots; }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts index 8484412f5fd943..a7a7b9295068e8 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts @@ -5,21 +5,24 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { createMockBrowserDriver } from '../browsers/mock'; +import { ConfigType } from '../config'; import { createMockLayout } from '../layouts/mock'; +import { EventLogger } from './event_logger'; import { getTimeRange } from './get_time_range'; describe('getTimeRange', () => { let browser: ReturnType; let layout: ReturnType; - let logger: jest.Mocked; + let eventLogger: EventLogger; + let config = {} as ConfigType; beforeEach(async () => { browser = createMockBrowserDriver(); layout = createMockLayout(); - logger = { debug: jest.fn(), info: jest.fn() } as unknown as jest.Mocked; - + config = { capture: { zoom: 2 } } as ConfigType; + eventLogger = new EventLogger(loggingSystemMock.createLogger(), config); browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); }); @@ -28,7 +31,7 @@ describe('getTimeRange', () => { }); it('should return null when there is no duration element', async () => { - await expect(getTimeRange(browser, logger, layout)).resolves.toBeNull(); + await expect(getTimeRange(browser, eventLogger, layout)).resolves.toBeNull(); }); it('should return null when duration attrbute is empty', async () => { @@ -36,7 +39,7 @@ describe('getTimeRange', () => {
`; - await expect(getTimeRange(browser, logger, layout)).resolves.toBeNull(); + await expect(getTimeRange(browser, eventLogger, layout)).resolves.toBeNull(); }); it('should return duration', async () => { @@ -44,6 +47,6 @@ describe('getTimeRange', () => {
`; - await expect(getTimeRange(browser, logger, layout)).resolves.toBe('10'); + await expect(getTimeRange(browser, eventLogger, layout)).resolves.toBe('10'); }); }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts index 41d902436d36b7..f9272fd27ac955 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts @@ -5,19 +5,21 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_GETTIMERANGE } from './constants'; +import { Actions, EventLogger } from './event_logger'; export const getTimeRange = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, layout: Layout ): Promise => { - const span = apm.startSpan('get_time_range', 'read'); - logger.debug('getting timeRange'); + const spanEnd = eventLogger.logScreenshottingEvent( + 'looking for time range', + Actions.GET_TIMERANGE, + 'read' + ); const timeRange = await browser.evaluate( { @@ -38,16 +40,14 @@ export const getTimeRange = async ( args: [layout.selectors.timefilterDurationAttribute], }, { context: CONTEXT_GETTIMERANGE }, - logger + eventLogger.kbnLogger ); if (timeRange) { - logger.info(`timeRange: ${timeRange}`); - } else { - logger.debug('no timeRange'); + eventLogger.kbnLogger.info(`timeRange: ${timeRange}`); } - span?.end(); + spanEnd(); return timeRange; }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.ts b/x-pack/plugins/screenshotting/server/screenshots/index.ts index b98270547dbece..33404bb5fadc29 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/index.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/index.ts @@ -8,8 +8,6 @@ import type { HttpServiceSetup, KibanaRequest, Logger, PackageInfo } from '@kbn/core/server'; import type { ExpressionAstExpression } from '@kbn/expressions-plugin/common'; import type { Optional } from '@kbn/utility-types'; -import type { Transaction } from 'elastic-apm-node'; -import apm from 'elastic-apm-node'; import ipaddr from 'ipaddr.js'; import { defaultsDeep, sum } from 'lodash'; import { from, Observable, of, throwError } from 'rxjs'; @@ -46,6 +44,7 @@ import { } from '../formats'; import type { Layout } from '../layouts'; import { createLayout } from '../layouts'; +import { EventLogger, Transactions } from './event_logger'; import type { ScreenshotObservableOptions, ScreenshotObservableResult } from './observable'; import { ScreenshotObservableHandler, UrlOrUrlWithContext } from './observable'; import { Semaphore } from './semaphore'; @@ -110,21 +109,18 @@ export class Screenshots { this.semaphore = new Semaphore(config.poolSize); } - private createLayout(transaction: Transaction | null, options: CaptureOptions): Layout { - const apmCreateLayout = transaction?.startSpan('create-layout', 'setup'); + private createLayout(options: CaptureOptions): Layout { const layout = createLayout(options.layout ?? {}); this.logger.debug(`Layout: width=${layout.width} height=${layout.height}`); - apmCreateLayout?.end(); return layout; } private captureScreenshots( + eventLogger: EventLogger, layout: Layout, - transaction: Transaction | null, options: ScreenshotObservableOptions ): Observable { - const apmCreatePage = transaction?.startSpan('create-page', 'wait'); const { browserTimezone } = options; return this.browserDriverFactory @@ -139,24 +135,22 @@ export class Screenshots { .pipe( this.semaphore.acquire(), mergeMap(({ driver, unexpectedExit$, close }) => { - apmCreatePage?.end(); - unexpectedExit$.subscribe({ error: () => transaction?.end() }); - const screen = new ScreenshotObservableHandler( driver, this.config, - this.logger, + eventLogger, layout, options ); return from(options.urls).pipe( concatMap((url, index) => - screen.setupPage(index, url, transaction).pipe( + screen.setupPage(index, url).pipe( catchError((error) => { screen.checkPageIsOpen(); // this fails the job if the browser has closed this.logger.error(error); + eventLogger.error(error, Transactions.SCREENSHOTTING); return of({ ...DEFAULT_SETUP_RESULT, error }); // allow failover screenshot capture }), takeUntil(unexpectedExit$), @@ -166,16 +160,8 @@ export class Screenshots { take(options.urls.length), toArray(), mergeMap((results) => - // At this point we no longer need the page, close it. - close().pipe( - tap(({ metrics }) => { - if (metrics) { - transaction?.setLabel('cpu', metrics.cpu, false); - transaction?.setLabel('memory', metrics.memory, false); - } - }), - map(({ metrics }) => ({ metrics, results })) - ) + // At this point we no longer need the page, close it and send out the results + close().pipe(map(({ metrics }) => ({ metrics, results }))) ) ); }), @@ -243,15 +229,28 @@ export class Screenshots { if (this.systemHasInsufficientMemory()) { return throwError(() => new errors.InsufficientMemoryAvailableOnCloudError()); } - const transaction = apm.startTransaction('screenshot-pipeline', 'screenshotting'); - const layout = this.createLayout(transaction, options); + + const eventLogger = new EventLogger(this.logger, this.config); + const transactionEnd = eventLogger.startTransaction(Transactions.SCREENSHOTTING); + + const layout = this.createLayout(options); const captureOptions = this.getCaptureOptions(options); - return this.captureScreenshots(layout, transaction, captureOptions).pipe( + return this.captureScreenshots(eventLogger, layout, captureOptions).pipe( + tap(({ results, metrics }) => { + transactionEnd({ + labels: { + cpu: metrics?.cpu, + memory: metrics?.memory, + memory_mb: metrics?.memoryInMegabytes, + ...eventLogger.getByteLengthFromCaptureResults(results), + }, + }); + }), mergeMap((result) => { switch (options.format) { case 'pdf': - return toPdf(this.logger, this.packageInfo, layout, options, result); + return toPdf(eventLogger, this.packageInfo, layout, options, result); default: return toPng(result); } diff --git a/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts b/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts index a77cfa8c9e8e69..41426e893ce586 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts @@ -7,26 +7,31 @@ import fs from 'fs'; import { promisify } from 'util'; -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_INJECTCSS } from './constants'; +import { Actions, EventLogger } from './event_logger'; const fsp = { readFile: promisify(fs.readFile) }; export const injectCustomCss = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, layout: Layout ): Promise => { - const span = apm.startSpan('inject_css', 'correction'); - logger.debug('injecting custom css'); - const filePath = layout.getCssOverridesPath(); if (!filePath) { return; } + + const { kbnLogger } = eventLogger; + + const spanEnd = eventLogger.logScreenshottingEvent( + 'inject CSS into the page', + Actions.INJECT_CSS, + 'correction' + ); + const buffer = await fsp.readFile(filePath); try { await browser.evaluate( @@ -40,14 +45,15 @@ export const injectCustomCss = async ( args: [buffer.toString()], }, { context: CONTEXT_INJECTCSS }, - logger + kbnLogger ); } catch (err) { - logger.error(err); + kbnLogger.error(err); + eventLogger.error(err, Actions.INJECT_CSS); throw new Error( `An error occurred when trying to update Kibana CSS for reporting. ${err.message}` ); } - span?.end(); + spanEnd(); }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts index d3acc96411dc64..b282cd32bbd803 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts @@ -5,36 +5,33 @@ * 2.0. */ +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { interval, of, throwError } from 'rxjs'; import { map } from 'rxjs/operators'; -import type { Logger } from '@kbn/core/server'; import { createMockBrowserDriver } from '../browsers/mock'; import type { ConfigType } from '../config'; import { createMockLayout } from '../layouts/mock'; +import { EventLogger } from './event_logger'; import { ScreenshotObservableHandler, ScreenshotObservableOptions } from './observable'; describe('ScreenshotObservableHandler', () => { let browser: ReturnType; let config: ConfigType; let layout: ReturnType; - let logger: jest.Mocked; + let eventLogger: EventLogger; let options: ScreenshotObservableOptions; beforeEach(async () => { browser = createMockBrowserDriver(); config = { capture: { - timeouts: { - openUrl: 30000, - waitForElements: 30000, - renderComplete: 30000, - }, + timeouts: { openUrl: 30000, waitForElements: 30000, renderComplete: 30000 }, loadDelay: 5000, zoom: 13, }, } as ConfigType; layout = createMockLayout(); - logger = { error: jest.fn() } as unknown as jest.Mocked; + eventLogger = new EventLogger(loggingSystemMock.createLogger(), config); options = { headers: { testHeader: 'testHeadValue' }, urls: [], @@ -46,7 +43,7 @@ describe('ScreenshotObservableHandler', () => { describe('waitUntil', () => { let screenshots: ScreenshotObservableHandler; beforeEach(() => { - screenshots = new ScreenshotObservableHandler(browser, config, logger, layout, options); + screenshots = new ScreenshotObservableHandler(browser, config, eventLogger, layout, options); }); it('catches TimeoutError and references the timeout config in a custom message', async () => { @@ -79,7 +76,7 @@ describe('ScreenshotObservableHandler', () => { describe('checkPageIsOpen', () => { let screenshots: ScreenshotObservableHandler; beforeEach(() => { - screenshots = new ScreenshotObservableHandler(browser, config, logger, layout, options); + screenshots = new ScreenshotObservableHandler(browser, config, eventLogger, layout, options); }); it('throws a decorated Error when page is not open', async () => { diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.ts index b19f3f254b2a24..5048d3f0a3be66 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.ts @@ -5,15 +5,15 @@ * 2.0. */ -import type { Transaction } from 'elastic-apm-node'; +import type { Headers } from '@kbn/core/server'; import { defer, forkJoin, Observable, throwError } from 'rxjs'; import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; -import type { Headers, Logger } from '@kbn/core/server'; import { errors } from '../../common'; import type { Context, HeadlessChromiumDriver } from '../browsers'; import { DEFAULT_VIEWPORT, getChromiumDisconnectedError } from '../browsers'; -import { durationToNumber as toNumber, ConfigType } from '../config'; +import { ConfigType, durationToNumber as toNumber } from '../config'; import type { Layout } from '../layouts'; +import { Actions, EventLogger } from './event_logger'; import type { ElementsPositionAndAttribute } from './get_element_position_data'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; @@ -90,7 +90,9 @@ interface PageSetupResults { renderErrors?: string[]; } -const getDefaultElementPosition = (dimensions: { height?: number; width?: number } | null) => { +const getDefaultElementPosition = ( + dimensions: { height?: number; width?: number } | null +): ElementsPositionAndAttribute[] => { const height = dimensions?.height || DEFAULT_VIEWPORT.height; const width = dimensions?.width || DEFAULT_VIEWPORT.width; @@ -118,7 +120,7 @@ export class ScreenshotObservableHandler { constructor( private readonly driver: HeadlessChromiumDriver, private readonly config: ConfigType, - private readonly logger: Logger, + private readonly eventLogger: EventLogger, private readonly layout: Layout, private options: ScreenshotObservableOptions ) {} @@ -154,7 +156,7 @@ export class ScreenshotObservableHandler { return openUrl( this.driver, - this.logger, + this.eventLogger, toNumber(this.config.capture.timeouts.openUrl), index, url, @@ -168,52 +170,70 @@ export class ScreenshotObservableHandler { const driver = this.driver; const waitTimeout = toNumber(this.config.capture.timeouts.waitForElements); - return defer(() => getNumberOfItems(driver, this.logger, waitTimeout, this.layout)).pipe( + return defer(() => getNumberOfItems(driver, this.eventLogger, waitTimeout, this.layout)).pipe( mergeMap(async (itemsCount) => { // set the viewport to the dimensions from the job, to allow elements to flow into the expected layout const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort(); // Set the viewport allowing time for the browser to handle reflow and redraw // before checking for readiness of visualizations. - await driver.setViewport(viewport, this.logger); - await waitForVisualizations(driver, this.logger, waitTimeout, itemsCount, this.layout); + await driver.setViewport(viewport, this.eventLogger.kbnLogger); + await waitForVisualizations(driver, this.eventLogger, waitTimeout, itemsCount, this.layout); }), this.waitUntil(waitTimeout, 'wait for elements') ); } - private completeRender(apmTrans: Transaction | null) { + private completeRender() { const driver = this.driver; const layout = this.layout; - const logger = this.logger; + const eventLogger = this.eventLogger; return defer(async () => { // Waiting till _after_ elements have rendered before injecting our CSS // allows for them to be displayed properly in many cases - await injectCustomCss(driver, logger, layout); + await injectCustomCss(driver, eventLogger, layout); - const apmPositionElements = apmTrans?.startSpan('position-elements', 'correction'); - // position panel elements for print layout - await layout.positionElements?.(driver, logger); - apmPositionElements?.end(); + const spanEnd = this.eventLogger.logScreenshottingEvent( + 'get positions of visualization elements', + Actions.GET_ELEMENT_POSITION_DATA, + 'read' + ); + try { + // position panel elements for print layout + await layout.positionElements?.(driver, eventLogger.kbnLogger); + spanEnd(); + } catch (error) { + eventLogger.error(error, Actions.GET_ELEMENT_POSITION_DATA); + throw error; + } - await waitForRenderComplete(driver, logger, toNumber(this.config.capture.loadDelay), layout); + await waitForRenderComplete( + driver, + eventLogger, + toNumber(this.config.capture.loadDelay), + layout + ); }).pipe( mergeMap(() => forkJoin({ - timeRange: getTimeRange(driver, logger, layout), - elementsPositionAndAttributes: getElementPositionAndAttributes(driver, logger, layout), - renderErrors: getRenderErrors(driver, logger, layout), + timeRange: getTimeRange(driver, eventLogger, layout), + elementsPositionAndAttributes: getElementPositionAndAttributes( + driver, + eventLogger, + layout + ), + renderErrors: getRenderErrors(driver, eventLogger, layout), }) ), this.waitUntil(toNumber(this.config.capture.timeouts.renderComplete), 'render complete') ); } - public setupPage(index: number, url: UrlOrUrlWithContext, apmTrans: Transaction | null) { + public setupPage(index: number, url: UrlOrUrlWithContext) { return this.openUrl(index, url).pipe( switchMapTo(this.waitForElements()), - switchMapTo(this.completeRender(apmTrans)) + switchMapTo(this.completeRender()) ); } @@ -227,7 +247,7 @@ export class ScreenshotObservableHandler { getDefaultElementPosition(this.layout.getViewport(1)); let screenshots: Screenshot[] = []; try { - screenshots = await getScreenshots(this.driver, this.logger, elements); + screenshots = await getScreenshots(this.driver, this.eventLogger, elements); } catch (e) { throw new errors.FailedToCaptureScreenshot(e.message); } diff --git a/x-pack/plugins/screenshotting/server/screenshots/open_url.ts b/x-pack/plugins/screenshotting/server/screenshots/open_url.ts index c557374ff98768..bdf8c678eb1d2d 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/open_url.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/open_url.ts @@ -5,33 +5,39 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Headers, Logger } from '@kbn/core/server'; -import type { HeadlessChromiumDriver } from '../browsers'; -import type { Context } from '../browsers'; +import type { Headers } from '@kbn/core/server'; +import type { Context, HeadlessChromiumDriver } from '../browsers'; import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; +import { Actions, EventLogger } from './event_logger'; export const openUrl = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, timeout: number, index: number, url: string, context: Context, headers: Headers ): Promise => { + const { kbnLogger } = eventLogger; + const spanEnd = eventLogger.logScreenshottingEvent('open url', Actions.OPEN_URL, 'wait'); + // If we're moving to another page in the app, we'll want to wait for the app to tell us // it's loaded the next page. const page = index + 1; const waitForSelector = page > 1 ? `[data-shared-page="${page}"]` : DEFAULT_PAGELOAD_SELECTOR; - const span = apm.startSpan('open_url', 'wait'); try { - await browser.open(url, { context, headers, waitForSelector, timeout }, logger); + await browser.open(url, { context, headers, waitForSelector, timeout }, kbnLogger); } catch (err) { - logger.error(err); - throw new Error(`An error occurred when trying to open the Kibana URL: ${err.message}`); + kbnLogger.error(err); + + const newError = new Error( + `An error occurred when trying to open the Kibana URL: ${err.message}` + ); + eventLogger.error(newError, Actions.OPEN_URL); + throw newError; } - span?.end(); + spanEnd(); }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts b/x-pack/plugins/screenshotting/server/screenshots/semaphore/index.test.ts similarity index 98% rename from x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts rename to x-pack/plugins/screenshotting/server/screenshots/semaphore/index.test.ts index 6d6dd213479744..0cc40a83723a95 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/semaphore/index.test.ts @@ -6,7 +6,7 @@ */ import { TestScheduler } from 'rxjs/testing'; -import { Semaphore } from './semaphore'; +import { Semaphore } from '.'; describe('Semaphore', () => { let testScheduler: TestScheduler; diff --git a/x-pack/plugins/screenshotting/server/screenshots/semaphore.ts b/x-pack/plugins/screenshotting/server/screenshots/semaphore/index.ts similarity index 100% rename from x-pack/plugins/screenshotting/server/screenshots/semaphore.ts rename to x-pack/plugins/screenshotting/server/screenshots/semaphore/index.ts diff --git a/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts b/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts index cee23616faeacc..8cf8174be152fc 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts @@ -5,21 +5,22 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_WAITFORRENDER } from './constants'; +import { Actions, EventLogger } from './event_logger'; export const waitForRenderComplete = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, loadDelay: number, layout: Layout ) => { - const span = apm.startSpan('wait_for_render', 'wait'); - - logger.debug('waiting for rendering to complete'); + const spanEnd = eventLogger.logScreenshottingEvent( + 'wait for render complete', + Actions.WAIT_RENDER, + 'wait' + ); return await browser .evaluate( @@ -66,11 +67,9 @@ export const waitForRenderComplete = async ( args: [layout.selectors.renderComplete, loadDelay], }, { context: CONTEXT_WAITFORRENDER }, - logger + eventLogger.kbnLogger ) .then(() => { - logger.debug('rendering is complete'); - - span?.end(); + spanEnd(); }); }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts b/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts index a7485545cdef00..cf49fbe7dc7984 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts @@ -5,11 +5,10 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; +import { Actions, EventLogger } from './event_logger'; interface CompletedItemsCountParameters { context: string; @@ -37,15 +36,21 @@ const getCompletedItemsCount = ({ */ export const waitForVisualizations = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, timeout: number, toEqual: number, layout: Layout ): Promise => { - const span = apm.startSpan('wait_for_visualizations', 'wait'); + const { kbnLogger } = eventLogger; + const spanEnd = eventLogger.logScreenshottingEvent( + 'waiting for each visualization to complete rendering', + Actions.WAIT_VISUALIZATIONS, + 'wait' + ); + const { renderComplete: renderCompleteSelector } = layout.selectors; - logger.debug(`waiting for ${toEqual} rendered elements to be in the DOM`); + kbnLogger.debug(`waiting for ${toEqual} rendered elements to be in the DOM`); try { await browser.waitFor({ @@ -54,13 +59,15 @@ export const waitForVisualizations = async ( timeout, }); - logger.debug(`found ${toEqual} rendered elements in the DOM`); + kbnLogger.debug(`found ${toEqual} rendered elements in the DOM`); } catch (err) { - logger.error(err); - throw new Error( + kbnLogger.error(err); + const newError = new Error( `An error occurred when trying to wait for ${toEqual} visualizations to finish rendering. ${err.message}` ); + eventLogger.error(newError, Actions.WAIT_VISUALIZATIONS); + throw newError; } - span?.end(); + spanEnd(); }; From 8aa9241a8a3a990d374cee4dc3305ebe1eb9eeea Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Fri, 6 May 2022 22:39:36 +0200 Subject: [PATCH 73/83] Optimize package installation performance, phase 2 (#131627) --- .../elasticsearch/ingest_pipeline/index.ts | 2 +- .../elasticsearch/ingest_pipeline/install.ts | 87 ++++---- .../elasticsearch/template/install.test.ts | 140 ++---------- .../epm/elasticsearch/template/install.ts | 204 ++++++++---------- .../fleet/server/services/epm/fields/field.ts | 6 +- .../services/epm/kibana/assets/install.ts | 1 + .../services/epm/packages/_install_package.ts | 52 +++-- .../server/services/epm/packages/assets.ts | 4 +- 8 files changed, 196 insertions(+), 300 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts index 574534290214ae..5f093a19157f97 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts @@ -5,6 +5,6 @@ * 2.0. */ -export { installPipelines, isTopLevelPipeline } from './install'; +export { prepareToInstallPipelines, isTopLevelPipeline } from './install'; export { deletePreviousPipelines, deletePipeline } from './remove'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index 49dae4d86b6395..da035a44c99214 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -6,13 +6,12 @@ */ import type { TransportRequestOptions } from '@elastic/elasticsearch'; -import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { ElasticsearchAssetType } from '../../../../types'; import type { EsAssetReference, RegistryDataStream, InstallablePackage } from '../../../../types'; import { getAsset, getPathParts } from '../../archive'; import type { ArchiveEntry } from '../../archive'; -import { updateEsAssetReferences } from '../../packages/install'; import { FLEET_FINAL_PIPELINE_CONTENT, FLEET_FINAL_PIPELINE_ID, @@ -36,23 +35,23 @@ export const isTopLevelPipeline = (path: string) => { ); }; -export const installPipelines = async ( +export const prepareToInstallPipelines = ( installablePackage: InstallablePackage, - paths: string[], - esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract, - logger: Logger, - esReferences: EsAssetReference[] -) => { + paths: string[] +): { + assetsToAdd: EsAssetReference[]; + install: (esClient: ElasticsearchClient, logger: Logger) => Promise; +} => { // unlike other ES assets, pipeline names are versioned so after a template is updated // it can be created pointing to the new template, without removing the old one and effecting data // so do not remove the currently installed pipelines here const dataStreams = installablePackage.data_streams; - const { name: pkgName, version: pkgVersion } = installablePackage; + const { version: pkgVersion } = installablePackage; const pipelinePaths = paths.filter((path) => isPipeline(path)); const topLevelPipelinePaths = paths.filter((path) => isTopLevelPipeline(path)); - if (!dataStreams?.length && topLevelPipelinePaths.length === 0) return []; + if (!dataStreams?.length && topLevelPipelinePaths.length === 0) + return { assetsToAdd: [], install: () => Promise.resolve() }; // get and save pipeline refs before installing pipelines let pipelineRefs = dataStreams @@ -85,41 +84,41 @@ export const installPipelines = async ( pipelineRefs = [...pipelineRefs, ...topLevelPipelineRefs]; - esReferences = await updateEsAssetReferences(savedObjectsClient, pkgName, esReferences, { + return { assetsToAdd: pipelineRefs, - }); - - const pipelines = dataStreams - ? dataStreams.reduce>>((acc, dataStream) => { - if (dataStream.ingest_pipeline) { - acc.push( - installAllPipelines({ - dataStream, - esClient, - logger, - paths: pipelinePaths, - installablePackage, - }) - ); - } - return acc; - }, []) - : []; - - if (topLevelPipelinePaths) { - pipelines.push( - installAllPipelines({ - dataStream: undefined, - esClient, - logger, - paths: topLevelPipelinePaths, - installablePackage, - }) - ); - } + install: async (esClient, logger) => { + const pipelines = dataStreams + ? dataStreams.reduce>>((acc, dataStream) => { + if (dataStream.ingest_pipeline) { + acc.push( + installAllPipelines({ + dataStream, + esClient, + logger, + paths: pipelinePaths, + installablePackage, + }) + ); + } + return acc; + }, []) + : []; + + if (topLevelPipelinePaths) { + pipelines.push( + installAllPipelines({ + dataStream: undefined, + esClient, + logger, + paths: topLevelPipelinePaths, + installablePackage, + }) + ); + } - await Promise.all(pipelines); - return esReferences; + await Promise.all(pipelines); + }, + }; }; export function rewriteIngestPipeline( diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts index 998d0f9fb1ae57..3478da69bf7212 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts @@ -4,30 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { loggerMock } from '@kbn/logging-mocks'; - import { createAppContextStartContractMock } from '../../../../mocks'; import { appContextService } from '../../..'; import type { RegistryDataStream } from '../../../../types'; -import type { Field } from '../../fields/field'; -import { installTemplate } from './install'; +import { prepareTemplate } from './install'; -describe('EPM install', () => { +describe('EPM index template install', () => { beforeEach(async () => { appContextService.start(createAppContextStartContractMock()); }); - it('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix not set', async () => { - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - esClient.indices.getIndexTemplate.mockImplementation(() => - elasticsearchServiceMock.createSuccessTransportRequestPromise({ index_templates: [] }) - ); - - const fields: Field[] = []; + it('tests prepareTemplate to use correct priority and index_patterns for data stream with dataset_is_prefix not set', async () => { const dataStreamDatasetIsPrefixUnset = { type: 'metrics', dataset: 'package.dataset', @@ -43,29 +32,14 @@ describe('EPM install', () => { }; const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; const templatePriorityDatasetIsPrefixUnset = 200; - await installTemplate({ - esClient, - logger: loggerMock.create(), - fields, - dataStream: dataStreamDatasetIsPrefixUnset, - packageVersion: pkg.version, - packageName: pkg.name, - }); - - const sentTemplate = ( - esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest - ).body; - - expect(sentTemplate).toBeDefined(); - expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixUnset); - expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); + const { + indexTemplate: { indexTemplate }, + } = prepareTemplate({ pkg, dataStream: dataStreamDatasetIsPrefixUnset }); + expect(indexTemplate.priority).toBe(templatePriorityDatasetIsPrefixUnset); + expect(indexTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); }); - it('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to false', async () => { - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - esClient.indices.getIndexTemplate.mockResponse({ index_templates: [] }); - - const fields: Field[] = []; + it('tests prepareTemplate to use correct priority and index_patterns for data stream with dataset_is_prefix set to false', async () => { const dataStreamDatasetIsPrefixFalse = { type: 'metrics', dataset: 'package.dataset', @@ -82,29 +56,15 @@ describe('EPM install', () => { }; const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*'; const templatePriorityDatasetIsPrefixFalse = 200; - await installTemplate({ - esClient, - logger: loggerMock.create(), - fields, - dataStream: dataStreamDatasetIsPrefixFalse, - packageVersion: pkg.version, - packageName: pkg.name, - }); - - const sentTemplate = ( - esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest - ).body; + const { + indexTemplate: { indexTemplate }, + } = prepareTemplate({ pkg, dataStream: dataStreamDatasetIsPrefixFalse }); - expect(sentTemplate).toBeDefined(); - expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixFalse); - expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixFalse]); + expect(indexTemplate.priority).toBe(templatePriorityDatasetIsPrefixFalse); + expect(indexTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixFalse]); }); - it('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to true', async () => { - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - esClient.indices.getIndexTemplate.mockResponse({ index_templates: [] }); - - const fields: Field[] = []; + it('tests prepareTemplate to use correct priority and index_patterns for data stream with dataset_is_prefix set to true', async () => { const dataStreamDatasetIsPrefixTrue = { type: 'metrics', dataset: 'package.dataset', @@ -121,71 +81,11 @@ describe('EPM install', () => { }; const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*'; const templatePriorityDatasetIsPrefixTrue = 150; - await installTemplate({ - esClient, - logger: loggerMock.create(), - fields, - dataStream: dataStreamDatasetIsPrefixTrue, - packageVersion: pkg.version, - packageName: pkg.name, - }); - - const sentTemplate = ( - esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest - ).body; - - expect(sentTemplate).toBeDefined(); - expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixTrue); - expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixTrue]); - }); - - it('tests installPackage remove the aliases property if the property existed', async () => { - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - - esClient.indices.getIndexTemplate.mockResponse({ - index_templates: [ - { - name: 'metrics-package.dataset', - // @ts-expect-error not full interface - index_template: { - index_patterns: ['metrics-package.dataset-*'], - template: { aliases: {} }, - }, - }, - ], - }); - - const fields: Field[] = []; - const dataStreamDatasetIsPrefixUnset = { - type: 'metrics', - dataset: 'package.dataset', - title: 'test data stream', - release: 'experimental', - package: 'package', - path: 'path', - ingest_pipeline: 'default', - } as RegistryDataStream; - const pkg = { - name: 'package', - version: '0.0.1', - }; - const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; - const templatePriorityDatasetIsPrefixUnset = 200; - await installTemplate({ - esClient, - logger: loggerMock.create(), - fields, - dataStream: dataStreamDatasetIsPrefixUnset, - packageVersion: pkg.version, - packageName: pkg.name, - }); + const { + indexTemplate: { indexTemplate }, + } = prepareTemplate({ pkg, dataStream: dataStreamDatasetIsPrefixTrue }); - const sentTemplate = ( - esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest - ).body; - expect(sentTemplate).toBeDefined(); - expect(sentTemplate?.template?.aliases).not.toBeDefined(); - expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixUnset); - expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); + expect(indexTemplate.priority).toBe(templatePriorityDatasetIsPrefixTrue); + expect(indexTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixTrue]); }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 2d2e5b2ffea2a3..df6d9d84a08c5e 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -7,7 +7,7 @@ import { merge } from 'lodash'; import Boom from '@hapi/boom'; -import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { ElasticsearchAssetType } from '../../../../types'; import type { @@ -20,13 +20,12 @@ import type { TemplateMapEntry, TemplateMap, EsAssetReference, + PackageInfo, } from '../../../../types'; import { loadFieldsFromYaml, processFields } from '../../fields/field'; -import type { Field } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; import { getAsset, getPathParts } from '../../archive'; -import { updateEsAssetReferences } from '../../packages/install'; import { FLEET_COMPONENT_TEMPLATES, PACKAGE_TEMPLATE_SUFFIX, @@ -47,65 +46,55 @@ import { buildDefaultSettings } from './default_settings'; const FLEET_COMPONENT_TEMPLATE_NAMES = FLEET_COMPONENT_TEMPLATES.map((tmpl) => tmpl.name); -export const installTemplates = async ( +export const prepareToInstallTemplates = ( installablePackage: InstallablePackage, - esClient: ElasticsearchClient, - logger: Logger, paths: string[], - savedObjectsClient: SavedObjectsClientContract, esReferences: EsAssetReference[] -): Promise<{ - installedTemplates: IndexTemplateEntry[]; - installedEsReferences: EsAssetReference[]; -}> => { - // install any pre-built index template assets, - // atm, this is only the base package's global index templates - // Install component templates first, as they are used by the index templates - await installPreBuiltComponentTemplates(paths, esClient, logger); - await installPreBuiltTemplates(paths, esClient, logger); - +): { + assetsToAdd: EsAssetReference[]; + assetsToRemove: EsAssetReference[]; + install: (esClient: ElasticsearchClient, logger: Logger) => Promise; +} => { // remove package installation's references to index templates - esReferences = await updateEsAssetReferences( - savedObjectsClient, - installablePackage.name, - esReferences, - { - assetsToRemove: esReferences.filter( - ({ type }) => - type === ElasticsearchAssetType.indexTemplate || - type === ElasticsearchAssetType.componentTemplate - ), - } + const assetsToRemove = esReferences.filter( + ({ type }) => + type === ElasticsearchAssetType.indexTemplate || + type === ElasticsearchAssetType.componentTemplate ); // build templates per data stream from yml files const dataStreams = installablePackage.data_streams; - if (!dataStreams) return { installedTemplates: [], installedEsReferences: esReferences }; - - const installedTemplatesNested = await Promise.all( - dataStreams.map((dataStream) => - installTemplateForDataStream({ - pkg: installablePackage, - esClient, - logger, - dataStream, - }) - ) - ); - const installedTemplates = installedTemplatesNested.flat(); + if (!dataStreams) return { assetsToAdd: [], assetsToRemove, install: () => Promise.resolve([]) }; - // get template refs to save - const installedIndexTemplateRefs = getAllTemplateRefs(installedTemplates); - - // add package installation's references to index templates - esReferences = await updateEsAssetReferences( - savedObjectsClient, - installablePackage.name, - esReferences, - { assetsToAdd: installedIndexTemplateRefs } + const templates = dataStreams.map((dataStream) => + prepareTemplate({ pkg: installablePackage, dataStream }) ); + const assetsToAdd = getAllTemplateRefs(templates.map((template) => template.indexTemplate)); + + return { + assetsToAdd, + assetsToRemove, + install: async (esClient, logger) => { + // install any pre-built index template assets, + // atm, this is only the base package's global index templates + // Install component templates first, as they are used by the index templates + await installPreBuiltComponentTemplates(paths, esClient, logger); + await installPreBuiltTemplates(paths, esClient, logger); + + await Promise.all( + templates.map((template) => + installComponentAndIndexTemplateForDataStream({ + esClient, + logger, + componentTemplates: template.componentTemplates, + indexTemplate: template.indexTemplate, + }) + ) + ); - return { installedTemplates, installedEsReferences: esReferences }; + return templates.map((template) => template.indexTemplate); + }, + }; }; const installPreBuiltTemplates = async ( @@ -187,31 +176,24 @@ const isComponentTemplate = (path: string) => { }; /** - * installTemplateForDataStream installs one template for each data stream + * installComponentAndIndexTemplateForDataStream installs one template for each data stream * * The template is currently loaded with the pkgkey-package-data_stream */ -export async function installTemplateForDataStream({ - pkg, +export async function installComponentAndIndexTemplateForDataStream({ esClient, logger, - dataStream, + componentTemplates, + indexTemplate, }: { - pkg: InstallablePackage; esClient: ElasticsearchClient; logger: Logger; - dataStream: RegistryDataStream; -}): Promise { - const fields = await loadFieldsFromYaml(pkg, dataStream.path); - return installTemplate({ - esClient, - logger, - fields, - dataStream, - packageVersion: pkg.version, - packageName: pkg.name, - }); + componentTemplates: TemplateMap; + indexTemplate: IndexTemplateEntry; +}) { + await installDataStreamComponentTemplates({ esClient, logger, componentTemplates }); + await installTemplate({ esClient, logger, template: indexTemplate }); } function putComponentTemplate( @@ -291,35 +273,18 @@ function buildComponentTemplates(params: { return templatesMap; } -async function installDataStreamComponentTemplates(params: { - mappings: IndexTemplateMappings; - templateName: string; - registryElasticsearch: RegistryElasticsearch | undefined; +async function installDataStreamComponentTemplates({ + esClient, + logger, + componentTemplates, +}: { esClient: ElasticsearchClient; logger: Logger; - packageName: string; - defaultSettings: IndexTemplate['template']['settings']; + componentTemplates: TemplateMap; }) { - const { - templateName, - registryElasticsearch, - esClient, - packageName, - defaultSettings, - logger, - mappings, - } = params; - const componentTemplates = buildComponentTemplates({ - mappings, - templateName, - registryElasticsearch, - packageName, - defaultSettings, - }); - const templateEntries = Object.entries(componentTemplates); // TODO: Check return values for errors await Promise.all( - templateEntries.map(async ([name, body]) => { + Object.entries(componentTemplates).map(async ([name, body]) => { if (isUserSettingsTemplate(name)) { try { // Attempt to create custom component templates, ignore if they already exist @@ -342,8 +307,6 @@ async function installDataStreamComponentTemplates(params: { } }) ); - - return { componentTemplateNames: Object.keys(componentTemplates) }; } export async function ensureDefaultComponentTemplates( @@ -387,21 +350,15 @@ export async function ensureComponentTemplate( return { isCreated: !existingTemplate }; } -export async function installTemplate({ - esClient, - logger, - fields, +export function prepareTemplate({ + pkg, dataStream, - packageVersion, - packageName, }: { - esClient: ElasticsearchClient; - logger: Logger; - fields: Field[]; + pkg: Pick; dataStream: RegistryDataStream; - packageVersion: string; - packageName: string; -}): Promise { +}): { componentTemplates: TemplateMap; indexTemplate: IndexTemplateEntry } { + const { name: packageName, version: packageVersion } = pkg; + const fields = loadFieldsFromYaml(pkg, dataStream.path); const validFields = processFields(fields); const mappings = generateMappings(validFields); const templateName = generateTemplateName(dataStream); @@ -425,40 +382,51 @@ export async function installTemplate({ ilmPolicy: dataStream.ilm_policy, }); - const { componentTemplateNames } = await installDataStreamComponentTemplates({ + const componentTemplates = buildComponentTemplates({ + defaultSettings, mappings, + packageName, templateName, registryElasticsearch: dataStream.elasticsearch, - esClient, - logger, - packageName, - defaultSettings, }); const template = getTemplate({ templateIndexPattern, pipelineName, packageName, - composedOfTemplates: componentTemplateNames, + composedOfTemplates: Object.keys(componentTemplates), templatePriority, hidden: dataStream.hidden, }); + return { + componentTemplates, + indexTemplate: { + templateName, + indexTemplate: template, + }, + }; +} + +async function installTemplate({ + esClient, + logger, + template, +}: { + esClient: ElasticsearchClient; + logger: Logger; + template: IndexTemplateEntry; +}) { // TODO: Check return values for errors const esClientParams = { - name: templateName, - body: template, + name: template.templateName, + body: template.indexTemplate, }; await retryTransientEsErrors( () => esClient.indices.putIndexTemplate(esClientParams, { ignore: [404] }), { logger } ); - - return { - templateName, - indexTemplate: template, - }; } export function getAllTemplateRefs(installedTemplates: IndexTemplateEntry[]) { diff --git a/x-pack/plugins/fleet/server/services/epm/fields/field.ts b/x-pack/plugins/fleet/server/services/epm/fields/field.ts index 3f1a8d8b2b7baa..0e00840b0c74ec 100644 --- a/x-pack/plugins/fleet/server/services/epm/fields/field.ts +++ b/x-pack/plugins/fleet/server/services/epm/fields/field.ts @@ -261,12 +261,12 @@ const isFields = (path: string) => { * Gets all field files, optionally filtered by dataset, extracts .yml files, merges them together */ -export const loadFieldsFromYaml = async ( +export const loadFieldsFromYaml = ( pkg: Pick, datasetName?: string -): Promise => { +): Field[] => { // Fetch all field definition files - const fieldDefinitionFiles = await getAssetsData(pkg, isFields, datasetName); + const fieldDefinitionFiles = getAssetsData(pkg, isFields, datasetName); return fieldDefinitionFiles.reduce((acc, file) => { // Make sure it is defined as it is optional. Should never happen. if (file.buffer) { diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index 1462cd61c4bd38..b9582ce1cf148c 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -267,6 +267,7 @@ export async function installKibanaSavedObjects({ overwrite: true, readStream: createListStream(toBeSavedObjects), createNewCopies: false, + refresh: false, }) ); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 24c324e6b7cd00..0124bff41736fa 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -22,10 +22,10 @@ import { import type { InstallablePackage, InstallSource, PackageAssetReference } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import type { AssetReference, Installation, InstallType } from '../../../types'; -import { installTemplates } from '../elasticsearch/template/install'; +import { prepareToInstallTemplates } from '../elasticsearch/template/install'; import { removeLegacyTemplates } from '../elasticsearch/template/remove_legacy'; import { - installPipelines, + prepareToInstallPipelines, isTopLevelPipeline, deletePreviousPipelines, } from '../elasticsearch/ingest_pipeline'; @@ -39,7 +39,7 @@ import { saveArchiveEntries } from '../archive/storage'; import { ConcurrentInstallOperationError } from '../../../errors'; import { packagePolicyService } from '../..'; -import { createInstallation } from './install'; +import { createInstallation, updateEsAssetReferences } from './install'; import { withPackageSpan } from './utils'; // this is only exported for testing @@ -146,17 +146,45 @@ export async function _installPackage({ installMlModel(packageInfo, paths, esClient, savedObjectsClient, logger, esReferences) ); - // installs versionized pipelines without removing currently installed ones - esReferences = await withPackageSpan('Install ingest pipelines', () => - installPipelines(packageInfo, paths, esClient, savedObjectsClient, logger, esReferences) + /** + * In order to install assets in parallel, we need to split the preparation step from the installation step. This + * allows us to know which asset references are going to be installed so that we can save them on the packages + * SO before installation begins. In the case of a failure during installing any individual asset, we'll have the + * references necessary to remove any assets in that were successfully installed during the rollback phase. + * + * This split of prepare/install could be extended to all asset types. Besides performance, it also allows us to + * more easily write unit tests against the asset generation code without needing to mock ES responses. + */ + const preparedIngestPipelines = prepareToInstallPipelines(packageInfo, paths); + const preparedIndexTemplates = prepareToInstallTemplates(packageInfo, paths, esReferences); + + // Update the references for the templates and ingest pipelines together. Need to be done togther to avoid race + // conditions on updating the installed_es field at the same time + // These must be saved before we actually attempt to install the templates or pipelines so that we know what to + // cleanup in the case that a single asset fails to install. + esReferences = await updateEsAssetReferences( + savedObjectsClient, + packageInfo.name, + esReferences, + { + assetsToRemove: preparedIndexTemplates.assetsToRemove, + assetsToAdd: [ + ...preparedIngestPipelines.assetsToAdd, + ...preparedIndexTemplates.assetsToAdd, + ], + } ); - // install or update the templates referencing the newly installed pipelines - const { installedTemplates, installedEsReferences: esReferencesAfterTemplates } = - await withPackageSpan('Install index templates', () => - installTemplates(packageInfo, esClient, logger, paths, savedObjectsClient, esReferences) - ); - esReferences = esReferencesAfterTemplates; + // Install index templates and ingest pipelines in parallel since they typically take the longest + const [installedTemplates] = await Promise.all([ + withPackageSpan('Install index templates', () => + preparedIndexTemplates.install(esClient, logger) + ), + // installs versionized pipelines without removing currently installed ones + withPackageSpan('Install ingest pipelines', () => + preparedIngestPipelines.install(esClient, logger) + ), + ]); try { await removeLegacyTemplates({ packageInfo, esClient, logger }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts index 0621d05d21497b..d67e76f90e551a 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts @@ -51,11 +51,11 @@ export function getAssets( // ASK: Does getAssetsData need an installSource now? // if so, should it be an Installation vs InstallablePackage or add another argument? -export async function getAssetsData( +export function getAssetsData( packageInfo: Pick, filter = (path: string): boolean => true, datasetName?: string -): Promise { +): ArchiveEntry[] { // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); const entries: ArchiveEntry[] = assets.map((path) => { From 19298ee3e6b39d832bd68170f9b9071459193bcf Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Fri, 6 May 2022 14:24:32 -0700 Subject: [PATCH 74/83] [RAM] Add shareable rule tag filter (#130710) * rule tag filter * lower case test subj * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Tag filter and aggregation APIs * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Fix and add more tests * Fix test * Fix test and add new function test * Addressed comments * Lint * Create new load tags function * bump bundle size Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-optimizer/limits.yml | 2 +- x-pack/plugins/alerting/common/rule.ts | 1 + .../server/routes/aggregate_rules.test.ts | 7 + .../alerting/server/routes/aggregate_rules.ts | 2 + .../server/rules_client/rules_client.ts | 13 ++ .../rules_client/tests/aggregate.test.ts | 27 ++++ .../common/experimental_features.ts | 1 + .../rule_tag_filter_sandbox.tsx | 25 ++++ .../shareable_components_sandbox.tsx | 2 + .../lib/rule_api/aggregate.test.ts | 66 ++++++++- .../application/lib/rule_api/aggregate.ts | 23 +++ .../public/application/lib/rule_api/index.ts | 2 +- .../lib/rule_api/map_filters_to_kql.test.ts | 12 +- .../lib/rule_api/map_filters_to_kql.ts | 5 + .../application/lib/rule_api/rules.test.ts | 38 +++++ .../public/application/lib/rule_api/rules.ts | 3 + .../public/application/sections/index.tsx | 3 + .../components/rule_tag_filter.test.tsx | 77 ++++++++++ .../rules_list/components/rule_tag_filter.tsx | 133 ++++++++++++++++++ .../rules_list/components/rules_list.test.tsx | 44 +++++- .../rules_list/components/rules_list.tsx | 44 +++++- .../common/get_experimental_features.test.tsx | 5 + .../public/common/get_rule_tag_filter.tsx | 14 ++ .../triggers_actions_ui/public/mocks.ts | 4 + .../triggers_actions_ui/public/plugin.ts | 6 + .../triggers_actions_ui/public/types.ts | 2 + .../spaces_only/tests/alerting/aggregate.ts | 6 + .../apps/triggers_actions_ui/alerts_list.ts | 83 +++++++++-- .../apps/triggers_actions_ui/index.ts | 1 + .../triggers_actions_ui/rule_tag_filter.ts | 54 +++++++ x-pack/test/functional_with_es_ssl/config.ts | 1 + 31 files changed, 685 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx create mode 100644 x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 9f73dcd620d300..9baed7a92a53e2 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -57,7 +57,7 @@ pageLoadAssetSize: telemetry: 51957 telemetryManagementSection: 38586 transform: 41007 - triggersActionsUi: 103400 + triggersActionsUi: 104400 upgradeAssistant: 81241 urlForwarding: 32579 usageCollection: 39762 diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index c8f282bf695d79..4509a004c6e585 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -75,6 +75,7 @@ export interface RuleAggregations { ruleEnabledStatus: { enabled: number; disabled: number }; ruleMutedStatus: { muted: number; unmuted: number }; ruleSnoozedStatus: { snoozed: number }; + ruleTags: string[]; } export interface MappedParamsProperties { diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts index 7123f1bf4ad6c4..8c24b457df5656 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts +++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts @@ -60,6 +60,7 @@ describe('aggregateRulesRoute', () => { ruleSnoozedStatus: { snoozed: 4, }, + ruleTags: ['a', 'b', 'c'], }; rulesClient.aggregate.mockResolvedValueOnce(aggregateResult); @@ -94,6 +95,11 @@ describe('aggregateRulesRoute', () => { "rule_snoozed_status": Object { "snoozed": 4, }, + "rule_tags": Array [ + "a", + "b", + "c", + ], }, } `); @@ -129,6 +135,7 @@ describe('aggregateRulesRoute', () => { rule_snoozed_status: { snoozed: 4, }, + rule_tags: ['a', 'b', 'c'], }, }); }); diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts index 312def72dd65e4..c48c74fc287549 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts +++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts @@ -50,6 +50,7 @@ const rewriteBodyRes: RewriteResponseCase = ({ ruleEnabledStatus, ruleMutedStatus, ruleSnoozedStatus, + ruleTags, ...rest }) => ({ ...rest, @@ -57,6 +58,7 @@ const rewriteBodyRes: RewriteResponseCase = ({ rule_enabled_status: ruleEnabledStatus, rule_muted_status: ruleMutedStatus, rule_snoozed_status: ruleSnoozedStatus, + rule_tags: ruleTags, }); export const aggregateRulesRoute = ( diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 00f67437ae4f2d..e229b15fcd1cdc 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -133,6 +133,12 @@ export interface RuleAggregation { doc_count: number; }>; }; + tags: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; } export interface ConstructorOptions { @@ -200,6 +206,7 @@ export interface AggregateResult { ruleEnabledStatus?: { enabled: number; disabled: number }; ruleMutedStatus?: { muted: number; unmuted: number }; ruleSnoozedStatus?: { snoozed: number }; + ruleTags?: string[]; } export interface FindResult { @@ -921,6 +928,9 @@ export class RulesClient { muted: { terms: { field: 'alert.attributes.muteAll' }, }, + tags: { + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } }, + }, snoozed: { date_range: { field: 'alert.attributes.snoozeEndTime', @@ -990,6 +1000,9 @@ export class RulesClient { snoozed: snoozedBuckets.reduce((acc, bucket) => acc + bucket.doc_count, 0), }; + const tagsBuckets = resp.aggregations.tags?.buckets || []; + ret.ruleTags = tagsBuckets.map((bucket) => bucket.key); + return ret; } diff --git a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts index b74059e4be3d66..1a3d203162bd61 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts @@ -112,6 +112,22 @@ describe('aggregate()', () => { }, ], }, + tags: { + buckets: [ + { + key: 'a', + doc_count: 10, + }, + { + key: 'b', + doc_count: 20, + }, + { + key: 'c', + doc_count: 30, + }, + ], + }, }, }); @@ -160,6 +176,11 @@ describe('aggregate()', () => { "ruleSnoozedStatus": Object { "snoozed": 2, }, + "ruleTags": Array [ + "a", + "b", + "c", + ], } `); expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); @@ -187,6 +208,9 @@ describe('aggregate()', () => { ranges: [{ from: 'now' }], }, }, + tags: { + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } }, + }, }, }, ]); @@ -221,6 +245,9 @@ describe('aggregate()', () => { ranges: [{ from: 'now' }], }, }, + tags: { + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } }, + }, }, }, ]); diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts index 0a0b8cdeab208d..33f5fdc44afcde 100644 --- a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts +++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts @@ -15,6 +15,7 @@ export const allowedExperimentalValues = Object.freeze({ rulesListDatagrid: true, internalAlertsTable: false, internalShareableComponentsSandbox: false, + ruleTagFilter: false, ruleStatusFilter: false, rulesDetailLogs: true, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx new file mode 100644 index 00000000000000..58603fdb8f1782 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { getRuleTagFilterLazy } from '../../../common/get_rule_tag_filter'; + +export const RuleTagFilterSandbox = () => { + const [selectedTags, setSelectedTags] = useState([]); + + return ( +
+ {getRuleTagFilterLazy({ + tags: ['tag1', 'tag2', 'tag3', 'tag4'], + selectedTags, + onChange: setSelectedTags, + })} + +
selected tags: {JSON.stringify(selectedTags)}
+
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx index bedcbb03045a5e..668b1ccb5aa692 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { RuleStatusDropdownSandbox } from './rule_status_dropdown_sandbox'; +import { RuleTagFilterSandbox } from './rule_tag_filter_sandbox'; import { RuleStatusFilterSandbox } from './rule_status_filter_sandbox'; import { RuleTagBadgeSandbox } from './rule_tag_badge_sandbox'; @@ -14,6 +15,7 @@ export const InternalShareableComponentsSandbox: React.FC<{}> = () => { return ( <> + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts index ab8f1b565c8880..5377e4269f46e8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts @@ -6,7 +6,7 @@ */ import { httpServiceMock } from '@kbn/core/public/mocks'; -import { loadRuleAggregations } from './aggregate'; +import { loadRuleAggregations, loadRuleTags } from './aggregate'; const http = httpServiceMock.createStartContract(); @@ -289,4 +289,68 @@ describe('loadRuleAggregations', () => { ] `); }); + + test('should call aggregate API with tagsFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadRuleAggregations({ + http, + searchText: 'baz', + tagsFilter: ['a', 'b', 'c'], + }); + + expect(result).toEqual({ + ruleExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.tags:(a or b or c)", + "search": "baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + }, + }, + ] + `); + }); + + test('loadRuleTags should call the aggregate API with no filters', async () => { + const resolvedValue = { + rule_tags: ['a', 'b', 'c'], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadRuleTags({ + http, + }); + + expect(result).toEqual({ + ruleTags: ['a', 'b', 'c'], + }); + + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + ] + `); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts index 9548445d0df9c9..1df61774436572 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts @@ -10,11 +10,16 @@ import { RuleAggregations, RuleStatus } from '../../../types'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; import { mapFiltersToKql } from './map_filters_to_kql'; +export interface RuleTagsAggregations { + ruleTags: string[]; +} + const rewriteBodyRes: RewriteRequestCase = ({ rule_execution_status: ruleExecutionStatus, rule_enabled_status: ruleEnabledStatus, rule_muted_status: ruleMutedStatus, rule_snoozed_status: ruleSnoozedStatus, + rule_tags: ruleTags, ...rest }: any) => ({ ...rest, @@ -22,8 +27,23 @@ const rewriteBodyRes: RewriteRequestCase = ({ ruleEnabledStatus, ruleMutedStatus, ruleSnoozedStatus, + ruleTags, +}); + +const rewriteTagsBodyRes: RewriteRequestCase = ({ + rule_tags: ruleTags, +}: any) => ({ + ruleTags, }); +// TODO: https://github.com/elastic/kibana/issues/131682 +export async function loadRuleTags({ http }: { http: HttpSetup }): Promise { + const res = await http.get>( + `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate` + ); + return rewriteTagsBodyRes(res); +} + export async function loadRuleAggregations({ http, searchText, @@ -31,6 +51,7 @@ export async function loadRuleAggregations({ actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, }: { http: HttpSetup; searchText?: string; @@ -38,12 +59,14 @@ export async function loadRuleAggregations({ actionTypesFilter?: string[]; ruleExecutionStatusesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; + tagsFilter?: string[]; }): Promise { const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, }); const res = await http.get>( `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts index 89ede79f4a21da..c9834dd140ea4f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts @@ -7,7 +7,7 @@ export { alertingFrameworkHealth } from './health'; export { mapFiltersToKql } from './map_filters_to_kql'; -export { loadRuleAggregations } from './aggregate'; +export { loadRuleAggregations, loadRuleTags } from './aggregate'; export { createRule } from './create'; export { deleteRules } from './delete'; export { disableRule, disableRules } from './disable'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts index df762d05e0effb..f67a27ef5409cb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts @@ -88,6 +88,14 @@ describe('mapFiltersToKql', () => { ]); }); + test('should handle tagsFilter', () => { + expect( + mapFiltersToKql({ + tagsFilter: ['a', 'b', 'c'], + }) + ).toEqual(['alert.attributes.tags:(a or b or c)']); + }); + test('should handle typesFilter and actionTypesFilter', () => { expect( mapFiltersToKql({ @@ -100,17 +108,19 @@ describe('mapFiltersToKql', () => { ]); }); - test('should handle typesFilter, actionTypesFilter and ruleExecutionStatusesFilter', () => { + test('should handle typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, and tagsFilter', () => { expect( mapFiltersToKql({ typesFilter: ['type', 'filter'], actionTypesFilter: ['action', 'types', 'filter'], ruleExecutionStatusesFilter: ['alert', 'statuses', 'filter'], + tagsFilter: ['a', 'b', 'c'], }) ).toEqual([ 'alert.attributes.alertTypeId:(type or filter)', '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', 'alert.attributes.executionStatus.status:(alert or statuses or filter)', + 'alert.attributes.tags:(a or b or c)', ]); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts index 0e64f5500454f6..ff2a49e3a5e45f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts @@ -25,9 +25,11 @@ export const mapFiltersToKql = ({ actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, }: { typesFilter?: string[]; actionTypesFilter?: string[]; + tagsFilter?: string[]; ruleExecutionStatusesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; }): string[] => { @@ -68,6 +70,9 @@ export const mapFiltersToKql = ({ filters.push(`${enablementFilter} or ${snoozedFilter}`); } } + if (tagsFilter && tagsFilter.length) { + filters.push(`alert.attributes.tags:(${tagsFilter.join(' or ')})`); + } return filters; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts index 8adc92738b7c60..2a20c9d9469f5d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts @@ -336,4 +336,42 @@ describe('loadRules', () => { ] `); }); + + test('should call find API with tagsFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + const result = await loadRules({ + http, + tagsFilter: ['a', 'b', 'c'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.tags:(a or b or c)", + "page": 1, + "per_page": 10, + "search": undefined, + "search_fields": undefined, + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts index bdbdcf2f094b25..6e527989cc91f9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts @@ -23,6 +23,7 @@ export async function loadRules({ actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, sort = { field: 'name', direction: 'asc' }, }: { http: HttpSetup; @@ -30,6 +31,7 @@ export async function loadRules({ searchText?: string; typesFilter?: string[]; actionTypesFilter?: string[]; + tagsFilter?: string[]; ruleExecutionStatusesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; sort?: Sorting; @@ -42,6 +44,7 @@ export async function loadRules({ const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, + tagsFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index e41c2a73a51240..9ab31ae12402fe 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -32,6 +32,9 @@ export const ActionForm = suspendedComponentWithProps( export const RuleStatusDropdown = suspendedComponentWithProps( lazy(() => import('./rules_list/components/rule_status_dropdown')) ); +export const RuleTagFilter = suspendedComponentWithProps( + lazy(() => import('./rules_list/components/rule_tag_filter')) +); export const RuleStatusFilter = suspendedComponentWithProps( lazy(() => import('./rules_list/components/rule_status_filter')) ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx new file mode 100644 index 00000000000000..a6b60b10993919 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { EuiFilterButton, EuiSelectable } from '@elastic/eui'; +import { RuleTagFilter } from './rule_tag_filter'; + +const onChangeMock = jest.fn(); + +const tags = ['a', 'b', 'c', 'd', 'e', 'f']; + +describe('rule_tag_filter', () => { + beforeEach(() => { + onChangeMock.mockReset(); + }); + + it('renders correctly', () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find(EuiFilterButton).exists()).toBeTruthy(); + expect(wrapper.find('.euiNotificationBadge').text()).toEqual('0'); + }); + + it('can open the popover correctly', () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="ruleTagFilterSelectable"]').exists()).toBeFalsy(); + + wrapper.find(EuiFilterButton).simulate('click'); + + expect(wrapper.find('[data-test-subj="ruleTagFilterSelectable"]').exists()).toBeTruthy(); + expect(wrapper.find('li').length).toEqual(tags.length); + }); + + it('can select tags', () => { + const wrapper = mountWithIntl( + + ); + + wrapper.find(EuiFilterButton).simulate('click'); + + wrapper.find('[data-test-subj="ruleTagFilterOption-a"]').at(0).simulate('click'); + expect(onChangeMock).toHaveBeenCalledWith(['a']); + + wrapper.setProps({ + selectedTags: ['a'], + }); + + wrapper.find('[data-test-subj="ruleTagFilterOption-a"]').at(0).simulate('click'); + expect(onChangeMock).toHaveBeenCalledWith([]); + + wrapper.find('[data-test-subj="ruleTagFilterOption-b"]').at(0).simulate('click'); + expect(onChangeMock).toHaveBeenCalledWith(['a', 'b']); + }); + + it('renders selected tags even if they get deleted from the tags array', () => { + const selectedTags = ['g', 'h']; + const wrapper = mountWithIntl( + + ); + + wrapper.find(EuiFilterButton).simulate('click'); + + expect(wrapper.find(EuiSelectable).props().options.length).toEqual( + tags.length + selectedTags.length + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx new file mode 100644 index 00000000000000..6aa8aa8c692130 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useState, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiSelectable, + EuiFilterGroup, + EuiFilterButton, + EuiPopover, + EuiSelectableProps, + EuiSelectableOption, + EuiSpacer, +} from '@elastic/eui'; + +export interface RuleTagFilterProps { + tags: string[]; + selectedTags: string[]; + isLoading?: boolean; + loadingMessage?: EuiSelectableProps['loadingMessage']; + noMatchesMessage?: EuiSelectableProps['noMatchesMessage']; + emptyMessage?: EuiSelectableProps['emptyMessage']; + errorMessage?: EuiSelectableProps['errorMessage']; + dataTestSubj?: string; + selectableDataTestSubj?: string; + optionDataTestSubj?: (tag: string) => string; + buttonDataTestSubj?: string; + onChange: (tags: string[]) => void; +} + +const getOptionDataTestSubj = (tag: string) => `ruleTagFilterOption-${tag}`; + +export const RuleTagFilter = (props: RuleTagFilterProps) => { + const { + tags = [], + selectedTags = [], + isLoading = false, + loadingMessage, + noMatchesMessage, + emptyMessage, + errorMessage, + dataTestSubj = 'ruleTagFilter', + selectableDataTestSubj = 'ruleTagFilterSelectable', + optionDataTestSubj = getOptionDataTestSubj, + buttonDataTestSubj = 'ruleTagFilterButton', + onChange = () => {}, + } = props; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const allTags = useMemo(() => { + return [...new Set([...tags, ...selectedTags])].sort(); + }, [tags, selectedTags]); + + const options: EuiSelectableOption[] = useMemo( + () => + allTags.map((tag) => ({ + label: tag, + checked: selectedTags.includes(tag) ? 'on' : undefined, + 'data-test-subj': optionDataTestSubj(tag), + })), + [allTags, selectedTags, optionDataTestSubj] + ); + + const onChangeInternal = useCallback( + (newOptions: EuiSelectableOption[]) => { + const newSelectedTags = newOptions.reduce((result, option) => { + if (option.checked === 'on') { + result = [...result, option.label]; + } + return result; + }, []); + + onChange(newSelectedTags); + }, + [onChange] + ); + + const onClosePopover = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + + const renderButton = () => { + return ( + 0} + numActiveFilters={selectedTags.length} + numFilters={selectedTags.length} + onClick={onClosePopover} + > + + + ); + }; + + return ( + + + + {(list, search) => ( + <> + {search} + + {list} + + )} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { RuleTagFilter as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 52c6e2d3ed1497..12e1b0f1e4a6e3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -33,6 +33,7 @@ jest.mock('../../../lib/rule_api', () => ({ loadRules: jest.fn(), loadRuleTypes: jest.fn(), loadRuleAggregations: jest.fn(), + loadRuleTags: jest.fn(), alertingFrameworkHealth: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true, @@ -63,7 +64,10 @@ jest.mock('../../../lib/capabilities', () => ({ jest.mock('../../../../common/get_experimental_features', () => ({ getIsExperimentalFeatureEnabled: jest.fn(), })); -const { loadRules, loadRuleTypes, loadRuleAggregations } = + +const ruleTags = ['a', 'b', 'c', 'd']; + +const { loadRules, loadRuleTypes, loadRuleAggregations, loadRuleTags } = jest.requireMock('../../../lib/rule_api'); const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -395,6 +399,10 @@ describe('rules_list component with items', () => { ruleEnabledStatus: { enabled: 2, disabled: 0 }, ruleExecutionStatus: { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 }, ruleMutedStatus: { muted: 0, unmuted: 2 }, + ruleTags, + }); + loadRuleTags.mockResolvedValue({ + ruleTags, }); const ruleTypeMock: RuleTypeModel = { @@ -842,6 +850,40 @@ describe('rules_list component with items', () => { expect(loadRules.mock.calls[3][0].ruleStatusesFilter).toEqual(['enabled']); }); + + it('does not render the tag filter is the feature flag is off', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="ruleTagFilter"]').exists()).toBeFalsy(); + }); + + it('renders the tag filter if the experiment is on', async () => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + await setup(); + expect(wrapper.find('[data-test-subj="ruleTagFilter"]').exists()).toBeTruthy(); + }); + + it('can filter by tags', async () => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + loadRules.mockReset(); + await setup(); + + expect(loadRules.mock.calls[0][0].tagsFilter).toEqual([]); + + wrapper.find('[data-test-subj="ruleTagFilterButton"] button').simulate('click'); + + const tagFilterListItems = wrapper.find( + '[data-test-subj="ruleTagFilterSelectable"] .euiSelectableListItem' + ); + expect(tagFilterListItems.length).toEqual(ruleTags.length); + + tagFilterListItems.at(0).simulate('click'); + + expect(loadRules.mock.calls[1][0].tagsFilter).toEqual(['a']); + + tagFilterListItems.at(1).simulate('click'); + + expect(loadRules.mock.calls[2][0].tagsFilter).toEqual(['a', 'b']); + }); }); describe('rules_list component empty with show only capability', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index b1255600b68de0..a5b96618351310 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -73,6 +73,7 @@ import { RuleExecutionStatusFilter, getHealthColor } from './rule_execution_stat import { loadRules, loadRuleAggregations, + loadRuleTags, loadRuleTypes, disableRule, enableRule, @@ -99,6 +100,7 @@ import { RuleDurationFormat } from './rule_duration_format'; import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils'; import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils'; import { triggersActionsUiConfig } from '../../../../common/lib/config_api'; +import { RuleTagFilter } from './rule_tag_filter'; import { RuleStatusFilter } from './rule_status_filter'; import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; @@ -158,6 +160,8 @@ export const RulesList: React.FunctionComponent = () => { const [actionTypesFilter, setActionTypesFilter] = useState([]); const [ruleExecutionStatusesFilter, setRuleExecutionStatusesFilter] = useState([]); const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]); + const [tags, setTags] = useState([]); + const [tagsFilter, setTagsFilter] = useState([]); const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); @@ -167,6 +171,7 @@ export const RulesList: React.FunctionComponent = () => { ); const [showErrors, setShowErrors] = useState(false); + const isRuleTagFilterEnabled = getIsExperimentalFeatureEnabled('ruleTagFilter'); const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter'); useEffect(() => { @@ -233,6 +238,7 @@ export const RulesList: React.FunctionComponent = () => { JSON.stringify(actionTypesFilter), JSON.stringify(ruleExecutionStatusesFilter), JSON.stringify(ruleStatusesFilter), + JSON.stringify(tagsFilter), ]); useEffect(() => { @@ -293,8 +299,10 @@ export const RulesList: React.FunctionComponent = () => { actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, sort, }); + await loadRuleTagsAggs(); await loadRuleAggs(); setRulesState({ isLoading: false, @@ -311,7 +319,8 @@ export const RulesList: React.FunctionComponent = () => { isEmpty(typesFilter) && isEmpty(actionTypesFilter) && isEmpty(ruleExecutionStatusesFilter) && - isEmpty(ruleStatusesFilter) + isEmpty(ruleStatusesFilter) && + isEmpty(tagsFilter) ); setNoData(rulesResponse.data.length === 0 && !isFilterApplied); @@ -339,6 +348,7 @@ export const RulesList: React.FunctionComponent = () => { actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, }); if (rulesAggs?.ruleExecutionStatus) { setRulesStatusesTotal(rulesAggs.ruleExecutionStatus); @@ -355,6 +365,24 @@ export const RulesList: React.FunctionComponent = () => { } } + async function loadRuleTagsAggs() { + if (!isRuleTagFilterEnabled) { + return; + } + try { + const ruleTagsAggs = await loadRuleTags({ http }); + if (ruleTagsAggs?.ruleTags) { + setTags(ruleTagsAggs.ruleTags); + } + } catch (e) { + toasts.addDanger({ + title: i18n.translate('xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTags', { + defaultMessage: 'Unable to load rule tags', + }), + }); + } + } + const renderRuleStatusDropdown = (ruleEnabled: boolean | undefined, item: RuleTableItem) => { return ( { sortable: false, width: '50px', 'data-test-subj': 'rulesTableCell-tagsPopover', - render: (tags: string[], item: RuleTableItem) => { - return tags.length > 0 ? ( + render: (ruleTags: string[], item: RuleTableItem) => { + return ruleTags.length > 0 ? ( setTagPopoverOpenIndex(item.index)} onClose={() => setTagPopoverOpenIndex(-1)} /> @@ -940,6 +968,13 @@ export const RulesList: React.FunctionComponent = () => { ); }; + const getRuleTagFilter = () => { + if (isRuleTagFilterEnabled) { + return []; + } + return []; + }; + const getRuleStatusFilter = () => { if (isRuleStatusFilterEnabled) { return [ @@ -960,6 +995,7 @@ export const RulesList: React.FunctionComponent = () => { }) )} />, + ...getRuleTagFilter(), ...getRuleStatusFilter(), { rulesListDatagrid: true, internalAlertsTable: true, rulesDetailLogs: true, + ruleTagFilter: true, ruleStatusFilter: true, internalShareableComponentsSandbox: true, }, @@ -39,6 +40,10 @@ describe('getIsExperimentalFeatureEnabled', () => { expect(result).toEqual(true); + result = getIsExperimentalFeatureEnabled('ruleTagFilter'); + + expect(result).toEqual(true); + result = getIsExperimentalFeatureEnabled('ruleStatusFilter'); expect(result).toEqual(true); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx new file mode 100644 index 00000000000000..ccca277ef10bab --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { RuleTagFilter } from '../application/sections'; +import type { RuleTagFilterProps } from '../application/sections/rules_list/components/rule_tag_filter'; + +export const getRuleTagFilterLazy = (props: RuleTagFilterProps) => { + return ; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index cb79a1509a6c1a..003748f7d421e8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -26,6 +26,7 @@ import { } from './types'; import { getAlertsTableLazy } from './common/get_alerts_table'; import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown'; +import { getRuleTagFilterLazy } from './common/get_rule_tag_filter'; import { getRuleStatusFilterLazy } from './common/get_rule_status_filter'; import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge'; @@ -66,6 +67,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { getRuleStatusDropdown: (props) => { return getRuleStatusDropdownLazy(props); }, + getRuleTagFilter: (props) => { + return getRuleTagFilterLazy(props); + }, getRuleStatusFilter: (props) => { return getRuleStatusFilterLazy(props); }, diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 1d9c3c07e44ca1..c95dd73102fd9a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -31,6 +31,7 @@ import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout'; import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout'; import { getAlertsTableLazy } from './common/get_alerts_table'; import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown'; +import { getRuleTagFilterLazy } from './common/get_rule_tag_filter'; import { getRuleStatusFilterLazy } from './common/get_rule_status_filter'; import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge'; import { ExperimentalFeaturesService } from './common/experimental_features_service'; @@ -48,6 +49,7 @@ import type { ConnectorEditFlyoutProps, AlertsTableProps, RuleStatusDropdownProps, + RuleTagFilterProps, RuleStatusFilterProps, RuleTagBadgeProps, AlertsTableConfigurationRegistry, @@ -80,6 +82,7 @@ export interface TriggersAndActionsUIPublicPluginStart { ) => ReactElement; getAlertsTable: (props: AlertsTableProps) => ReactElement; getRuleStatusDropdown: (props: RuleStatusDropdownProps) => ReactElement; + getRuleTagFilter: (props: RuleTagFilterProps) => ReactElement; getRuleStatusFilter: (props: RuleStatusFilterProps) => ReactElement; getRuleTagBadge: (props: RuleTagBadgeProps) => ReactElement; } @@ -255,6 +258,9 @@ export class Plugin getRuleStatusDropdown: (props: RuleStatusDropdownProps) => { return getRuleStatusDropdownLazy(props); }, + getRuleTagFilter: (props: RuleTagFilterProps) => { + return getRuleTagFilterLazy(props); + }, getRuleStatusFilter: (props: RuleStatusFilterProps) => { return getRuleStatusFilterLazy(props); }, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 25efbfb6ecc38b..ef7ea7096961b2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -48,6 +48,7 @@ import { import { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common'; import { TypeRegistry } from './application/type_registry'; import type { ComponentOpts as RuleStatusDropdownProps } from './application/sections/rules_list/components/rule_status_dropdown'; +import type { RuleTagFilterProps } from './application/sections/rules_list/components/rule_tag_filter'; import type { RuleStatusFilterProps } from './application/sections/rules_list/components/rule_status_filter'; import type { RuleTagBadgeProps } from './application/sections/rules_list/components/rule_tag_badge'; @@ -82,6 +83,7 @@ export type { ResolvedRule, SanitizedRule, RuleStatusDropdownProps, + RuleTagFilterProps, RuleStatusFilterProps, RuleTagBadgeProps, }; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts index 588e7132f268c0..4424175e369532 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts @@ -44,6 +44,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) rule_snoozed_status: { snoozed: 0, }, + rule_tags: [], }); }); @@ -122,6 +123,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) rule_snoozed_status: { snoozed: 0, }, + rule_tags: ['foo'], }); }); @@ -137,6 +139,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) { rule_type_id: 'test.noop', schedule: { interval: '1s' }, + tags: ['a', 'b'], }, 'ok' ); @@ -153,6 +156,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) params: { pattern: { instance: new Array(100).fill(true) }, }, + tags: ['a', 'c', 'f'], }, 'active' ); @@ -166,6 +170,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) { rule_type_id: 'test.throw', schedule: { interval: '1s' }, + tags: ['b', 'c', 'd'], }, 'error' ); @@ -202,6 +207,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) ruleSnoozedStatus: { snoozed: 0, }, + ruleTags: ['a', 'b', 'c', 'd', 'f'], }); }); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index a921256a091485..a036c25e3d657d 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -32,6 +32,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } describe('rules list', function () { + const assertRulesLength = async (length: number) => { + return await retry.try(async () => { + const rules = await pageObjects.triggersActionsUI.getAlertsList(); + expect(rules.length).to.equal(length); + }); + }; + before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('rulesTab'); @@ -603,13 +610,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should filter alerts by the rule status', async () => { - const assertRulesLength = async (length: number) => { - return await retry.try(async () => { - const rules = await pageObjects.triggersActionsUI.getAlertsList(); - expect(rules.length).to.equal(length); - }); - }; - // Enabled alert await createAlert({ supertest, @@ -640,34 +640,93 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('ruleStatusFilterButton'); await testSubjects.click('ruleStatusFilterOption-enabled'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(1); // Select disabled await testSubjects.click('ruleStatusFilterOption-enabled'); await testSubjects.click('ruleStatusFilterOption-disabled'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(1); // Select snoozed await testSubjects.click('ruleStatusFilterOption-disabled'); await testSubjects.click('ruleStatusFilterOption-snoozed'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(1); // Select disabled and snoozed await testSubjects.click('ruleStatusFilterOption-disabled'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(2); // Select all 3 await testSubjects.click('ruleStatusFilterOption-enabled'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(3); }); + + it('should filter alerts by the tag', async () => { + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['a'], + }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['b'], + }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['a', 'b'], + }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['b', 'c'], + }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['c'], + }, + }); + + await refreshAlertsList(); + await testSubjects.click('ruleTagFilter'); + + // Select a -> selected: a + await testSubjects.click('ruleTagFilterOption-a'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(2); + + // Unselect a -> selected: none + await testSubjects.click('ruleTagFilterOption-a'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(5); + + // Select a, b -> selected: a, b + await testSubjects.click('ruleTagFilterOption-a'); + await testSubjects.click('ruleTagFilterOption-b'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(4); + + // Unselect a, b, select c -> selected: c + await testSubjects.click('ruleTagFilterOption-a'); + await testSubjects.click('ruleTagFilterOption-b'); + await testSubjects.click('ruleTagFilterOption-c'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(2); + }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index 73b084c2ce0e4d..3b2803e17e184f 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -16,6 +16,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./connectors')); loadTestFile(require.resolve('./alerts_table')); loadTestFile(require.resolve('./rule_status_dropdown')); + loadTestFile(require.resolve('./rule_tag_filter')); loadTestFile(require.resolve('./rule_status_filter')); loadTestFile(require.resolve('./rule_tag_badge')); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts new file mode 100644 index 00000000000000..77d57e2819db59 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const PageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); + const esArchiver = getService('esArchiver'); + + describe('Rule tag filter', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + await PageObjects.common.navigateToUrlWithBrowserHistory( + 'triggersActions', + '/__components_sandbox' + ); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + }); + + it('shoud load from shareable lazy loader', async () => { + await testSubjects.find('ruleTagFilter'); + const exists = await testSubjects.exists('ruleTagFilter'); + expect(exists).to.be(true); + }); + + it('should allow tag filters to be selected', async () => { + let badge = await find.byCssSelector('.euiFilterButton__notification'); + expect(await badge.getVisibleText()).to.be('0'); + + await testSubjects.click('ruleTagFilter'); + await testSubjects.click('ruleTagFilterOption-tag1'); + + badge = await find.byCssSelector('.euiFilterButton__notification'); + expect(await badge.getVisibleText()).to.be('1'); + + await testSubjects.click('ruleTagFilterOption-tag2'); + + badge = await find.byCssSelector('.euiFilterButton__notification'); + expect(await badge.getVisibleText()).to.be('2'); + + await testSubjects.click('ruleTagFilterOption-tag1'); + expect(await badge.getVisibleText()).to.be('1'); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 4872d2fd6fa38b..62984ace526fb4 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -76,6 +76,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.trigger_actions_ui.enableExperimental=${JSON.stringify([ 'internalAlertsTable', 'internalShareableComponentsSandbox', + 'ruleTagFilter', 'ruleStatusFilter', ])}`, `--xpack.alerting.rules.minimumScheduleInterval.value="2s"`, From c8e3fc57eddca46724d496200b89988b38c4853a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Sat, 7 May 2022 01:21:23 +0200 Subject: [PATCH 75/83] [EBT] Fix `userId` generation (#131701) --- x-pack/plugins/cloud/public/plugin.test.ts | 44 +++++++++++++++------- x-pack/plugins/cloud/public/plugin.tsx | 15 +++++++- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts index cfd0d456674175..36be9e590f216b 100644 --- a/x-pack/plugins/cloud/public/plugin.test.ts +++ b/x-pack/plugins/cloud/public/plugin.test.ts @@ -11,6 +11,7 @@ import { homePluginMock } from '@kbn/home-plugin/public/mocks'; import { securityMock } from '@kbn/security-plugin/public/mocks'; import { CloudPlugin, CloudConfigType } from './plugin'; import { firstValueFrom } from 'rxjs'; +import { Sha256 } from '@kbn/core/public/utils'; describe('Cloud Plugin', () => { describe('#setup', () => { @@ -74,6 +75,9 @@ describe('Cloud Plugin', () => { }); describe('setupTelemetryContext', () => { + const username = '1234'; + const expectedHashedPlainUsername = new Sha256().update(username, 'utf8').digest('hex'); + beforeEach(() => { jest.clearAllMocks(); }); @@ -121,9 +125,7 @@ describe('Cloud Plugin', () => { test('register the context provider for the cloud user with hashed user ID when security is available', async () => { const { coreSetup } = await setupPlugin({ config: { id: 'cloudId' }, - currentUserProps: { - username: '1234', - }, + currentUserProps: { username }, }); expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); @@ -140,9 +142,7 @@ describe('Cloud Plugin', () => { it('user hash includes cloud id', async () => { const { coreSetup: coreSetup1 } = await setupPlugin({ config: { id: 'esOrg1' }, - currentUserProps: { - username: '1234', - }, + currentUserProps: { username }, }); const [{ context$: context1$ }] = @@ -151,12 +151,11 @@ describe('Cloud Plugin', () => { )!; const hashId1 = await firstValueFrom(context1$); + expect(hashId1).not.toEqual(expectedHashedPlainUsername); const { coreSetup: coreSetup2 } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg2' }, - currentUserProps: { - username: '1234', - }, + currentUserProps: { username }, }); const [{ context$: context2$ }] = @@ -165,15 +164,17 @@ describe('Cloud Plugin', () => { )!; const hashId2 = await firstValueFrom(context2$); + expect(hashId2).not.toEqual(expectedHashedPlainUsername); expect(hashId1).not.toEqual(hashId2); }); - test('user hash does not include cloudId when not provided', async () => { + test('user hash does not include cloudId when authenticated via Cloud SAML', async () => { const { coreSetup } = await setupPlugin({ - config: {}, + config: { id: 'cloudDeploymentId' }, currentUserProps: { - username: '1234', + username, + authentication_realm: { type: 'saml', name: 'cloud-saml-kibana' }, }, }); @@ -184,7 +185,24 @@ describe('Cloud Plugin', () => { )!; await expect(firstValueFrom(context$)).resolves.toEqual({ - userId: '03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4', + userId: expectedHashedPlainUsername, + }); + }); + + test('user hash does not include cloudId when not provided', async () => { + const { coreSetup } = await setupPlugin({ + config: {}, + currentUserProps: { username }, + }); + + expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); + + const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find( + ([{ name }]) => name === 'cloud_user_id' + )!; + + await expect(firstValueFrom(context$)).resolves.toEqual({ + userId: expectedHashedPlainUsername, }); }); diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index db6b2305495bff..1bccf219225dc7 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -261,10 +261,21 @@ export class CloudPlugin implements Plugin { analytics.registerContextProvider({ name: 'cloud_user_id', context$: from(security.authc.getCurrentUser()).pipe( - map((user) => user.username), + map((user) => { + if ( + getIsCloudEnabled(cloudId) && + user.authentication_realm?.type === 'saml' && + user.authentication_realm?.name === 'cloud-saml-kibana' + ) { + // If authenticated via Cloud SAML, use the SAML username as the user ID + return user.username; + } + + return cloudId ? `${cloudId}:${user.username}` : user.username; + }), // Join the cloud org id and the user to create a truly unique user id. // The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs - map((userId) => ({ userId: sha256(cloudId ? `${cloudId}:${userId}` : `${userId}`) })), + map((userId) => ({ userId: sha256(userId) })), catchError(() => of({ userId: undefined })) ), schema: { From 45e5d08d27b1b557a6ca32c1d0a213efa842bfe1 Mon Sep 17 00:00:00 2001 From: Kfir Peled <61654899+kfirpeled@users.noreply.github.com> Date: Sun, 8 May 2022 18:59:16 +0300 Subject: [PATCH 76/83] [Cloud Posture] Enabled findings group by feature (#131780) --- x-pack/plugins/cloud_security_posture/common/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index b2edd268c84850..30e9651b6e739e 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -31,7 +31,7 @@ export const INTERNAL_FEATURE_FLAGS = { showBenchmarks: true, showManageRulesMock: false, showRisksMock: false, - showFindingsGroupBy: false, + showFindingsGroupBy: true, } as const; export const cspRuleAssetSavedObjectType = 'csp_rule'; From df16573e4edc07b54b17d48ca3e78a1072ca6153 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 9 May 2022 12:55:22 +0200 Subject: [PATCH 77/83] [Discover] Fix Document Explorer infinite height growth (#131723) * [Discover] Fix grid height for various screen sizes * [Discover] Move the callout out of grid container to improve the layout --- .../main/components/layout/discover_documents.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 955d69509cf01c..7b715bb56a74c7 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -156,9 +156,9 @@ function DiscoverDocumentsComponent({ )} {!isLegacy && ( -
- <> - + <> + +
- -
+
+ )} ); From 41706738583e97444c8e85a64419f7be241aff74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Mon, 9 May 2022 14:43:51 +0200 Subject: [PATCH 78/83] Cleanup unused dependencies (#130624) --- package.json | 14 -- packages/kbn-pm/dist/index.js | 214 +++++++++--------- packages/kbn-test-jest-helpers/BUILD.bazel | 2 - packages/kbn-test/BUILD.bazel | 2 - .../apis/mock_http_server.d.ts | 10 - yarn.lock | 146 +----------- 6 files changed, 117 insertions(+), 271 deletions(-) delete mode 100644 x-pack/test/fleet_api_integration/apis/mock_http_server.d.ts diff --git a/package.json b/package.json index 2f0a37ff477355..b0b21934009c5b 100644 --- a/package.json +++ b/package.json @@ -279,9 +279,7 @@ "https-proxy-agent": "^5.0.0", "i18n-iso-countries": "^4.3.1", "icalendar": "0.7.1", - "idx": "^2.5.6", "immer": "^9.0.6", - "inline-style": "^2.0.0", "inquirer": "^7.3.3", "intl": "^1.2.5", "intl-format-cache": "^2.1.0", @@ -415,7 +413,6 @@ "styled-components": "^5.1.0", "suricata-sid-db": "^1.0.2", "symbol-observable": "^1.2.0", - "tabbable": "1.1.3", "tar": "^6.1.11", "tinycolor2": "1.4.1", "tinygradient": "0.4.3", @@ -438,7 +435,6 @@ "vega-tooltip": "^0.28.0", "venn.js": "0.2.20", "vinyl": "^2.2.0", - "vt-pbf": "^3.1.1", "whatwg-fetch": "^3.0.0", "xml2js": "^0.4.22", "yauzl": "^2.10.0" @@ -715,7 +711,6 @@ "@types/opn": "^5.1.0", "@types/ora": "^1.3.5", "@types/papaparse": "^5.0.3", - "@types/parse-link-header": "^1.0.0", "@types/pbf": "3.0.2", "@types/pdfmake": "^0.1.19", "@types/pegjs": "^0.10.1", @@ -757,7 +752,6 @@ "@types/supertest": "^2.0.5", "@types/tapable": "^1.0.6", "@types/tar": "^4.0.5", - "@types/tar-fs": "^1.16.1", "@types/tempy": "^0.2.0", "@types/testing-library__jest-dom": "^5.14.3", "@types/tinycolor2": "^1.4.1", @@ -854,7 +848,6 @@ "file-loader": "^4.2.0", "form-data": "^4.0.0", "geckodriver": "^3.0.1", - "glob-watcher": "5.0.3", "gulp": "4.0.2", "gulp-babel": "^8.0.0", "gulp-brotli": "^3.0.0", @@ -894,13 +887,11 @@ "marge": "^1.0.1", "micromatch": "3.1.10", "minimist": "^1.2.6", - "mkdirp": "0.5.1", "mocha": "^9.1.0", "mocha-junit-reporter": "^2.0.2", "mochawesome": "^7.0.1", "mochawesome-merge": "^4.2.1", "mock-fs": "^5.1.2", - "mock-http-server": "1.3.0", "ms-chromium-edge-driver": "^0.5.1", "multimatch": "^4.0.0", "mutation-observer": "^1.0.3", @@ -910,10 +901,8 @@ "nyc": "^15.1.0", "oboe": "^2.1.4", "openapi-types": "^10.0.0", - "parse-link-header": "^1.0.1", "pbf": "3.2.1", "pirates": "^4.0.1", - "pixelmatch": "^5.1.0", "playwright": "^1.17.1", "postcss": "^7.0.32", "postcss-loader": "^3.0.0", @@ -927,9 +916,7 @@ "resolve": "^1.22.0", "rxjs-marbles": "^5.0.6", "sass-loader": "^10.2.0", - "sass-resources-loader": "^2.0.1", "selenium-webdriver": "^4.1.1", - "serve-static": "1.14.1", "shelljs": "^0.8.4", "simple-git": "1.116.0", "sinon": "^7.4.2", @@ -945,7 +932,6 @@ "supertest": "^3.1.0", "supports-color": "^7.0.0", "tape": "^5.0.1", - "tar-fs": "^2.1.0", "tempy": "^0.3.0", "terser": "^5.7.1", "terser-webpack-plugin": "^4.2.3", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 184c16f96167f7..f2d5a60cd325e6 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -19305,7 +19305,7 @@ cmdShim.ifExists = cmdShimIfExists var fs = __webpack_require__("../../node_modules/graceful-fs/graceful-fs.js") -var mkdir = __webpack_require__("../../node_modules/cmd-shim/node_modules/mkdirp/index.js") +var mkdir = __webpack_require__("../../node_modules/mkdirp/index.js") , path = __webpack_require__("path") , toBatchSyntax = __webpack_require__("../../node_modules/cmd-shim/lib/to-batch-syntax.js") , shebangExpr = /^#\!\s*(?:\/usr\/bin\/env)?\s*([^ \t]+=[^ \t]+\s+)*\s*([^ \t]+)(.*)$/ @@ -19598,112 +19598,6 @@ function replaceDollarWithPercentPair(value) { -/***/ }), - -/***/ "../../node_modules/cmd-shim/node_modules/mkdirp/index.js": -/***/ (function(module, exports, __webpack_require__) { - -var path = __webpack_require__("path"); -var fs = __webpack_require__("fs"); -var _0777 = parseInt('0777', 8); - -module.exports = mkdirP.mkdirp = mkdirP.mkdirP = mkdirP; - -function mkdirP (p, opts, f, made) { - if (typeof opts === 'function') { - f = opts; - opts = {}; - } - else if (!opts || typeof opts !== 'object') { - opts = { mode: opts }; - } - - var mode = opts.mode; - var xfs = opts.fs || fs; - - if (mode === undefined) { - mode = _0777 & (~process.umask()); - } - if (!made) made = null; - - var cb = f || function () {}; - p = path.resolve(p); - - xfs.mkdir(p, mode, function (er) { - if (!er) { - made = made || p; - return cb(null, made); - } - switch (er.code) { - case 'ENOENT': - if (path.dirname(p) === p) return cb(er); - mkdirP(path.dirname(p), opts, function (er, made) { - if (er) cb(er, made); - else mkdirP(p, opts, cb, made); - }); - break; - - // In the case of any other error, just see if there's a dir - // there already. If so, then hooray! If not, then something - // is borked. - default: - xfs.stat(p, function (er2, stat) { - // if the stat fails, then that's super weird. - // let the original error be the failure reason. - if (er2 || !stat.isDirectory()) cb(er, made) - else cb(null, made); - }); - break; - } - }); -} - -mkdirP.sync = function sync (p, opts, made) { - if (!opts || typeof opts !== 'object') { - opts = { mode: opts }; - } - - var mode = opts.mode; - var xfs = opts.fs || fs; - - if (mode === undefined) { - mode = _0777 & (~process.umask()); - } - if (!made) made = null; - - p = path.resolve(p); - - try { - xfs.mkdirSync(p, mode); - made = made || p; - } - catch (err0) { - switch (err0.code) { - case 'ENOENT' : - made = sync(path.dirname(p), opts, made); - sync(p, opts, made); - break; - - // In the case of any other error, just see if there's a dir - // there already. If so, then hooray! If not, then something - // is borked. - default: - var stat; - try { - stat = xfs.statSync(p); - } - catch (err1) { - throw err0; - } - if (!stat.isDirectory()) throw err0; - break; - } - } - - return made; -}; - - /***/ }), /***/ "../../node_modules/color-convert/conversions.js": @@ -36304,6 +36198,112 @@ function isConstructorOrProto (obj, key) { } +/***/ }), + +/***/ "../../node_modules/mkdirp/index.js": +/***/ (function(module, exports, __webpack_require__) { + +var path = __webpack_require__("path"); +var fs = __webpack_require__("fs"); +var _0777 = parseInt('0777', 8); + +module.exports = mkdirP.mkdirp = mkdirP.mkdirP = mkdirP; + +function mkdirP (p, opts, f, made) { + if (typeof opts === 'function') { + f = opts; + opts = {}; + } + else if (!opts || typeof opts !== 'object') { + opts = { mode: opts }; + } + + var mode = opts.mode; + var xfs = opts.fs || fs; + + if (mode === undefined) { + mode = _0777 & (~process.umask()); + } + if (!made) made = null; + + var cb = f || function () {}; + p = path.resolve(p); + + xfs.mkdir(p, mode, function (er) { + if (!er) { + made = made || p; + return cb(null, made); + } + switch (er.code) { + case 'ENOENT': + if (path.dirname(p) === p) return cb(er); + mkdirP(path.dirname(p), opts, function (er, made) { + if (er) cb(er, made); + else mkdirP(p, opts, cb, made); + }); + break; + + // In the case of any other error, just see if there's a dir + // there already. If so, then hooray! If not, then something + // is borked. + default: + xfs.stat(p, function (er2, stat) { + // if the stat fails, then that's super weird. + // let the original error be the failure reason. + if (er2 || !stat.isDirectory()) cb(er, made) + else cb(null, made); + }); + break; + } + }); +} + +mkdirP.sync = function sync (p, opts, made) { + if (!opts || typeof opts !== 'object') { + opts = { mode: opts }; + } + + var mode = opts.mode; + var xfs = opts.fs || fs; + + if (mode === undefined) { + mode = _0777 & (~process.umask()); + } + if (!made) made = null; + + p = path.resolve(p); + + try { + xfs.mkdirSync(p, mode); + made = made || p; + } + catch (err0) { + switch (err0.code) { + case 'ENOENT' : + made = sync(path.dirname(p), opts, made); + sync(p, opts, made); + break; + + // In the case of any other error, just see if there's a dir + // there already. If so, then hooray! If not, then something + // is borked. + default: + var stat; + try { + stat = xfs.statSync(p); + } + catch (err1) { + throw err0; + } + if (!stat.isDirectory()) throw err0; + break; + } + } + + return made; +}; + + /***/ }), /***/ "../../node_modules/multimatch/index.js": diff --git a/packages/kbn-test-jest-helpers/BUILD.bazel b/packages/kbn-test-jest-helpers/BUILD.bazel index dc8b83495494cb..85192829003e4f 100644 --- a/packages/kbn-test-jest-helpers/BUILD.bazel +++ b/packages/kbn-test-jest-helpers/BUILD.bazel @@ -60,7 +60,6 @@ RUNTIME_DEPS = [ "@npm//joi", "@npm//mustache", "@npm//normalize-path", - "@npm//parse-link-header", "@npm//prettier", "@npm//react", "@npm//react-dom", @@ -106,7 +105,6 @@ TYPES_DEPS = [ "@npm//@types/mustache", "@npm//@types/normalize-path", "@npm//@types/node", - "@npm//@types/parse-link-header", "@npm//@types/prettier", "@npm//@types/react", "@npm//@types/react-dom", diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index f7599e6d816498..15487aa781b8da 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -67,7 +67,6 @@ RUNTIME_DEPS = [ "@npm//js-yaml", "@npm//mustache", "@npm//normalize-path", - "@npm//parse-link-header", "@npm//prettier", "@npm//react-dom", "@npm//react-redux", @@ -115,7 +114,6 @@ TYPES_DEPS = [ "@npm//@types/mustache", "@npm//@types/normalize-path", "@npm//@types/node", - "@npm//@types/parse-link-header", "@npm//@types/prettier", "@npm//@types/react-dom", "@npm//@types/react-redux", diff --git a/x-pack/test/fleet_api_integration/apis/mock_http_server.d.ts b/x-pack/test/fleet_api_integration/apis/mock_http_server.d.ts deleted file mode 100644 index ac8e88b6fefe3b..00000000000000 --- a/x-pack/test/fleet_api_integration/apis/mock_http_server.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// No types for mock-http-server available, but we don't need them. - -declare module 'mock-http-server'; diff --git a/yarn.lock b/yarn.lock index 439a288a9db598..45307af4aa044b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6660,11 +6660,6 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== -"@types/parse-link-header@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-link-header/-/parse-link-header-1.0.0.tgz#69f059e40a0fa93dc2e095d4142395ae6adc5d7a" - integrity sha512-fCA3btjE7QFeRLfcD0Sjg+6/CnmC66HpMBoRfRzd2raTaWMJV21CCZ0LO8MOqf8onl5n0EPfjq4zDhbyX8SVwA== - "@types/parse5@*", "@types/parse5@^5.0.0": version "5.0.3" resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" @@ -7096,13 +7091,6 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74" integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== -"@types/tar-fs@^1.16.1": - version "1.16.1" - resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-1.16.1.tgz#6e3fba276c173e365ae91e55f7b797a0e64298e5" - integrity sha512-uQQIaa8ukcKf/1yy2kzfP1PF+7jEZghFDKpDvgtsYo/mbqM1g4Qza1Y5oAw6kJMa7eLA/HkmxUsDqb2sWKVF9g== - dependencies: - "@types/node" "*" - "@types/tar@^4.0.5": version "4.0.5" resolved "https://registry.yarnpkg.com/@types/tar/-/tar-4.0.5.tgz#5f953f183e36a15c6ce3f336568f6051b7b183f3" @@ -8587,7 +8575,7 @@ async@^1.4.2: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= -async@^2.1.4, async@^2.6.2: +async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -9189,7 +9177,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.9: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== -body-parser@1.19.0, body-parser@^1.18.1, body-parser@^1.18.3: +body-parser@1.19.0, body-parser@^1.18.3: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== @@ -10833,16 +10821,6 @@ connect-history-api-fallback@^1.6.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== -connect@^3.4.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" - integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== - dependencies: - debug "2.6.9" - finalhandler "1.1.2" - parseurl "~1.3.3" - utils-merge "1.0.1" - console-browserify@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" @@ -12041,11 +12019,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -dashify@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.1.0.tgz#107daf9cca5e326e30a8b39ffa5048b6684922ea" - integrity sha1-EH2vnMpeMm4wqLOf+lBItmhJIuo= - data-uri-to-buffer@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz#18ae979a6a0ca994b0625853916d2662bbae0b1a" @@ -14434,7 +14407,7 @@ fbjs@^0.8.1, fbjs@^0.8.9: setimmediate "^1.0.5" ua-parser-js "^0.7.18" -fd-slicer@1.1.0, fd-slicer@~1.1.0: +fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= @@ -14586,7 +14559,7 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.1.2, finalhandler@~1.1.2: +finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== @@ -15384,7 +15357,7 @@ glob-to-regexp@^0.4.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob-watcher@5.0.3, glob-watcher@^5.0.3: +glob-watcher@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-5.0.3.tgz#88a8abf1c4d131eb93928994bc4a593c2e5dd626" integrity sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg== @@ -16408,7 +16381,7 @@ http-deceiver@^1.2.7: resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= -http-errors@1.7.2, http-errors@~1.7.0, http-errors@~1.7.2: +http-errors@1.7.2, http-errors@~1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== @@ -16579,11 +16552,6 @@ icss-utils@^4.0.0, icss-utils@^4.1.1: dependencies: postcss "^7.0.14" -idx@^2.5.6: - version "2.5.6" - resolved "https://registry.yarnpkg.com/idx/-/idx-2.5.6.tgz#1f824595070100ae9ad585c86db08dc74f83a59d" - integrity sha512-WFXLF7JgPytbMgelpRY46nHz5tyDcedJ76pLV+RJWdb8h33bxFq4bdZau38DhNSzk5eVniBf1K3jwfK+Lb5nYA== - ieee754@^1.1.12, ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -16780,13 +16748,6 @@ inline-style-prefixer@^4.0.0: bowser "^1.7.3" css-in-js-utils "^2.0.0" -inline-style@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/inline-style/-/inline-style-2.0.0.tgz#2fa9cf624596a8109355b925094e138bbd5ea29b" - integrity sha1-L6nPYkWWqBCTVbklCU4Ti71eops= - dependencies: - dashify "^0.1.0" - inquirer@^7.0.0, inquirer@^7.3.3: version "7.3.3" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" @@ -19234,7 +19195,7 @@ loader-utils@2.0.0, loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.0.4, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: +loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== @@ -20266,7 +20227,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@0.0.8, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.0: +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.0: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== @@ -20368,13 +20329,6 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - mkdirp@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" @@ -20481,16 +20435,6 @@ mock-fs@^5.1.2: resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7" integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A== -mock-http-server@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/mock-http-server/-/mock-http-server-1.3.0.tgz#d2c2ffe65f77d3a4da8302c91d3bf687e5b51519" - integrity sha512-WC1fQ4kfOiiRZZ6IEOispJcfvz66m7VVbVFmnWsv1pOwL3psqYyLQGjFXg//zjPeZ//y/rxa8e2eh1Bb58cN7g== - dependencies: - body-parser "^1.18.1" - connect "^3.4.0" - multiparty "^4.1.2" - underscore "^1.8.3" - module-deps@^6.2.3: version "6.2.3" resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-6.2.3.tgz#15490bc02af4b56cf62299c7c17cba32d71a96ee" @@ -20642,16 +20586,6 @@ multimatch@^4.0.0: arrify "^2.0.1" minimatch "^3.0.4" -multiparty@^4.1.2: - version "4.2.1" - resolved "https://registry.yarnpkg.com/multiparty/-/multiparty-4.2.1.tgz#d9b6c46d8b8deab1ee70c734b0af771dd46e0b13" - integrity sha512-AvESCnNoQlZiOfP9R4mxN8M9csy2L16EIbWIkt3l4FuGti9kXBS8QVzlfyg4HEnarJhrzZilgNFlZtqmoiAIIA== - dependencies: - fd-slicer "1.1.0" - http-errors "~1.7.0" - safe-buffer "5.1.2" - uid-safe "2.1.5" - murmurhash-js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" @@ -22050,13 +21984,6 @@ parse-json@^5.0.0: json-parse-better-errors "^1.0.1" lines-and-columns "^1.1.6" -parse-link-header@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-1.0.1.tgz#bedfe0d2118aeb84be75e7b025419ec8a61140a7" - integrity sha1-vt/g0hGK64S+deewJUGeyKYRQKc= - dependencies: - xtend "~4.0.1" - parse-ms@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" @@ -22232,7 +22159,7 @@ pathval@^1.1.0: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -pbf@3.2.1, pbf@^3.0.5, pbf@^3.2.1: +pbf@3.2.1, pbf@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.2.1.tgz#b4c1b9e72af966cd82c6531691115cc0409ffe2a" integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ== @@ -22379,13 +22306,6 @@ pixelmatch@^4.0.2: dependencies: pngjs "^3.0.0" -pixelmatch@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.1.0.tgz#b640f0e5a03a09f235a4b818ef3b9b98d9d0b911" - integrity sha512-HqtgvuWN12tBzKJf7jYsc38Ha28Q2NYpmBL9WostEGgDHJqbTLkjydZXL1ZHM02ZnB+Dkwlxo87HBY38kMiD6A== - dependencies: - pngjs "^3.4.0" - pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -23603,11 +23523,6 @@ randexp@0.4.6: discontinuous-range "1.0.0" ret "~0.1.10" -random-bytes@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" - integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= - random-word-slugs@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/random-word-slugs/-/random-word-slugs-0.0.5.tgz#6ccd6c7ea320be9fbc19507f8c3a7d4a970ff61f" @@ -25618,16 +25533,6 @@ sass-loader@^10.2.0: schema-utils "^3.0.0" semver "^7.3.2" -sass-resources-loader@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/sass-resources-loader/-/sass-resources-loader-2.0.1.tgz#c8427f3760bf7992f24f27d3889a1c797e971d3a" - integrity sha512-UsjQWm01xglINC1kPidYwKOBBzOElVupm9RwtOkRlY0hPA4GKi2KFsn4BZypRD1kudaXgUnGnfbiVOE7c+ybAg== - dependencies: - async "^2.1.4" - chalk "^1.1.3" - glob "^7.1.1" - loader-utils "^1.0.4" - save-pixels@^2.3.2: version "2.3.4" resolved "https://registry.yarnpkg.com/save-pixels/-/save-pixels-2.3.4.tgz#49d349c06b8d7c0127dbf0da24b44aca5afb59fe" @@ -27478,11 +27383,6 @@ syntax-error@^1.1.1: dependencies: acorn-node "^1.2.0" -tabbable@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.3.tgz#0e4ee376f3631e42d7977a074dbd2b3827843081" - integrity sha512-nOWwx35/JuDI4ONuF0ZTo6lYvI0fY0tZCH1ErzY2EXfu4az50ZyiUX8X073FLiZtmWUVlkRnuXsehjJgCw9tYg== - tabbable@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.2.1.tgz#e3fda7367ddbb172dcda9f871c0fdb36d1c4cd9c" @@ -27567,16 +27467,6 @@ tar-fs@^2.0.0, tar-fs@^2.1.1: pump "^3.0.0" tar-stream "^2.1.4" -tar-fs@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" - integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.0.0" - tar-stream@^2.0.0: version "2.1.3" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.3.tgz#1e2022559221b7866161660f118255e20fa79e41" @@ -28447,13 +28337,6 @@ uglify-to-browserify@~1.0.0: resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= -uid-safe@2.1.5: - version "2.1.5" - resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" - integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== - dependencies: - random-bytes "~1.0.0" - umd@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf" @@ -28506,7 +28389,7 @@ undefsafe@^2.0.5: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== -underscore@^1.13.1, underscore@^1.8.3: +underscore@^1.13.1: version "1.13.1" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== @@ -29726,15 +29609,6 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw== -vt-pbf@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.1.tgz#b0f627e39a10ce91d943b898ed2363d21899fb82" - integrity sha512-pHjWdrIoxurpmTcbfBWXaPwSmtPAHS105253P1qyEfSTV2HJddqjM+kIHquaT/L6lVJIk9ltTGc0IxR/G47hYA== - dependencies: - "@mapbox/point-geometry" "0.1.0" - "@mapbox/vector-tile" "^1.3.1" - pbf "^3.0.5" - vt-pbf@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.3.tgz#68fd150756465e2edae1cc5c048e063916dcfaac" From 9e05c08a6f80407213a5b1f99db5692a2b7ea236 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Mon, 9 May 2022 14:51:31 +0200 Subject: [PATCH 79/83] [Osquery] Fix select 3 platforms. (#131727) Fix select all 3 platforms issue --- .../fixtures/saved_objects/saved_query.ndjson | 1 + ...ngs.spec.ts => edit_saved_queries.spec.ts} | 52 ++++++++++++++++++- .../form/use_saved_query_form.tsx | 7 --- 3 files changed, 51 insertions(+), 9 deletions(-) rename x-pack/plugins/osquery/cypress/integration/all/{delete_all_ecs_mappings.spec.ts => edit_saved_queries.spec.ts} (59%) diff --git a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson index b29c4e28e731d6..0e1a2f4b67caca 100644 --- a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson +++ b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson @@ -14,6 +14,7 @@ "id": "Saved-Query-Id", "interval": "3600", "query": "select * from uptime;", + "platform": "linux,darwin", "updated_at": "2021-12-21T08:54:38.648Z", "updated_by": "elastic" }, diff --git a/x-pack/plugins/osquery/cypress/integration/all/delete_all_ecs_mappings.spec.ts b/x-pack/plugins/osquery/cypress/integration/all/edit_saved_queries.spec.ts similarity index 59% rename from x-pack/plugins/osquery/cypress/integration/all/delete_all_ecs_mappings.spec.ts rename to x-pack/plugins/osquery/cypress/integration/all/edit_saved_queries.spec.ts index 1ce25a77f834a4..6dde0013a4bc69 100644 --- a/x-pack/plugins/osquery/cypress/integration/all/delete_all_ecs_mappings.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/all/edit_saved_queries.spec.ts @@ -10,7 +10,7 @@ import { login } from '../../tasks/login'; import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver'; import { ROLES } from '../../test'; -describe('ALL - Delete ECS Mappings', () => { +describe('ALL - Edit saved query', () => { const SAVED_QUERY_ID = 'Saved-Query-Id'; before(() => { @@ -25,7 +25,7 @@ describe('ALL - Delete ECS Mappings', () => { runKbnArchiverScript(ArchiverMethod.UNLOAD, 'saved_query'); }); - it('to click the edit button and edit pack', () => { + it('by changing ecs mappings and platforms', () => { cy.react('CustomItemAction', { props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } }, }).click(); @@ -35,7 +35,33 @@ describe('ALL - Delete ECS Mappings', () => { .parents('[data-test-subj="ECSMappingEditorForm"]') .react('EuiButtonIcon', { props: { iconType: 'trash' } }) .click(); + + cy.react('PlatformCheckBoxGroupField').within(() => { + cy.react('EuiCheckbox', { + props: { + id: 'linux', + checked: true, + }, + }).should('exist'); + cy.react('EuiCheckbox', { + props: { + id: 'darwin', + checked: true, + }, + }).should('exist'); + + cy.react('EuiCheckbox', { + props: { + id: 'windows', + checked: false, + }, + }).should('exist'); + }); + + cy.get('#windows').check({ force: true }); + cy.react('EuiButton').contains('Update query').click(); + cy.wait(5000); cy.react('CustomItemAction', { @@ -43,5 +69,27 @@ describe('ALL - Delete ECS Mappings', () => { }).click(); cy.contains('Custom key/value pairs').should('not.exist'); cy.contains('Hours of uptime').should('not.exist'); + + cy.react('PlatformCheckBoxGroupField').within(() => { + cy.react('EuiCheckbox', { + props: { + id: 'linux', + checked: true, + }, + }).should('exist'); + cy.react('EuiCheckbox', { + props: { + id: 'darwin', + checked: true, + }, + }).should('exist'); + + cy.react('EuiCheckbox', { + props: { + id: 'windows', + checked: true, + }, + }).should('exist'); + }); }); }); diff --git a/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx b/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx index 6da252f78aedf8..1d0d9f28d097b6 100644 --- a/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx @@ -56,13 +56,6 @@ export const useSavedQueryForm = ({ defaultValue, handleSubmit }: UseSavedQueryF defaultValue, serializer: (payload) => produce(payload, (draft) => { - // @ts-expect-error update types - if (draft.platform?.split(',').length === 3) { - // if all platforms are checked then use undefined - // @ts-expect-error update types - delete draft.platform; - } - if (isArray(draft.version)) { if (!draft.version.length) { // @ts-expect-error update types From 8166d628945f1e7bec324249f9fd0a3183e5255f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Mon, 9 May 2022 15:27:55 +0200 Subject: [PATCH 80/83] [Security solution][Endpoint] Disable add endpoint event filter when not in host events page (#131705) * Disables add endpoint event filters context menu action when not in host events page * Disables also when is an event but is not from endpoint, added tooltip and unit test cases * Set tooltip to undefined when no needed instead to empty string * Update wording for tooltip when item is disabled * Split test cases in blocks un order to reuse functions and props --- .../alert_context_menu.test.tsx | 92 +++++++++++++++++-- .../timeline_actions/alert_context_menu.tsx | 12 ++- .../use_event_filter_action.tsx | 5 +- .../components/alerts_table/translations.ts | 8 ++ 4 files changed, 109 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index 0f613aff8d4568..8cb29901abdad7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -13,6 +13,10 @@ import React from 'react'; import { Ecs } from '../../../../../common/ecs'; import { mockTimelines } from '../../../../common/mock/mock_timelines_plugin'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; +import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../../../../common/components/user_privileges/user_privileges_context'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; + +jest.mock('../../../../common/components/user_privileges'); const ecsRowData: Ecs = { _id: '1', @@ -71,6 +75,7 @@ const addToNewCaseButton = '[data-test-subj="add-to-new-case-action"]'; const markAsOpenButton = '[data-test-subj="open-alert-status"]'; const markAsAcknowledgedButton = '[data-test-subj="acknowledged-alert-status"]'; const markAsClosedButton = '[data-test-subj="close-alert-status"]'; +const addEndpointEventFilterButton = '[data-test-subj="add-event-filter-menu-item"]'; describe('InvestigateInResolverAction', () => { test('it render AddToCase context menu item if timelineId === TimelineId.detectionsPage', () => { @@ -107,12 +112,7 @@ describe('InvestigateInResolverAction', () => { }); test('it does NOT render AddToCase context menu item when timelineId is not in the allowed list', () => { - // In order to enable alert context menu without a timelineId, event needs to be event.kind === 'event' and agent.type === 'endpoint' - const customProps = { - ...props, - ecsRowData: { ...ecsRowData, agent: { type: ['endpoint'] }, event: { kind: ['event'] } }, - }; - const wrapper = mount(, { + const wrapper = mount(, { wrappingComponent: TestProviders, }); wrapper.find(actionMenuButton).simulate('click'); @@ -131,4 +131,84 @@ describe('InvestigateInResolverAction', () => { expect(wrapper.find(markAsAcknowledgedButton).first().exists()).toEqual(true); expect(wrapper.find(markAsClosedButton).first().exists()).toEqual(true); }); + + describe('AddEndpointEventFilter', () => { + const endpointEventProps = { + ...props, + ecsRowData: { ...ecsRowData, agent: { type: ['endpoint'] }, event: { kind: ['event'] } }, + }; + + describe('when users can access endpoint management', () => { + beforeEach(() => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...mockInitialUserPrivilegesState(), + endpointPrivileges: { loading: false, canAccessEndpointManagement: true }, + }); + }); + + test('it disables AddEndpointEventFilter when timeline id is not host events page', () => { + const wrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + + wrapper.find(actionMenuButton).simulate('click'); + expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); + expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual(true); + }); + + test('it enables AddEndpointEventFilter when timeline id is host events page', () => { + const wrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + + wrapper.find(actionMenuButton).simulate('click'); + expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); + expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual(false); + }); + + test('it disables AddEndpointEventFilter when timeline id is host events page but is not from endpoint', () => { + const customProps = { + ...props, + ecsRowData: { ...ecsRowData, agent: { type: ['other'] }, event: { kind: ['event'] } }, + }; + const wrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + + wrapper.find(actionMenuButton).simulate('click'); + expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); + expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual(true); + }); + }); + describe('when users can NOT access endpoint management', () => { + beforeEach(() => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...mockInitialUserPrivilegesState(), + endpointPrivileges: { loading: false, canAccessEndpointManagement: false }, + }); + }); + + test('it disables AddEndpointEventFilter when timeline id is host events page but cannot acces endpoint management', () => { + const wrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + + wrapper.find(actionMenuButton).simulate('click'); + expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); + expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual(true); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index a6af9febe8b3e3..1427b2b3bf3880 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -88,6 +88,9 @@ const AlertContextMenuComponent: React.FC indexOf(ecsRowData.event?.kind, 'event') !== -1, [ecsRowData]); + const isAgentEndpoint = useMemo(() => ecsRowData.agent?.type?.includes('endpoint'), [ecsRowData]); + + const isEndpointEvent = useMemo(() => isEvent && isAgentEndpoint, [isEvent, isAgentEndpoint]); const onButtonClick = useCallback(() => { setPopover(!isPopoverOpen); @@ -173,7 +176,14 @@ const AlertContextMenuComponent: React.FC diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_event_filter_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_event_filter_action.tsx index 1a56c575057f05..4327c5a69a949c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_event_filter_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_event_filter_action.tsx @@ -12,9 +12,11 @@ import { ACTION_ADD_EVENT_FILTER } from '../translations'; export const useEventFilterAction = ({ onAddEventFilterClick, disabled = false, + tooltipMessage, }: { onAddEventFilterClick: () => void; disabled?: boolean; + tooltipMessage?: string; }) => { const eventFilterActionItems = useMemo( () => [ @@ -23,11 +25,12 @@ export const useEventFilterAction = ({ data-test-subj="add-event-filter-menu-item" onClick={onAddEventFilterClick} disabled={disabled} + toolTipContent={tooltipMessage} > {ACTION_ADD_EVENT_FILTER} , ], - [onAddEventFilterClick, disabled] + [onAddEventFilterClick, disabled, tooltipMessage] ); return { eventFilterActionItems }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index bdddd8ab462076..eba1fa8238d051 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -185,6 +185,14 @@ export const ACTION_ADD_EVENT_FILTER = i18n.translate( } ); +export const ACTION_ADD_EVENT_FILTER_DISABLED_TOOLTIP = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.addEventFilter.disabled.tooltip', + { + defaultMessage: + 'Endpoint event filters can be created from the Events section of the Hosts page.', + } +); + export const ACTION_ADD_ENDPOINT_EXCEPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.actions.addEndpointException', { From 190ed558fa94c22675c872748f1f2925f4ec5f6e Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 9 May 2022 15:47:39 +0200 Subject: [PATCH 81/83] [Cases] severity field in the cases list and allow to filter by it (#131694) --- x-pack/plugins/cases/common/api/cases/case.ts | 4 + x-pack/plugins/cases/common/ui/types.ts | 5 + .../all_cases/all_cases_list.test.tsx | 6 ++ .../components/all_cases/all_cases_list.tsx | 1 + .../public/components/all_cases/columns.tsx | 67 ++++++++----- .../all_cases/severity_filter.test.tsx | 56 +++++++++++ .../components/all_cases/severity_filter.tsx | 71 ++++++++++++++ .../all_cases/table_filters.test.tsx | 18 +++- .../components/all_cases/table_filters.tsx | 30 +++++- .../components/all_cases/translations.ts | 4 + .../public/components/severity/config.ts | 11 ++- .../components/severity/translations.ts | 4 + .../public/components/status/translations.ts | 2 +- .../cases/public/containers/__mocks__/api.ts | 3 +- .../cases/public/containers/api.test.tsx | 43 ++++++++- x-pack/plugins/cases/public/containers/api.ts | 8 +- .../public/containers/use_get_cases.test.tsx | 3 +- .../cases/public/containers/use_get_cases.tsx | 2 + .../plugins/cases/server/client/cases/find.ts | 1 + x-pack/plugins/cases/server/client/utils.ts | 24 +++++ .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../cases_api_integration/common/lib/mock.ts | 1 + .../tests/common/cases/find_cases.ts | 95 ++++++++++++++++++- 25 files changed, 423 insertions(+), 39 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx create mode 100644 x-pack/plugins/cases/public/components/all_cases/severity_filter.tsx diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index b3dbe4801f5443..ec855d98e7144e 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -170,6 +170,10 @@ export const CasesFindRequestRt = rt.partial({ * The status of the case (open, closed, in-progress) */ status: CaseStatusRt, + /** + * The severity of the case + */ + severity: CaseSeverityRt, /** * The reporters to filter by */ diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 7ed9bfb3f22942..5443302bce467b 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -20,6 +20,7 @@ import { CasesFindResponse, CasesStatusResponse, CasesMetricsResponse, + CaseSeverity, } from '../api'; import { SnakeToCamelCase } from '../types'; @@ -45,6 +46,9 @@ export type StatusAllType = typeof StatusAll; export type CaseStatusWithAllStatus = CaseStatuses | StatusAllType; +export const SeverityAll = 'all' as const; +export type CaseSeverityWithAll = CaseSeverity | typeof SeverityAll; + /** * The type for the `refreshRef` prop (a `React.Ref`) defined by the `CaseViewComponentProps`. * @@ -84,6 +88,7 @@ export interface QueryParams { export interface FilterOptions { search: string; + severity: CaseSeverityWithAll; status: CaseStatusWithAllStatus; tags: string[]; reporters: User[]; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index f0a3502fd6813f..22e12d5ee11b59 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -204,6 +204,11 @@ describe('AllCasesListGeneric', () => { .childAt(0) .prop('value') ).toBe(useGetCasesMockState.data.cases[0].createdAt); + + expect( + wrapper.find(`[data-test-subj="case-table-column-severity"]`).first().text().toLowerCase() + ).toBe(useGetCasesMockState.data.cases[0].severity); + expect(wrapper.find(`[data-test-subj="case-table-case-count"]`).first().text()).toEqual( 'Showing 10 cases' ); @@ -223,6 +228,7 @@ describe('AllCasesListGeneric', () => { createdAt: null, createdBy: null, status: null, + severity: null, tags: null, title: null, totalComment: null, diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx index 5eac485e24c7b8..96b220283b4524 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx @@ -218,6 +218,7 @@ export const AllCasesList = React.memo( tags: filterOptions.tags, status: filterOptions.status, owner: filterOptions.owner, + severity: filterOptions.severity, }} setFilterRefetch={setFilterRefetch} hiddenStatuses={hiddenStatuses} diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index 543e6ef6f4871e..43096d3de061c7 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -18,12 +18,13 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiHealth, } from '@elastic/eui'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import styled from 'styled-components'; import { Case, DeleteCase } from '../../../common/ui/types'; -import { CaseStatuses, ActionConnector } from '../../../common/api'; +import { CaseStatuses, ActionConnector, CaseSeverity } from '../../../common/api'; import { OWNER_INFO } from '../../../common/constants'; import { getEmptyTagValue } from '../empty_value'; import { FormattedRelativePreferenceDate } from '../formatted_date'; @@ -40,6 +41,7 @@ import { TruncatedText } from '../truncated_text'; import { getConnectorIcon } from '../utils'; import type { CasesOwners } from '../../client/helpers/can_use_cases'; import { useCasesFeatures } from '../cases_context/use_cases_features'; +import { severities } from '../severity/config'; export type CasesColumns = | EuiTableActionsColumnType @@ -300,30 +302,6 @@ export const useCasesColumns = ({ return getEmptyTagValue(); }, }, - ...(isSelectorView - ? [ - { - align: RIGHT_ALIGNMENT, - render: (theCase: Case) => { - if (theCase.id != null) { - return ( - { - assignCaseAction(theCase); - }} - size="s" - fill={true} - > - {i18n.SELECT} - - ); - } - return getEmptyTagValue(); - }, - }, - ] - : []), ...(!isSelectorView ? [ { @@ -351,6 +329,45 @@ export const useCasesColumns = ({ }, ] : []), + { + name: i18n.SEVERITY, + render: (theCase: Case) => { + if (theCase.severity != null) { + const severityData = severities[theCase.severity ?? CaseSeverity.LOW]; + return ( + + {severityData.label} + + ); + } + return getEmptyTagValue(); + }, + }, + + ...(isSelectorView + ? [ + { + align: RIGHT_ALIGNMENT, + render: (theCase: Case) => { + if (theCase.id != null) { + return ( + { + assignCaseAction(theCase); + }} + size="s" + fill={true} + > + {i18n.SELECT} + + ); + } + return getEmptyTagValue(); + }, + }, + ] + : []), ...(userCanCrud && !isSelectorView ? [ { diff --git a/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx b/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx new file mode 100644 index 00000000000000..7366bb3fceebb6 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseSeverity } from '../../../common/api'; +import React from 'react'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; +import userEvent from '@testing-library/user-event'; +import { waitFor } from '@testing-library/dom'; +import { SeverityFilter } from './severity_filter'; + +describe('Severity form field', () => { + const onSeverityChange = jest.fn(); + let appMockRender: AppMockRenderer; + const props = { + isLoading: false, + selectedSeverity: CaseSeverity.LOW, + isDisabled: false, + onSeverityChange, + }; + beforeEach(() => { + appMockRender = createAppMockRenderer(); + }); + it('renders', () => { + const result = appMockRender.render(); + expect(result.getByTestId('case-severity-filter')).not.toHaveAttribute('disabled'); + }); + + // default to LOW in this test configuration + it('defaults to the correct value', () => { + const result = appMockRender.render(); + // two items. one for the popover one for the selected field + expect(result.getAllByTestId('case-severity-filter-low').length).toBe(2); + }); + + it('selects the correct value when changed', async () => { + const result = appMockRender.render(); + userEvent.click(result.getByTestId('case-severity-filter')); + userEvent.click(result.getByTestId('case-severity-filter-high')); + await waitFor(() => { + expect(onSeverityChange).toHaveBeenCalledWith('high'); + }); + }); + + it('selects the correct value when changed (all)', async () => { + const result = appMockRender.render(); + userEvent.click(result.getByTestId('case-severity-filter')); + userEvent.click(result.getByTestId('case-severity-filter-all')); + await waitFor(() => { + expect(onSeverityChange).toHaveBeenCalledWith('all'); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/severity_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/severity_filter.tsx new file mode 100644 index 00000000000000..a9f4a6565c318b --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/severity_filter.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiSuperSelect, + EuiSuperSelectOption, + EuiText, +} from '@elastic/eui'; +import React from 'react'; +import { CaseSeverityWithAll, SeverityAll } from '../../containers/types'; +import { severitiesWithAll } from '../severity/config'; + +interface Props { + selectedSeverity: CaseSeverityWithAll; + onSeverityChange: (status: CaseSeverityWithAll) => void; + isLoading: boolean; + isDisabled: boolean; +} + +export const SeverityFilter: React.FC = ({ + selectedSeverity, + onSeverityChange, + isLoading, + isDisabled, +}) => { + const caseSeverities = Object.keys(severitiesWithAll) as CaseSeverityWithAll[]; + const options: Array> = caseSeverities.map( + (severity) => { + const severityData = severitiesWithAll[severity]; + return { + value: severity, + inputDisplay: ( + + + {severity === SeverityAll ? ( + {severityData.label} + ) : ( + {severityData.label} + )} + + + ), + }; + } + ); + + return ( + + ); +}; +SeverityFilter.displayName = 'SeverityFilter'; diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx index 5e83c33717abd1..ff1c00b56d0311 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx @@ -10,11 +10,12 @@ import { mount } from 'enzyme'; import { CaseStatuses } from '../../../common/api'; import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants'; -import { TestProviders } from '../../common/mock'; +import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases'; import { CasesTableFilters } from './table_filters'; +import userEvent from '@testing-library/user-event'; jest.mock('../../containers/use_get_reporters'); jest.mock('../../containers/use_get_tags'); @@ -35,7 +36,9 @@ const props = { }; describe('CasesTableFilters ', () => { + let appMockRender: AppMockRenderer; beforeEach(() => { + appMockRender = createAppMockRenderer(); jest.clearAllMocks(); (useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags }); (useGetReporters as jest.Mock).mockReturnValue({ @@ -57,6 +60,19 @@ describe('CasesTableFilters ', () => { expect(wrapper.find(`[data-test-subj="case-status-filter"]`).first().exists()).toBeTruthy(); }); + it('should render the case severity filter dropdown', () => { + const result = appMockRender.render(); + expect(result.getByTestId('case-severity-filter')).toBeTruthy(); + }); + + it('should call onFilterChange when the severity filter changes', () => { + const result = appMockRender.render(); + userEvent.click(result.getByTestId('case-severity-filter')); + userEvent.click(result.getByTestId('case-severity-filter-high')); + + expect(onFilterChanged).toBeCalledWith({ severity: 'high' }); + }); + it('should call onFilterChange when selected tags change', () => { const wrapper = mount( diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx index faee469d1c4bc5..0a34e756e37a63 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx @@ -10,7 +10,12 @@ import { isEqual } from 'lodash/fp'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiFilterGroup, EuiButton } from '@elastic/eui'; -import { StatusAll, CaseStatusWithAllStatus } from '../../../common/ui/types'; +import { + StatusAll, + CaseStatusWithAllStatus, + SeverityAll, + CaseSeverityWithAll, +} from '../../../common/ui/types'; import { CaseStatuses } from '../../../common/api'; import { FilterOptions } from '../../containers/types'; import { useGetTags } from '../../containers/use_get_tags'; @@ -18,6 +23,7 @@ import { useGetReporters } from '../../containers/use_get_reporters'; import { FilterPopover } from '../filter_popover'; import { StatusFilter } from './status_filter'; import * as i18n from './translations'; +import { SeverityFilter } from './severity_filter'; interface CasesTableFiltersProps { countClosedCases: number | null; @@ -39,6 +45,12 @@ const StatusFilterWrapper = styled(EuiFlexItem)` } `; +const SeverityFilterWrapper = styled(EuiFlexItem)` + && { + flex-basis: 180px; + } +`; + /** * Collection of filters for filtering data within the CasesTable. Contains search bar, * and tag selection @@ -48,6 +60,7 @@ const StatusFilterWrapper = styled(EuiFlexItem)` const defaultInitial = { search: '', + severity: SeverityAll, reporters: [], status: StatusAll, tags: [], @@ -151,6 +164,13 @@ const CasesTableFiltersComponent = ({ [onFilterChanged] ); + const onSeverityChanged = useCallback( + (severity: CaseSeverityWithAll) => { + onFilterChanged({ severity }); + }, + [onFilterChanged] + ); + const stats = useMemo( () => ({ [StatusAll]: null, @@ -181,6 +201,14 @@ const CasesTableFiltersComponent = ({ onSearch={handleOnSearch} /> + + + { }); }); + test('should apply the severity field correctly (with severity value)', async () => { + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + severity: CaseSeverity.HIGH, + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + reporters: [], + tags: [], + severity: CaseSeverity.HIGH, + }, + signal: abortCtrl.signal, + }); + }); + + test('should not send the severity field with "all" severity value', async () => { + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + severity: 'all', + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + reporters: [], + tags: [], + }, + signal: abortCtrl.signal, + }); + }); + test('should handle tags with weird chars', async () => { const weirdTags: string[] = ['(', '"double"']; diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 63a2ea794e065a..b0f00ad202c5f3 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { omit } from 'lodash'; import { Cases, FetchCasesProps, ResolvedCase, + SeverityAll, SortFieldCase, StatusAll, } from '../../common/ui/types'; @@ -149,6 +149,7 @@ export const getCaseUserActions = async ( export const getCases = async ({ filterOptions = { search: '', + severity: SeverityAll, reporters: [], status: StatusAll, tags: [], @@ -163,9 +164,10 @@ export const getCases = async ({ signal, }: FetchCasesProps): Promise => { const query = { + ...(filterOptions.status !== StatusAll ? { status: filterOptions.status } : {}), + ...(filterOptions.severity !== SeverityAll ? { severity: filterOptions.severity } : {}), reporters: filterOptions.reporters.map((r) => r.username ?? '').filter((r) => r !== ''), tags: filterOptions.tags, - status: filterOptions.status, ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), ...(filterOptions.owner.length > 0 ? { owner: filterOptions.owner } : {}), ...queryParams, @@ -173,7 +175,7 @@ export const getCases = async ({ const response = await KibanaServices.get().http.fetch(`${CASES_URL}/_find`, { method: 'GET', - query: query.status === StatusAll ? omit(query, ['status']) : query, + query, signal, }); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx index dee4d424c84def..b689746a7af001 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; -import { CaseStatuses } from '../../common/api'; +import { CaseSeverity, CaseStatuses } from '../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { DEFAULT_FILTER_OPTIONS, @@ -219,6 +219,7 @@ describe('useGetCases', () => { const spyOnGetCases = jest.spyOn(api, 'getCases'); const newFilters = { search: 'new', + severity: CaseSeverity.LOW, tags: ['new'], status: CaseStatuses.closed, owner: [SECURITY_SOLUTION_OWNER], diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.tsx index d817dc9d9ac0f6..f708d982822528 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.tsx @@ -15,6 +15,7 @@ import { SortFieldCase, StatusAll, UpdateByKey, + SeverityAll, } from './types'; import { useToasts } from '../common/lib/kibana'; import * as i18n from './translations'; @@ -101,6 +102,7 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS export const DEFAULT_FILTER_OPTIONS: FilterOptions = { search: '', + severity: SeverityAll, reporters: [], status: StatusAll, tags: [], diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index b5d3cee05ced68..0c222296928427 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -53,6 +53,7 @@ export const find = async ( reporters: queryParams.reporters, sortByField: queryParams.sortField, status: queryParams.status, + severity: queryParams.severity, owner: queryParams.owner, from: queryParams.from, to: queryParams.to, diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index faae6450c52381..334b974c06108d 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -23,6 +23,7 @@ import { ContextTypeUserRt, excess, throwErrors, + CaseSeverity, } from '../../common/api'; import { combineFilterWithAuthorizationFilter } from '../authorization/utils'; import { @@ -114,6 +115,25 @@ export const addStatusFilter = ({ return filters.length > 1 ? nodeBuilder.and(filters) : filters[0]; }; +export const addSeverityFilter = ({ + severity, + appendFilter, + type = CASE_SAVED_OBJECT, +}: { + severity: CaseSeverity; + appendFilter?: KueryNode; + type?: string; +}): KueryNode => { + const filters: KueryNode[] = []; + filters.push(nodeBuilder.is(`${type}.attributes.severity`, severity)); + + if (appendFilter) { + filters.push(appendFilter); + } + + return filters.length > 1 ? nodeBuilder.and(filters) : filters[0]; +}; + interface FilterField { filters?: string | string[]; field: string; @@ -222,6 +242,7 @@ export const constructQueryOptions = ({ tags, reporters, status, + severity, sortByField, owner, authorizationFilter, @@ -231,6 +252,7 @@ export const constructQueryOptions = ({ tags?: string | string[]; reporters?: string | string[]; status?: CaseStatuses; + severity?: CaseSeverity; sortByField?: string; owner?: string | string[]; authorizationFilter?: KueryNode; @@ -250,10 +272,12 @@ export const constructQueryOptions = ({ const ownerFilter = buildFilter({ filters: owner ?? [], field: OWNER_FIELD, operator: 'or' }); const statusFilter = status != null ? addStatusFilter({ status }) : undefined; + const severityFilter = severity != null ? addSeverityFilter({ severity }) : undefined; const rangeFilter = buildRangeFilter({ from, to }); const filters: KueryNode[] = [ statusFilter, + severityFilter, tagsFilter, reportersFilter, rangeFilter, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f23d9cf64202b7..8a30c8bb80de3c 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -9716,7 +9716,6 @@ "xpack.cases.recentCases.viewAllCasesLink": "Afficher tous les cas", "xpack.cases.settings.syncAlertsSwitchLabelOff": "Arrêt", "xpack.cases.settings.syncAlertsSwitchLabelOn": "Marche", - "xpack.cases.status.all": "Tout", "xpack.cases.status.closed": "Fermé", "xpack.cases.status.iconAria": "Modifier le statut", "xpack.cases.status.inProgress": "En cours", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9b42d92c275e92..fb1ca2dcd650a0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9812,7 +9812,6 @@ "xpack.cases.recentCases.viewAllCasesLink": "すべてのケースを表示", "xpack.cases.settings.syncAlertsSwitchLabelOff": "オフ", "xpack.cases.settings.syncAlertsSwitchLabelOn": "オン", - "xpack.cases.status.all": "すべて", "xpack.cases.status.closed": "終了", "xpack.cases.status.iconAria": "ステータスの変更", "xpack.cases.status.inProgress": "進行中", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index dd87bbaa23fef3..3b96df93ad35da 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9834,7 +9834,6 @@ "xpack.cases.recentCases.viewAllCasesLink": "查看所有案例", "xpack.cases.settings.syncAlertsSwitchLabelOff": "关闭", "xpack.cases.settings.syncAlertsSwitchLabelOn": "开启", - "xpack.cases.status.all": "全部", "xpack.cases.status.closed": "已关闭", "xpack.cases.status.iconAria": "更改状态", "xpack.cases.status.inProgress": "进行中", diff --git a/x-pack/test/cases_api_integration/common/lib/mock.ts b/x-pack/test/cases_api_integration/common/lib/mock.ts index 08f30f8df024e4..c77c4ff8d6451d 100644 --- a/x-pack/test/cases_api_integration/common/lib/mock.ts +++ b/x-pack/test/cases_api_integration/common/lib/mock.ts @@ -30,6 +30,7 @@ export const postCaseReq: CasePostRequest = { description: 'This is a brand new case of a bad meanie defacing data', title: 'Super Bad Security Issue', tags: ['defacement'], + severity: CaseSeverity.LOW, connector: { id: 'none', name: 'none', diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index ddf0425fb53864..0381c46214669d 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -7,7 +7,12 @@ import expect from '@kbn/expect'; import { CASES_URL } from '@kbn/cases-plugin/common/constants'; -import { CaseResponse, CaseStatuses, CommentType } from '@kbn/cases-plugin/common/api'; +import { + CaseResponse, + CaseSeverity, + CaseStatuses, + CommentType, +} from '@kbn/cases-plugin/common/api'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -117,6 +122,45 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('filters by severity', async () => { + await createCase(supertest, postCaseReq); + const theCase = await createCase(supertest, postCaseReq); + const patchedCase = await updateCase({ + supertest, + params: { + cases: [ + { + id: theCase.id, + version: theCase.version, + severity: CaseSeverity.HIGH, + }, + ], + }, + }); + + const cases = await findCases({ supertest, query: { severity: CaseSeverity.HIGH } }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [patchedCase[0]], + count_open_cases: 1, + }); + }); + + it('filters by severity (none found)', async () => { + await createCase(supertest, postCaseReq); + await createCase(supertest, postCaseReq); + + const cases = await findCases({ supertest, query: { severity: CaseSeverity.CRITICAL } }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 0, + cases: [], + }); + }); + it('filters by reporters', async () => { const postedCase = await createCase(supertest, postCaseReq); const cases = await findCases({ supertest, query: { reporters: 'elastic' } }); @@ -802,6 +846,55 @@ export default ({ getService }: FtrProviderContext): void => { ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); }); }); + + describe('RBAC query filter', () => { + it('should return the correct cases when trying to query filter by severity', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', severity: CaseSeverity.HIGH }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', severity: CaseSeverity.HIGH }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', severity: CaseSeverity.HIGH }), + 200, + { + user: obsOnly, + space: 'space1', + } + ), + ]); + + // User with permissions only to security solution should get only the security solution cases + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + severity: CaseSeverity.HIGH, + }, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res.cases, 2, ['securitySolutionFixture']); + }); + }); }); }); }; From 58f480b9094a8a77cf888a3b55fdebb8b8b0f919 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 9 May 2022 06:57:05 -0700 Subject: [PATCH 82/83] [Screenshotting] Fix typo in warning logs regarding chromium's sandbox (#131584) * [Screenshotting] fix typo and prevent linebreak in OS * remove unnecessary messaging Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/browsers/chromium/driver_factory/index.ts | 4 ---- x-pack/plugins/screenshotting/server/config/create_config.ts | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts index 7d31cdc0c6b8cd..bfdc74aa43ba60 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts @@ -114,10 +114,6 @@ export class HeadlessChromiumDriverFactory { const dataDir = getDataPath(); fs.mkdirSync(dataDir, { recursive: true }); this.userDataDir = fs.mkdtempSync(path.join(dataDir, 'chromium-')); - - if (this.config.browser.chromium.disableSandbox) { - logger.warn(`Enabling the Chromium sandbox provides an additional layer of protection.`); - } } private getChromiumArgs() { diff --git a/x-pack/plugins/screenshotting/server/config/create_config.ts b/x-pack/plugins/screenshotting/server/config/create_config.ts index f12f2205d3a578..1b7076d05e4782 100644 --- a/x-pack/plugins/screenshotting/server/config/create_config.ts +++ b/x-pack/plugins/screenshotting/server/config/create_config.ts @@ -24,13 +24,13 @@ export async function createConfig(parentLogger: Logger, config: ConfigType) { // disableSandbox was not set by user, apply default for OS const { os, disableSandbox } = await getDefaultChromiumSandboxDisabled(); - const osName = [os.os, os.dist, os.release].filter(Boolean).map(upperFirst).join(' '); + const osName = [os.os, os.dist, os.release].filter(Boolean).map(upperFirst).join(' ').trim(); logger.debug(`Running on OS: '${osName}'`); if (disableSandbox === true) { logger.warn( - `Chromium sandbox provides an additional layer of protection, but is not supported for ${osName} OS. Automatically setting 'xpack.screenshotting.capture.browser.chromium.disableSandbox: true'.` + `Chromium sandbox provides an additional layer of protection, but is not supported for ${osName} OS. Automatically setting 'xpack.screenshotting.browser.chromium.disableSandbox: true'.` ); } else { logger.info( From a8017dffd48955bb23b85eddf6770616bc144c4d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 9 May 2022 16:07:29 +0200 Subject: [PATCH 83/83] [Lens] Make open in discover drilldown work (#131237) * make open in discover drilldown work * cleanup and tests * fix test * fix icon * fix type * fix open in new tab * fix open in new tab * fix test * make it possible to filter out drilldowns from list based on context * review comments * remove isConfigurable from the actionfactory Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli --- x-pack/plugins/lens/kibana.json | 1 + x-pack/plugins/lens/public/plugin.ts | 21 ++- .../open_in_discover_action.test.ts | 1 + .../open_in_discover_action.ts | 32 ++-- .../open_in_discover_drilldown.test.tsx | 65 ++++++++ .../open_in_discover_drilldown.tsx | 139 ++++++++++++++++++ .../open_in_discover_helpers.ts | 49 ++++++ x-pack/plugins/lens/tsconfig.json | 1 + .../public/drilldowns/drilldown_definition.ts | 6 + .../action_factory_picker.tsx | 8 +- .../state/drilldown_manager_state.ts | 26 +++- .../ui_actions_service_enhancements.ts | 3 +- 12 files changed, 328 insertions(+), 24 deletions(-) create mode 100644 x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx create mode 100644 x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx create mode 100644 x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index e5a55322a2f105..adf791e8d2f481 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -16,6 +16,7 @@ "visualizations", "dashboard", "uiActions", + "uiActionsEnhanced", "embeddable", "share", "presentationUtil", diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 876cb63b0333d0..e3c879d864a468 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -44,6 +44,7 @@ import { VISUALIZE_EDITOR_TRIGGER } from '@kbn/visualizations-plugin/public'; import { createStartServicesGetter } from '@kbn/kibana-utils-plugin/public'; import type { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { AdvancedUiActionsSetup } from '@kbn/ui-actions-enhanced-plugin/public'; import type { EditorFrameService as EditorFrameServiceType } from './editor_frame_service'; import type { IndexPatternDatasource as IndexPatternDatasourceType, @@ -93,6 +94,7 @@ import type { SaveModalContainerProps } from './app_plugin/save_modal_container' import { setupExpressions } from './expressions'; import { getSearchProvider } from './search_provider'; +import { OpenInDiscoverDrilldown } from './trigger_actions/open_in_discover_drilldown'; export interface LensPluginSetupDependencies { urlForwarding: UrlForwardingSetup; @@ -106,6 +108,7 @@ export interface LensPluginSetupDependencies { globalSearch?: GlobalSearchPluginSetup; usageCollection?: UsageCollectionSetup; discover?: DiscoverSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; } export interface LensPluginStartDependencies { @@ -224,6 +227,7 @@ export class LensPlugin { private heatmapVisualization: HeatmapVisualizationType | undefined; private gaugeVisualization: GaugeVisualizationType | undefined; private topNavMenuEntries: LensTopNavMenuEntryGenerator[] = []; + private hasDiscoverAccess: boolean = false; private stopReportManager?: () => void; @@ -240,6 +244,8 @@ export class LensPlugin { eventAnnotation, globalSearch, usageCollection, + uiActionsEnhanced, + discover, }: LensPluginSetupDependencies ) { const startServices = createStartServicesGetter(core.getStartServices); @@ -285,6 +291,15 @@ export class LensPlugin { visualizations.registerAlias(getLensAliasConfig()); + if (discover) { + uiActionsEnhanced.registerDrilldown( + new OpenInDiscoverDrilldown({ + discover, + hasDiscoverAccess: () => this.hasDiscoverAccess, + }) + ); + } + setupExpressions( expressions, () => startServices().plugins.fieldFormats.deserialize, @@ -427,6 +442,7 @@ export class LensPlugin { } start(core: CoreStart, startDependencies: LensPluginStartDependencies): LensPublicStart { + this.hasDiscoverAccess = core.application.capabilities.discover.show as boolean; // unregisters the Visualize action and registers the lens one if (startDependencies.uiActions.hasAction(ACTION_VISUALIZE_FIELD)) { startDependencies.uiActions.unregisterAction(ACTION_VISUALIZE_FIELD); @@ -443,10 +459,7 @@ export class LensPlugin { startDependencies.uiActions.addTriggerAction( CONTEXT_MENU_TRIGGER, - createOpenInDiscoverAction( - startDependencies.discover!, - core.application.capabilities.discover.show as boolean - ) + createOpenInDiscoverAction(startDependencies.discover!, this.hasDiscoverAccess) ); return { diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts index 084bd65b70d31f..eebdf04337f698 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts @@ -83,6 +83,7 @@ describe('open in discover action', () => { const embeddable = { getViewUnderlyingDataArgs: jest.fn(() => viewUnderlyingDataArgs), + type: 'lens', }; const discoverUrl = 'https://discover-redirect-url'; diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts index bd666f52bf0bc5..54a24aac269b59 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts @@ -5,17 +5,23 @@ * 2.0. */ -import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; import { createAction } from '@kbn/ui-actions-plugin/public'; import type { DiscoverStart } from '@kbn/discover-plugin/public'; -import type { Embeddable } from '../embeddable'; -import { DOC_TYPE } from '../../common'; +import { IEmbeddable } from '@kbn/embeddable-plugin/public'; +import { execute, isCompatible } from './open_in_discover_helpers'; const ACTION_OPEN_IN_DISCOVER = 'ACTION_OPEN_IN_DISCOVER'; -export const createOpenInDiscoverAction = (discover: DiscoverStart, hasDiscoverAccess: boolean) => - createAction<{ embeddable: IEmbeddable }>({ +interface Context { + embeddable: IEmbeddable; +} + +export const createOpenInDiscoverAction = ( + discover: Pick, + hasDiscoverAccess: boolean +) => + createAction({ type: ACTION_OPEN_IN_DISCOVER, id: ACTION_OPEN_IN_DISCOVER, order: 19, // right after Inspect which is 20 @@ -24,18 +30,10 @@ export const createOpenInDiscoverAction = (discover: DiscoverStart, hasDiscoverA i18n.translate('xpack.lens.app.exploreDataInDiscover', { defaultMessage: 'Explore data in Discover', }), - isCompatible: async (context: { embeddable: IEmbeddable }) => { - if (!hasDiscoverAccess) return false; - return ( - context.embeddable.type === DOC_TYPE && - (await (context.embeddable as Embeddable).canViewUnderlyingData()) - ); + isCompatible: async (context: Context) => { + return isCompatible({ hasDiscoverAccess, discover, embeddable: context.embeddable }); }, - execute: async (context: { embeddable: Embeddable }) => { - const args = context.embeddable.getViewUnderlyingDataArgs()!; - const discoverUrl = discover.locator?.getRedirectUrl({ - ...args, - }); - window.open(discoverUrl, '_blank'); + execute: async (context: Context) => { + return execute({ ...context, discover, hasDiscoverAccess }); }, }); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx new file mode 100644 index 00000000000000..bd1fc948eb9372 --- /dev/null +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FormEvent } from 'react'; +import { IEmbeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public'; +import { DiscoverSetup } from '@kbn/discover-plugin/public'; +import { execute, isCompatible } from './open_in_discover_helpers'; +import { mount } from 'enzyme'; +import { Filter } from '@kbn/es-query'; +import { + ActionFactoryContext, + CollectConfigProps, + OpenInDiscoverDrilldown, +} from './open_in_discover_drilldown'; + +jest.mock('./open_in_discover_helpers', () => ({ + isCompatible: jest.fn(() => true), + execute: jest.fn(), +})); + +describe('open in discover drilldown', () => { + let drilldown: OpenInDiscoverDrilldown; + beforeEach(() => { + drilldown = new OpenInDiscoverDrilldown({ + discover: {} as DiscoverSetup, + hasDiscoverAccess: () => true, + }); + }); + it('provides UI to edit config', () => { + const Component = (drilldown as unknown as { ReactCollectConfig: React.FC }) + .ReactCollectConfig; + const setConfig = jest.fn(); + const instance = mount( + + ); + instance.find('EuiSwitch').prop('onChange')!({} as unknown as FormEvent<{}>); + expect(setConfig).toHaveBeenCalledWith({ openInNewTab: true }); + }); + it('calls through to isCompatible helper', () => { + const filters: Filter[] = [{ meta: { disabled: false } }]; + drilldown.isCompatible( + { openInNewTab: true }, + { embeddable: { type: 'lens' } as IEmbeddable, filters } + ); + expect(isCompatible).toHaveBeenCalledWith(expect.objectContaining({ filters })); + }); + it('calls through to execute helper', () => { + const filters: Filter[] = [{ meta: { disabled: false } }]; + drilldown.execute( + { openInNewTab: true }, + { embeddable: { type: 'lens' } as IEmbeddable, filters } + ); + expect(execute).toHaveBeenCalledWith( + expect.objectContaining({ filters, openInSameTab: false }) + ); + }); +}); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx new file mode 100644 index 00000000000000..d957b9cafd4be1 --- /dev/null +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { IEmbeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public'; +import { + Query, + Filter, + TimeRange, + extractTimeRange, + APPLY_FILTER_TRIGGER, +} from '@kbn/data-plugin/public'; +import { CollectConfigProps as CollectConfigPropsBase } from '@kbn/kibana-utils-plugin/public'; +import { reactToUiComponent } from '@kbn/kibana-react-plugin/public'; +import { + UiActionsEnhancedDrilldownDefinition as Drilldown, + UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, +} from '@kbn/ui-actions-enhanced-plugin/public'; +import { EuiFormRow, EuiSwitch } from '@elastic/eui'; +import { DiscoverSetup } from '@kbn/discover-plugin/public'; +import { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { execute, isCompatible, isLensEmbeddable } from './open_in_discover_helpers'; + +interface EmbeddableQueryInput extends EmbeddableInput { + query?: Query; + filters?: Filter[]; + timeRange?: TimeRange; +} + +/** @internal */ +export type EmbeddableWithQueryInput = IEmbeddable; + +interface UrlDrilldownDeps { + discover: Pick; + hasDiscoverAccess: () => boolean; +} + +export type ActionContext = ApplyGlobalFilterActionContext; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type Config = { + openInNewTab: boolean; +}; + +export type OpenInDiscoverTrigger = typeof APPLY_FILTER_TRIGGER; + +export interface ActionFactoryContext extends BaseActionFactoryContext { + embeddable?: EmbeddableWithQueryInput; +} +export type CollectConfigProps = CollectConfigPropsBase; + +const OPEN_IN_DISCOVER_DRILLDOWN = 'OPEN_IN_DISCOVER_DRILLDOWN'; + +export class OpenInDiscoverDrilldown + implements Drilldown +{ + public readonly id = OPEN_IN_DISCOVER_DRILLDOWN; + + constructor(private readonly deps: UrlDrilldownDeps) {} + + public readonly order = 8; + + public readonly getDisplayName = () => + i18n.translate('xpack.lens.app.exploreDataInDiscoverDrilldown', { + defaultMessage: 'Open in Discover', + }); + + public readonly euiIcon = 'discoverApp'; + + supportedTriggers(): OpenInDiscoverTrigger[] { + return [APPLY_FILTER_TRIGGER]; + } + + private readonly ReactCollectConfig: React.FC = ({ + config, + onConfig, + context, + }) => { + return ( + + onConfig({ ...config, openInNewTab: !config.openInNewTab })} + data-test-subj="openInDiscoverDrilldownOpenInNewTab" + /> + + ); + }; + + public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); + + public readonly createConfig = () => ({ + openInNewTab: true, + }); + + public readonly isConfigValid = (config: Config): config is Config => { + return true; + }; + + public readonly isCompatible = async (config: Config, context: ActionContext) => { + return isCompatible({ + discover: this.deps.discover, + hasDiscoverAccess: this.deps.hasDiscoverAccess(), + ...context, + embeddable: context.embeddable as IEmbeddable, + ...config, + }); + }; + + public readonly isConfigurable = (context: ActionFactoryContext) => { + return this.deps.hasDiscoverAccess() && isLensEmbeddable(context.embeddable as IEmbeddable); + }; + + public readonly execute = async (config: Config, context: ActionContext) => { + const { restOfFilters: filters, timeRange: timeRange } = extractTimeRange( + context.filters, + context.timeFieldName + ); + execute({ + discover: this.deps.discover, + hasDiscoverAccess: this.deps.hasDiscoverAccess(), + ...context, + embeddable: context.embeddable as IEmbeddable, + openInSameTab: !config.openInNewTab, + filters, + timeRange, + }); + }; +} diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts new file mode 100644 index 00000000000000..87f0931f1a3dbd --- /dev/null +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DiscoverSetup } from '@kbn/discover-plugin/public'; +import { Filter } from '@kbn/es-query'; +import { TimeRange } from '@kbn/data-plugin/public'; +import { IEmbeddable } from '@kbn/embeddable-plugin/public'; +import type { Embeddable } from '../embeddable'; +import { DOC_TYPE } from '../../common'; + +interface Context { + embeddable: IEmbeddable; + filters?: Filter[]; + timeRange?: TimeRange; + openInSameTab?: boolean; + hasDiscoverAccess: boolean; + discover: Pick; +} + +export function isLensEmbeddable(embeddable: IEmbeddable): embeddable is Embeddable { + return embeddable.type === DOC_TYPE; +} + +export async function isCompatible({ hasDiscoverAccess, embeddable }: Context) { + if (!hasDiscoverAccess) return false; + return isLensEmbeddable(embeddable) && (await embeddable.canViewUnderlyingData()); +} + +export function execute({ embeddable, discover, timeRange, filters, openInSameTab }: Context) { + if (!isLensEmbeddable(embeddable)) { + // shouldn't be executed because of the isCompatible check + throw new Error('Can only be executed in the context of Lens visualization'); + } + const args = embeddable.getViewUnderlyingDataArgs(); + if (!args) { + // shouldn't be executed because of the isCompatible check + throw new Error('Underlying data is not ready'); + } + const discoverUrl = discover.locator?.getRedirectUrl({ + ...args, + timeRange: timeRange || args.timeRange, + filters: [...(filters || []), ...args.filters], + }); + window.open(discoverUrl, !openInSameTab ? '_blank' : '_self'); +} diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index e00581833f621c..20def97df7aed1 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -12,6 +12,7 @@ { "path": "../../../src/core/tsconfig.json" }, { "path": "../task_manager/tsconfig.json" }, { "path": "../global_search/tsconfig.json" }, + { "path": "../ui_actions_enhanced/tsconfig.json" }, { "path": "../saved_objects_tagging/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../../../src/plugins/data_views/tsconfig.json" }, diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index 4d64f02d2c14b5..4928b368a96b42 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -95,6 +95,12 @@ export interface DrilldownDefinition< */ isConfigValid: ActionFactoryDefinition['isConfigValid']; + /** + * Compatibility check during drilldown creation. + * Could be used to filter out a drilldown if it's not compatible with the current context. + */ + isConfigurable?(context: FactoryContext): boolean; + /** * Name of EUI icon to display when showing this drilldown to user. */ diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx index db9951f235dfc6..f52ac6e1615778 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { ActionFactoryPicker as ActionFactoryPickerUi } from '../../../../components/action_factory_picker'; import { useDrilldownManager } from '../context'; import { ActionFactoryView } from '../action_factory_view'; @@ -14,14 +15,19 @@ export const ActionFactoryPicker: React.FC = ({}) => { const drilldowns = useDrilldownManager(); const factory = drilldowns.useActionFactory(); const context = React.useMemo(() => drilldowns.getActionFactoryContext(), [drilldowns]); + const compatibleFactories = drilldowns.useCompatibleActionFactories(context); if (!!factory) { return ; } + if (!compatibleFactories) { + return ; + } + return ( { drilldowns.setActionFactory(actionFactory); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts index 15997355a2ae24..231057a50ee1f3 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts @@ -6,9 +6,10 @@ */ import useObservable from 'react-use/lib/useObservable'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import type { SerializableRecord } from '@kbn/utility-types'; +import { useMemo } from 'react'; import { PublicDrilldownManagerProps, DrilldownManagerDependencies, @@ -255,6 +256,24 @@ export class DrilldownManagerState { return context; } + public getCompatibleActionFactories( + context: BaseActionFactoryContext + ): Observable { + const compatibleActionFactories$ = new BehaviorSubject(undefined); + Promise.allSettled( + this.deps.actionFactories.map((factory) => factory.isCompatible(context)) + ).then((factoryCompatibility) => { + compatibleActionFactories$.next( + this.deps.actionFactories.filter((_factory, i) => { + const result = factoryCompatibility[i]; + // treat failed isCompatible checks as non-compatible + return result.status === 'fulfilled' && result.value; + }) + ); + }); + return compatibleActionFactories$.asObservable(); + } + /** * Get state object of the drilldown which is currently being created. */ @@ -478,4 +497,9 @@ export class DrilldownManagerState { public readonly useActionFactory = () => useObservable(this.actionFactory$, this.actionFactory$.getValue()); public readonly useEvents = () => useObservable(this.events$, this.events$.getValue()); + public readonly useCompatibleActionFactories = (context: BaseActionFactoryContext) => + useObservable( + useMemo(() => this.getCompatibleActionFactories(context), [context]), + undefined + ); } diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index 63f90d5a55a1f8..fb2dc3ea5bd035 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -116,6 +116,7 @@ export class UiActionsServiceEnhancements licenseFeatureName, supportedTriggers, isCompatible, + isConfigurable, telemetry, extract, inject, @@ -135,7 +136,7 @@ export class UiActionsServiceEnhancements extract, inject, getIconType: () => euiIcon, - isCompatible: async () => true, + isCompatible: async (context) => !isConfigurable || isConfigurable(context), create: (serializedAction) => ({ id: '', type: factoryId,