diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index de323128afed1..39daa5780436f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -203,7 +203,6 @@ /packages/kbn-legacy-logging/ @elastic/kibana-core /packages/kbn-crypto/ @elastic/kibana-core /packages/kbn-http-tools/ @elastic/kibana-core -/src/plugins/status_page/ @elastic/kibana-core /src/plugins/saved_objects_management/ @elastic/kibana-core /src/dev/run_check_published_api_changes.ts @elastic/kibana-core /src/plugins/home/public @elastic/kibana-core @@ -215,7 +214,6 @@ #CC# /src/plugins/legacy_export/ @elastic/kibana-core #CC# /src/plugins/xpack_legacy/ @elastic/kibana-core #CC# /src/plugins/saved_objects/ @elastic/kibana-core -#CC# /src/plugins/status_page/ @elastic/kibana-core #CC# /x-pack/plugins/cloud/ @elastic/kibana-core #CC# /x-pack/plugins/features/ @elastic/kibana-core #CC# /x-pack/plugins/global_search/ @elastic/kibana-core diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md index 2a30693f4da84..9fe43a2f3f477 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md @@ -9,5 +9,5 @@ The version in which this object type is being converted to a multi-namespace ty Signature: ```typescript -convertToMultiNamespaceTypeVersion?: string; +readonly convertToMultiNamespaceTypeVersion?: string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md index a1b3378afc53b..20a0e99275a39 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md @@ -9,5 +9,5 @@ logger instance to be used by the migration handler Signature: ```typescript -log: SavedObjectsMigrationLogger; +readonly log: SavedObjectsMigrationLogger; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md index 7b20ae41048f6..a1c2717e6e4a0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md @@ -9,5 +9,5 @@ The migration version that this migration function is defined for Signature: ```typescript -migrationVersion: string; +readonly migrationVersion: string; ``` diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index f30cfc53018db..c96de6ebbfcdd 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -661,13 +661,14 @@ function wrapWithTry( migrationFn: SavedObjectMigrationFn, log: Logger ) { + const context = Object.freeze({ + log: new MigrationLogger(log), + migrationVersion: version, + convertToMultiNamespaceTypeVersion: type.convertToMultiNamespaceTypeVersion, + }); + return function tryTransformDoc(doc: SavedObjectUnsanitizedDoc) { try { - const context = { - log: new MigrationLogger(log), - migrationVersion: version, - convertToMultiNamespaceTypeVersion: type.convertToMultiNamespaceTypeVersion, - }; const result = migrationFn(doc, context); // A basic sanity check to help migration authors detect basic errors diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index 619a7f85a327b..570315e780ebe 100644 --- a/src/core/server/saved_objects/migrations/types.ts +++ b/src/core/server/saved_objects/migrations/types.ts @@ -56,15 +56,15 @@ export interface SavedObjectMigrationContext { /** * logger instance to be used by the migration handler */ - log: SavedObjectsMigrationLogger; + readonly log: SavedObjectsMigrationLogger; /** * The migration version that this migration function is defined for */ - migrationVersion: string; + readonly migrationVersion: string; /** * The version in which this object type is being converted to a multi-namespace type */ - convertToMultiNamespaceTypeVersion?: string; + readonly convertToMultiNamespaceTypeVersion?: string; } /** diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index adeb78e568af3..7a47e58f1947c 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -198,6 +198,31 @@ describe('migrations v2 model', () => { }); describe('model transitions from', () => { + it('transition returns new state', () => { + const initState: State = { + ...baseState, + controlState: 'INIT', + currentAlias: '.kibana', + versionAlias: '.kibana_7.11.0', + versionIndex: '.kibana_7.11.0_001', + }; + + const res: ResponseType<'INIT'> = Either.right({ + '.kibana_7.11.0_001': { + aliases: { + '.kibana': {}, + '.kibana_7.11.0': {}, + }, + mappings: { + properties: {}, + }, + settings: {}, + }, + }); + const newState = model(initState, res); + expect(newState).not.toBe(initState); + }); + describe('INIT', () => { const initState: State = { ...baseState, diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 3ef3cb4f83b6f..f4185225ae073 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -9,7 +9,7 @@ import { gt, valid } from 'semver'; import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; -import { cloneDeep } from 'lodash'; + import { AliasAction, FetchIndexResponse, isLeftTypeof, RetryableEsClientError } from './actions'; import { AllActionStates, InitState, State } from './types'; import { IndexMapping } from '../mappings'; @@ -187,7 +187,7 @@ export const model = (currentState: State, resW: ResponseType): // control state using: // `const res = resW as ResponseType;` - let stateP: State = cloneDeep(currentState); + let stateP: State = currentState; // Handle retryable_es_client_errors. Other left values need to be handled // by the control state specific code below. diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index e3e52212d56cb..adcd2ad32fd24 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -381,7 +381,7 @@ export interface LegacyDeleteState extends LegacyBaseState { readonly controlState: 'LEGACY_DELETE'; } -export type State = +export type State = Readonly< | FatalState | InitState | DoneState @@ -411,7 +411,8 @@ export type State = | LegacySetWriteBlockState | LegacyReindexState | LegacyReindexWaitForTaskState - | LegacyDeleteState; + | LegacyDeleteState +>; export type AllControlStates = State['controlState']; /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 972e220baae3e..3e6a69d159192 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2152,9 +2152,9 @@ export interface SavedObjectExportBaseOptions { // @public export interface SavedObjectMigrationContext { - convertToMultiNamespaceTypeVersion?: string; - log: SavedObjectsMigrationLogger; - migrationVersion: string; + readonly convertToMultiNamespaceTypeVersion?: string; + readonly log: SavedObjectsMigrationLogger; + readonly migrationVersion: string; } // @public diff --git a/src/plugins/dashboard/common/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts index 16ab470ce7d6f..9f0858759d0d9 100644 --- a/src/plugins/dashboard/common/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.ts @@ -5,8 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import semverSatisfies from 'semver/functions/satisfies'; +import Semver from 'semver'; import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types'; import { DashboardContainerStateWithType, DashboardPanelState } from './types'; import { EmbeddablePersistableStateService } from '../../embeddable/common/types'; @@ -24,7 +23,7 @@ export interface SavedObjectAttributesAndReferences { } const isPre730Panel = (panel: Record): boolean => { - return 'version' in panel ? semverSatisfies(panel.version, '<7.3') : true; + return 'version' in panel ? Semver.gt('7.3.0', panel.version) : true; }; function dashboardAttributesToState( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts index 271a09849cba7..b444c1cc94383 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts @@ -6,3 +6,4 @@ */ export { mockEngineValues, mockEngineActions } from './engine_logic.mock'; +export { mockRecursivelyFetchEngines, mockSourceEngines } from './recursively_fetch_engines.mock'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts new file mode 100644 index 0000000000000..dd4c86a2a6360 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EngineDetails } from '../components/engine/types'; + +export const mockSourceEngines = [ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, +] as EngineDetails[]; + +export const mockRecursivelyFetchEngines = jest.fn(({ onComplete }) => + onComplete(mockSourceEngines) +); + +jest.mock('../utils/recursively_fetch_engines', () => ({ + recursivelyFetchEngines: mockRecursivelyFetchEngines, +})); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts index b90207331ffd6..de1902c7cf748 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts @@ -5,15 +5,16 @@ * 2.0. */ -import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__'; - -import { nextTick } from '@kbn/test/jest'; +import { LogicMounter } from '../../../../../__mocks__'; +import { mockRecursivelyFetchEngines } from '../../../../__mocks__/recursively_fetch_engines.mock'; import { EngineDetails } from '../../../engine/types'; import { MetaEnginesTableLogic } from './meta_engines_table_logic'; describe('MetaEnginesTableLogic', () => { + const { mount } = new LogicMounter(MetaEnginesTableLogic); + const DEFAULT_VALUES = { expandedRows: {}, sourceEngines: {}, @@ -44,15 +45,11 @@ describe('MetaEnginesTableLogic', () => { metaEngines: [...SOURCE_ENGINES, ...META_ENGINES] as EngineDetails[], }; - const { http } = mockHttpValues; - const { mount } = new LogicMounter(MetaEnginesTableLogic); - const { flashAPIErrors } = mockFlashMessageHelpers; - beforeEach(() => { jest.clearAllMocks(); }); - it('has expected default values', async () => { + it('has expected default values', () => { mount({}, DEFAULT_PROPS); expect(MetaEnginesTableLogic.values).toEqual(DEFAULT_VALUES); }); @@ -122,16 +119,6 @@ describe('MetaEnginesTableLogic', () => { }); it('calls fetchSourceEngines when it needs to fetch data for the itemId', () => { - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 1, - }, - }, - results: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - }) - ); mount(); jest.spyOn(MetaEnginesTableLogic.actions, 'fetchSourceEngines'); @@ -142,88 +129,22 @@ describe('MetaEnginesTableLogic', () => { }); describe('fetchSourceEngines', () => { - it('calls addSourceEngines and displayRow when it has retrieved all pages', async () => { + it('calls addSourceEngines and displayRow when it has retrieved all pages', () => { mount(); - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 1, - }, - }, - results: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - }) - ); jest.spyOn(MetaEnginesTableLogic.actions, 'displayRow'); jest.spyOn(MetaEnginesTableLogic.actions, 'addSourceEngines'); MetaEnginesTableLogic.actions.fetchSourceEngines('test-engine-1'); - await nextTick(); - - expect(http.get).toHaveBeenCalledWith( - '/api/app_search/engines/test-engine-1/source_engines', - { - query: { - 'page[current]': 1, - 'page[size]': 25, - }, - } - ); - expect(MetaEnginesTableLogic.actions.addSourceEngines).toHaveBeenCalledWith({ - 'test-engine-1': [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - }); - expect(MetaEnginesTableLogic.actions.displayRow).toHaveBeenCalledWith('test-engine-1'); - }); - - it('display a flash message on error', async () => { - http.get.mockReturnValueOnce(Promise.reject()); - mount(); - MetaEnginesTableLogic.actions.fetchSourceEngines('test-engine-1'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledTimes(1); - }); - - it('recursively fetches a number of pages', async () => { - mount(); - jest.spyOn(MetaEnginesTableLogic.actions, 'addSourceEngines'); - - // First page - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 2, - }, - }, - results: [{ name: 'source-engine-1' }], - }) - ); - - // Second and final page - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 2, - }, - }, - results: [{ name: 'source-engine-2' }], + expect(mockRecursivelyFetchEngines).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: '/api/app_search/engines/test-engine-1/source_engines', }) ); - - MetaEnginesTableLogic.actions.fetchSourceEngines('test-engine-1'); - await nextTick(); - expect(MetaEnginesTableLogic.actions.addSourceEngines).toHaveBeenCalledWith({ - 'test-engine-1': [ - // First page - { name: 'source-engine-1' }, - // Second and final page - { name: 'source-engine-2' }, - ], + 'test-engine-1': [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], }); + expect(MetaEnginesTableLogic.actions.displayRow).toHaveBeenCalledWith('test-engine-1'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts index 3a4c7d51c50a9..af4d0119a94af 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts @@ -7,10 +7,8 @@ import { kea, MakeLogicType } from 'kea'; -import { flashAPIErrors } from '../../../../../shared/flash_messages'; -import { HttpLogic } from '../../../../../shared/http'; +import { recursivelyFetchEngines } from '../../../../utils/recursively_fetch_engines'; import { EngineDetails } from '../../../engine/types'; -import { EnginesAPIResponse } from '../../types'; interface MetaEnginesTableValues { expandedRows: { [id: string]: boolean }; @@ -85,36 +83,13 @@ export const MetaEnginesTableLogic = kea< } }, fetchSourceEngines: ({ engineName }) => { - const { http } = HttpLogic.values; - - let enginesAccumulator: EngineDetails[] = []; - - const recursiveFetchSourceEngines = async (page = 1) => { - try { - const { meta, results }: EnginesAPIResponse = await http.get( - `/api/app_search/engines/${engineName}/source_engines`, - { - query: { - 'page[current]': page, - 'page[size]': 25, - }, - } - ); - - enginesAccumulator = [...enginesAccumulator, ...results]; - - if (page >= meta.page.total_pages) { - actions.addSourceEngines({ [engineName]: enginesAccumulator }); - actions.displayRow(engineName); - } else { - recursiveFetchSourceEngines(page + 1); - } - } catch (e) { - flashAPIErrors(e); - } - }; - - recursiveFetchSourceEngines(); + recursivelyFetchEngines({ + endpoint: `/api/app_search/engines/${engineName}/source_engines`, + onComplete: (sourceEngines) => { + actions.addSourceEngines({ [engineName]: sourceEngines }); + actions.displayRow(engineName); + }, + }); }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts index 7da44849b5bc0..6e17547a93980 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts @@ -8,3 +8,6 @@ export { SchemaCallouts } from './schema_callouts'; export { SchemaTable } from './schema_table'; export { EmptyState } from './empty_state'; +export { MetaEnginesSchemaTable } from './meta_engines_schema_table'; +export { MetaEnginesConflictsTable } from './meta_engines_conflicts_table'; +export { TruncatedEnginesList } from './truncated_engines_list'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx new file mode 100644 index 0000000000000..eb40d70e13ff8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { mount } from 'enzyme'; + +import { EuiTable, EuiTableHeaderCell, EuiTableRow } from '@elastic/eui'; + +import { MetaEnginesConflictsTable } from './'; + +describe('MetaEnginesConflictsTable', () => { + const values = { + conflictingFields: { + hello_field: { + text: ['engine1'], + number: ['engine2'], + date: ['engine3'], + }, + world_field: { + text: ['engine1'], + location: ['engine2', 'engine3', 'engine4'], + }, + }, + }; + + setMockValues(values); + const wrapper = mount(); + const fieldNames = wrapper.find('EuiTableRowCell[data-test-subj="fieldName"]'); + const fieldTypes = wrapper.find('EuiTableRowCell[data-test-subj="fieldTypes"]'); + const engines = wrapper.find('EuiTableRowCell[data-test-subj="enginesPerFieldType"]'); + + it('renders', () => { + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell).at(0).text()).toEqual('Field name'); + expect(wrapper.find(EuiTableHeaderCell).at(1).text()).toEqual('Field type conflicts'); + expect(wrapper.find(EuiTableHeaderCell).at(2).text()).toEqual('Engines'); + }); + + it('renders a rowspan on the initial field name column so that it stretches to all associated field conflict rows', () => { + expect(fieldNames).toHaveLength(2); + expect(fieldNames.at(0).prop('rowSpan')).toEqual(3); + expect(fieldNames.at(1).prop('rowSpan')).toEqual(2); + }); + + it('renders a row for each field type conflict and the engines that have that field type', () => { + expect(wrapper.find(EuiTableRow)).toHaveLength(5); + + expect(fieldNames.at(0).text()).toEqual('hello_field'); + expect(fieldTypes.at(0).text()).toEqual('text'); + expect(engines.at(0).text()).toEqual('engine1'); + expect(fieldTypes.at(1).text()).toEqual('number'); + expect(engines.at(1).text()).toEqual('engine2'); + expect(fieldTypes.at(2).text()).toEqual('date'); + expect(engines.at(2).text()).toEqual('engine3'); + + expect(fieldNames.at(1).text()).toEqual('world_field'); + expect(fieldTypes.at(3).text()).toEqual('text'); + expect(engines.at(3).text()).toEqual('engine1'); + expect(fieldTypes.at(4).text()).toEqual('location'); + expect(engines.at(4).text()).toEqual('engine2, engine3, +1'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx new file mode 100644 index 0000000000000..a37caafe69a59 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { + EuiTable, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableBody, + EuiTableRow, + EuiTableRowCell, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_NAME } from '../../../../shared/schema/constants'; +import { ENGINES_TITLE } from '../../engines'; + +import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic'; + +import { TruncatedEnginesList } from './'; + +export const MetaEnginesConflictsTable: React.FC = () => { + const { conflictingFields } = useValues(MetaEngineSchemaLogic); + + return ( + + + {FIELD_NAME} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.fieldTypeConflicts', + { defaultMessage: 'Field type conflicts' } + )} + + {ENGINES_TITLE} + + + {Object.entries(conflictingFields).map(([fieldName, conflicts]) => + Object.entries(conflicts).map(([fieldType, engines], i) => { + const isFirstRow = i === 0; + return ( + + {isFirstRow && ( + + {fieldName} + + )} + {fieldType} + + + + + ); + }) + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx new file mode 100644 index 0000000000000..7d377d5a92714 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { mount } from 'enzyme'; + +import { EuiTable, EuiTableHeaderCell, EuiTableRow, EuiTableRowCell } from '@elastic/eui'; + +import { MetaEnginesSchemaTable } from './'; + +describe('MetaEnginesSchemaTable', () => { + const values = { + schema: { + some_text_field: 'text', + some_number_field: 'number', + }, + fields: { + some_text_field: { + text: ['engine1', 'engine2'], + }, + some_number_field: { + number: ['engine1', 'engine2', 'engine3', 'engine4'], + }, + }, + }; + + setMockValues(values); + const wrapper = mount(); + const fieldNames = wrapper.find('EuiTableRowCell[data-test-subj="fieldName"]'); + const engines = wrapper.find('EuiTableRowCell[data-test-subj="engines"]'); + const fieldTypes = wrapper.find('EuiTableRowCell[data-test-subj="fieldType"]'); + + it('renders', () => { + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell).at(0).text()).toEqual('Field name'); + expect(wrapper.find(EuiTableHeaderCell).at(1).text()).toEqual('Engines'); + expect(wrapper.find(EuiTableHeaderCell).at(2).text()).toEqual('Field type'); + }); + + it('always renders an initial ID row', () => { + expect(wrapper.find('code').at(0).text()).toEqual('id'); + expect(wrapper.find(EuiTableRowCell).at(1).text()).toEqual('All'); + }); + + it('renders subsequent table rows for each schema field', () => { + expect(wrapper.find(EuiTableRow)).toHaveLength(3); + + expect(fieldNames.at(0).text()).toEqual('some_text_field'); + expect(engines.at(0).text()).toEqual('engine1, engine2'); + expect(fieldTypes.at(0).text()).toEqual('text'); + + expect(fieldNames.at(1).text()).toEqual('some_number_field'); + expect(engines.at(1).text()).toEqual('engine1, engine2, engine3, +1'); + expect(fieldTypes.at(1).text()).toEqual('number'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx new file mode 100644 index 0000000000000..2367ad4e0c53e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { useValues } from 'kea'; + +import { + EuiTable, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableBody, + EuiTableRow, + EuiTableRowCell, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_NAME, FIELD_TYPE } from '../../../../shared/schema/constants'; +import { ENGINES_TITLE } from '../../engines'; + +import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic'; + +import { TruncatedEnginesList } from './'; + +export const MetaEnginesSchemaTable: React.FC = () => { + const { schema, fields } = useValues(MetaEngineSchemaLogic); + + return ( + + + {FIELD_NAME} + {ENGINES_TITLE} + {FIELD_TYPE} + + + + + + id + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.allEngines', + { defaultMessage: 'All' } + )} + + + + + {Object.keys(fields).map((fieldName) => { + const fieldType = schema[fieldName]; + const engines = fields[fieldName][fieldType]; + + return ( + + + {fieldName} + + + + + + {fieldType} + + + ); + })} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx new file mode 100644 index 0000000000000..193d727be00b5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { shallow } from 'enzyme'; + +import { TruncatedEnginesList } from './'; + +describe('TruncatedEnginesList', () => { + it('renders a list of engines with links to their schema pages', () => { + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="displayedEngine"]')).toHaveLength(3); + expect(wrapper.find('[data-test-subj="displayedEngine"]').first().prop('to')).toEqual( + '/engines/engine1/schema' + ); + }); + + it('renders a tooltip when the number of engines is greater than the cutoff', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="displayedEngine"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="hiddenEnginesTooltip"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="hiddenEnginesTooltip"]').prop('content')).toEqual( + 'engine2, engine3' + ); + }); + + it('does not render if no engines are passed', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx new file mode 100644 index 0000000000000..a642eb99e3563 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.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 React, { Fragment } from 'react'; + +import { EuiText, EuiToolTip, EuiButtonEmpty } from '@elastic/eui'; + +import { EuiLinkTo } from '../../../../shared/react_router_helpers'; +import { ENGINE_SCHEMA_PATH } from '../../../routes'; +import { generateEncodedPath } from '../../../utils/encode_path_params'; + +interface Props { + engines?: string[]; + cutoff?: number; +} + +export const TruncatedEnginesList: React.FC = ({ engines, cutoff = 3 }) => { + if (!engines?.length) return null; + + const displayedEngines = engines.slice(0, cutoff); + const hiddenEngines = engines.slice(cutoff); + const SEPARATOR = ', '; + + return ( + + {displayedEngines.map((engineName, i) => { + const isLast = i === displayedEngines.length - 1; + return ( + + + {engineName} + + {!isLast ? SEPARATOR : ''} + + ); + })} + {hiddenEngines.length > 0 && ( + <> + {SEPARATOR} + + + +{hiddenEngines.length} + + + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx index a6e9eef8efa70..b1322c148b577 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx @@ -12,8 +12,12 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { EuiCallOut } from '@elastic/eui'; + import { Loading } from '../../../../shared/loading'; +import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components'; + import { MetaEngineSchema } from './'; describe('MetaEngineSchema', () => { @@ -33,8 +37,7 @@ describe('MetaEngineSchema', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBe(false); - // TODO: Check for schema components + expect(wrapper.find(MetaEnginesSchemaTable)).toHaveLength(1); }); it('calls loadSchema on mount', () => { @@ -49,4 +52,12 @@ describe('MetaEngineSchema', () => { expect(wrapper.find(Loading)).toHaveLength(1); }); + + it('renders an inactive fields callout & table when source engines have schema conflicts', () => { + setMockValues({ ...values, hasConflicts: true, conflictingFieldsCount: 5 }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find(MetaEnginesConflictsTable)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx index 234fcdb5a5a50..4c0235cf81129 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx @@ -9,17 +9,19 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { EuiPageHeader, EuiPageContentBody } from '@elastic/eui'; +import { EuiPageHeader, EuiPageContentBody, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FlashMessages } from '../../../../shared/flash_messages'; import { Loading } from '../../../../shared/loading'; +import { DataPanel } from '../../data_panel'; +import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components'; import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic'; export const MetaEngineSchema: React.FC = () => { const { loadSchema } = useActions(MetaEngineSchemaLogic); - const { dataLoading } = useValues(MetaEngineSchemaLogic); + const { dataLoading, hasConflicts, conflictingFieldsCount } = useValues(MetaEngineSchemaLogic); useEffect(() => { loadSchema(); @@ -40,7 +42,75 @@ export const MetaEngineSchema: React.FC = () => { )} /> - TODO + + {hasConflicts && ( + <> + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.conflictsCalloutDescription', + { + defaultMessage: + 'The field(s) have an inconsistent field-type across the source engines that make up this meta engine. Apply a consistent field-type from the source engines to make these fields searchable.', + } + )} +

+
+ + + )} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle', + { defaultMessage: 'Active fields' } + )} + + } + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription', + { defaultMessage: 'Fields which belong to one or more engine.' } + )} + > + + + + {hasConflicts && ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle', + { defaultMessage: 'Inactive fields' } + )} + + } + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription', + { + defaultMessage: + 'These fields have type conflicts. To activate these fields, change types in the source engines to match.', + } + )} + > + + + )} +
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx new file mode 100644 index 0000000000000..43a4682849c78 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.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 { setMockActions } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton } from '@elastic/eui'; + +import { AddSourceEnginesButton } from './add_source_engines_button'; + +describe('AddSourceEnginesButton', () => { + const MOCK_ACTIONS = { + openModal: jest.fn(), + }; + + it('opens the modal on click', () => { + setMockActions(MOCK_ACTIONS); + + const wrapper = shallow(); + const button = wrapper.find(EuiButton); + + expect(button).toHaveLength(1); + + button.simulate('click'); + + expect(MOCK_ACTIONS.openModal).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx new file mode 100644 index 0000000000000..004217d88987b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.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 from 'react'; + +import { useActions } from 'kea'; + +import { EuiButton } from '@elastic/eui'; + +import { ADD_SOURCE_ENGINES_BUTTON_LABEL } from '../i18n'; +import { SourceEnginesLogic } from '../source_engines_logic'; + +export const AddSourceEnginesButton: React.FC = () => { + const { openModal } = useActions(SourceEnginesLogic); + + return ( + + {ADD_SOURCE_ENGINES_BUTTON_LABEL} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx new file mode 100644 index 0000000000000..19c2f72ed6f52 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions, setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton, EuiButtonEmpty, EuiComboBox, EuiModal } from '@elastic/eui'; + +import { AddSourceEnginesModal } from './add_source_engines_modal'; + +describe('AddSourceEnginesModal', () => { + const MOCK_VALUES = { + selectableEngineNames: ['source-engine-1', 'source-engine-2', 'source-engine-3'], + selectedEngineNamesToAdd: ['source-engine-2'], + modalLoading: false, + }; + + const MOCK_ACTIONS = { + addSourceEngines: jest.fn(), + closeModal: jest.fn(), + onAddEnginesSelection: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(MOCK_VALUES); + setMockActions(MOCK_ACTIONS); + }); + + it('calls closeAddSourceEnginesModal when the modal is closed', () => { + const wrapper = shallow(); + wrapper.find(EuiModal).simulate('close'); + + expect(MOCK_ACTIONS.closeModal).toHaveBeenCalled(); + }); + + describe('combo box', () => { + it('has the proper options and selected options', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiComboBox).prop('options')).toEqual([ + { label: 'source-engine-1' }, + { label: 'source-engine-2' }, + { label: 'source-engine-3' }, + ]); + expect(wrapper.find(EuiComboBox).prop('selectedOptions')).toEqual([ + { label: 'source-engine-2' }, + ]); + }); + + it('calls setSelectedEngineNamesToAdd when changed', () => { + const wrapper = shallow(); + wrapper.find(EuiComboBox).simulate('change', [{ label: 'source-engine-3' }]); + + expect(MOCK_ACTIONS.onAddEnginesSelection).toHaveBeenCalledWith(['source-engine-3']); + }); + }); + + describe('cancel button', () => { + it('calls closeModal when clicked', () => { + const wrapper = shallow(); + wrapper.find(EuiButtonEmpty).simulate('click'); + + expect(MOCK_ACTIONS.closeModal).toHaveBeenCalled(); + }); + }); + + describe('save button', () => { + it('is disabled when user has selected no engines', () => { + setMockValues({ + ...MOCK_VALUES, + selectedEngineNamesToAdd: [], + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('disabled')).toEqual(true); + }); + + it('passes modalLoading state', () => { + setMockValues({ + ...MOCK_VALUES, + modalLoading: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('isLoading')).toEqual(true); + }); + + it('calls addSourceEngines when clicked', () => { + const wrapper = shallow(); + wrapper.find(EuiButton).simulate('click'); + + expect(MOCK_ACTIONS.addSourceEngines).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx new file mode 100644 index 0000000000000..24e27e03818ad --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiComboBox, + EuiModalFooter, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { CANCEL_BUTTON_LABEL, SAVE_BUTTON_LABEL } from '../../../../shared/constants'; + +import { + ADD_SOURCE_ENGINES_MODAL_TITLE, + ADD_SOURCE_ENGINES_MODAL_DESCRIPTION, + ADD_SOURCE_ENGINES_PLACEHOLDER, +} from '../i18n'; +import { SourceEnginesLogic } from '../source_engines_logic'; + +export const AddSourceEnginesModal: React.FC = () => { + const { addSourceEngines, closeModal, onAddEnginesSelection } = useActions(SourceEnginesLogic); + const { selectableEngineNames, selectedEngineNamesToAdd, modalLoading } = useValues( + SourceEnginesLogic + ); + + return ( + + + {ADD_SOURCE_ENGINES_MODAL_TITLE} + + + {ADD_SOURCE_ENGINES_MODAL_DESCRIPTION} + + ({ label: engineName }))} + selectedOptions={selectedEngineNamesToAdd.map((engineName) => ({ label: engineName }))} + onChange={(options) => onAddEnginesSelection(options.map((option) => option.label))} + placeholder={ADD_SOURCE_ENGINES_PLACEHOLDER} + /> + + + {CANCEL_BUTTON_LABEL} + addSourceEngines(selectedEngineNamesToAdd)} + fill + > + {SAVE_BUTTON_LABEL} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts new file mode 100644 index 0000000000000..edec07a70a0bf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AddSourceEnginesButton } from './add_source_engines_button'; +export { AddSourceEnginesModal } from './add_source_engines_modal'; +export { SourceEnginesTable } from './source_engines_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx new file mode 100644 index 0000000000000..895c7ab35e86a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mountWithIntl, setMockActions, setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiInMemoryTable, EuiButtonIcon } from '@elastic/eui'; + +import { SourceEnginesTable } from './source_engines_table'; + +describe('SourceEnginesTable', () => { + const MOCK_VALUES = { + // AppLogic + myRole: { + canManageMetaEngineSourceEngines: true, + }, + // SourceEnginesLogic + sourceEngines: [{ name: 'source-engine-1', document_count: 15, field_count: 26 }], + }; + + const MOCK_ACTIONS = { + removeSourceEngine: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(MOCK_ACTIONS); + setMockValues(MOCK_VALUES); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1); + }); + + it('contains relevant informatiom from source engines', () => { + const wrapper = mountWithIntl(); + + expect(wrapper.find(EuiInMemoryTable).text()).toContain('source-engine-1'); + expect(wrapper.find(EuiInMemoryTable).text()).toContain('15'); + expect(wrapper.find(EuiInMemoryTable).text()).toContain('26'); + }); + + describe('actions column', () => { + it('clicking a remove engine link calls a confirm dialogue before remove the engine', () => { + const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValueOnce(true); + const wrapper = mountWithIntl(); + + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(confirmSpy).toHaveBeenCalled(); + expect(MOCK_ACTIONS.removeSourceEngine).toHaveBeenCalled(); + }); + + it('does not remove an engine if the user cancels the confirmation dialog', () => { + const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValueOnce(false); + const wrapper = mountWithIntl(); + + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(confirmSpy).toHaveBeenCalled(); + expect(MOCK_ACTIONS.removeSourceEngine).not.toHaveBeenCalled(); + }); + + it('does not render the actions column if the user does not have permission to manage the engine', () => { + setMockValues({ + ...MOCK_VALUES, + myRole: { canManageMetaEngineSourceEngines: false }, + }); + const wrapper = mountWithIntl(); + + expect(wrapper.find(EuiButtonIcon)).toHaveLength(0); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx new file mode 100644 index 0000000000000..f8c3e3ca00c95 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiBasicTableColumn, EuiInMemoryTable } from '@elastic/eui'; + +import { EuiLinkTo } from '../../../../shared/react_router_helpers'; +import { AppLogic } from '../../../app_logic'; +import { ENGINE_PATH } from '../../../routes'; +import { generateEncodedPath } from '../../../utils/encode_path_params'; +import { EngineDetails } from '../../engine/types'; +import { + NAME_COLUMN, + DOCUMENT_COUNT_COLUMN, + FIELD_COUNT_COLUMN, + ACTIONS_COLUMN, +} from '../../engines/components/tables/shared_columns'; + +import { REMOVE_SOURCE_ENGINE_BUTTON_LABEL, REMOVE_SOURCE_ENGINE_CONFIRM_DIALOGUE } from '../i18n'; +import { SourceEnginesLogic } from '../source_engines_logic'; + +export const SourceEnginesTable: React.FC = () => { + const { + myRole: { canManageMetaEngineSourceEngines }, + } = useValues(AppLogic); + + const { removeSourceEngine } = useActions(SourceEnginesLogic); + const { sourceEngines } = useValues(SourceEnginesLogic); + + const columns: Array> = [ + { + ...NAME_COLUMN, + render: (engineName: string) => ( + {engineName} + ), + }, + DOCUMENT_COUNT_COLUMN, + FIELD_COUNT_COLUMN, + ]; + if (canManageMetaEngineSourceEngines) { + columns.push({ + name: ACTIONS_COLUMN.name, + actions: [ + { + name: REMOVE_SOURCE_ENGINE_BUTTON_LABEL, + description: REMOVE_SOURCE_ENGINE_BUTTON_LABEL, + type: 'icon', + icon: 'trash', + color: 'danger', + onClick: (engine: EngineDetails) => { + if (confirm(REMOVE_SOURCE_ENGINE_CONFIRM_DIALOGUE(engine.name))) { + removeSourceEngine(engine.name); + } + }, + }, + ], + }); + } + + return ( + 10} + search={{ box: { incremental: true } }} + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts new file mode 100644 index 0000000000000..4e3f4f81d5a9f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const SOURCE_ENGINES_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.title', + { defaultMessage: 'Manage engines' } +); + +export const ADD_SOURCE_ENGINES_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesButtonLabel', + { defaultMessage: 'Add engines' } +); + +export const ADD_SOURCE_ENGINES_MODAL_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesModal.title', + { defaultMessage: 'Add engines' } +); + +export const ADD_SOURCE_ENGINES_MODAL_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesModal.description', + { defaultMessage: 'Add additional engines to this meta engine.' } +); + +export const ADD_SOURCE_ENGINES_PLACEHOLDER = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesPlaceholder', + { defaultMessage: 'Select engine(s)' } +); + +export const ADD_SOURCE_ENGINES_SUCCESS_MESSAGE = (sourceEngineNames: string[]) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesSuccessMessage', + { + defaultMessage: + '{sourceEnginesCount, plural, one {# engine has} other {# engines have}} been added to this meta engine.', + values: { sourceEnginesCount: sourceEngineNames.length }, + } + ); + +export const REMOVE_SOURCE_ENGINE_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.sourceEngines.removeEngineButton.label', + { defaultMessage: 'Remove from meta engine' } +); + +export const REMOVE_SOURCE_ENGINE_CONFIRM_DIALOGUE = (engineName: string) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.sourceEngines.removeEngineConfirmDialogue.description', + { + defaultMessage: + 'This will remove the engine, {engineName}, from this meta engine. All existing settings will be lost. Are you sure?', + values: { engineName }, + } + ); + +export const REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE = (engineName: string) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.removeSourceEngineSuccessMessage', + { + defaultMessage: 'Engine {engineName} has been removed from this meta engine.', + values: { engineName }, + } + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx index 4bf62de408a2b..8cfcaeec97b87 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx @@ -5,52 +5,88 @@ * 2.0. */ -import '../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../__mocks__'; +import '../../../__mocks__/shallow_useeffect.mock'; import '../../__mocks__/engine_logic.mock'; import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiCodeBlock } from '@elastic/eui'; +import { EuiPageHeader } from '@elastic/eui'; import { Loading } from '../../../shared/loading'; +import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components'; + import { SourceEngines } from '.'; -const MOCK_ACTIONS = { - // SourceEnginesLogic - fetchSourceEngines: jest.fn(), -}; +describe('SourceEngines', () => { + const MOCK_ACTIONS = { + fetchIndexedEngines: jest.fn(), + fetchSourceEngines: jest.fn(), + }; -const MOCK_VALUES = { - dataLoading: false, - sourceEngines: [], -}; + const MOCK_VALUES = { + // AppLogic + myRole: { + canManageMetaEngineSourceEngines: true, + }, + // SourceEnginesLogic + dataLoading: false, + isModalOpen: false, + }; -describe('SourceEngines', () => { beforeEach(() => { jest.clearAllMocks(); + setMockValues(MOCK_VALUES); setMockActions(MOCK_ACTIONS); }); - describe('non-happy-path states', () => { - it('renders a loading component before data has loaded', () => { - setMockValues({ ...MOCK_VALUES, dataLoading: true }); - const wrapper = shallow(); + it('renders and calls a function to initialize data', () => { + const wrapper = shallow(); + + expect(wrapper.find(SourceEnginesTable)).toHaveLength(1); + expect(MOCK_ACTIONS.fetchIndexedEngines).toHaveBeenCalled(); + expect(MOCK_ACTIONS.fetchSourceEngines).toHaveBeenCalled(); + }); - expect(wrapper.find(Loading)).toHaveLength(1); + it('renders the add source engines modal', () => { + setMockValues({ + ...MOCK_VALUES, + isModalOpen: true, }); + const wrapper = shallow(); + + expect(wrapper.find(AddSourceEnginesModal)).toHaveLength(1); + }); + + it('renders a loading component before data has loaded', () => { + setMockValues({ ...MOCK_VALUES, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); }); - describe('happy-path states', () => { - it('renders and calls a function to initialize data', () => { - setMockValues(MOCK_VALUES); + describe('page actions', () => { + const getPageHeader = (wrapper: ShallowWrapper) => + wrapper.find(EuiPageHeader).dive().children().dive(); + + it('contains a button to add source engines', () => { + const wrapper = shallow(); + expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(1); + }); + + it('hides the add source engines button if the user does not have permissions', () => { + setMockValues({ + ...MOCK_VALUES, + myRole: { + canManageMetaEngineSourceEngines: false, + }, + }); const wrapper = shallow(); - expect(wrapper.find(EuiCodeBlock)).toHaveLength(1); - expect(MOCK_ACTIONS.fetchSourceEngines).toHaveBeenCalled(); + expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(0); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx index 0b68eb5fd2c2e..190c44c919020 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx @@ -9,29 +9,27 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiCodeBlock, EuiPageHeader } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; +import { EuiPageHeader, EuiPageContent } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { Loading } from '../../../shared/loading'; +import { AppLogic } from '../../app_logic'; import { getEngineBreadcrumbs } from '../engine'; +import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components'; +import { SOURCE_ENGINES_TITLE } from './i18n'; import { SourceEnginesLogic } from './source_engines_logic'; -const SOURCE_ENGINES_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.souceEngines.title', - { - defaultMessage: 'Manage engines', - } -); - export const SourceEngines: React.FC = () => { - const { fetchSourceEngines } = useActions(SourceEnginesLogic); - const { dataLoading, sourceEngines } = useValues(SourceEnginesLogic); + const { + myRole: { canManageMetaEngineSourceEngines }, + } = useValues(AppLogic); + const { fetchIndexedEngines, fetchSourceEngines } = useActions(SourceEnginesLogic); + const { dataLoading, isModalOpen } = useValues(SourceEnginesLogic); useEffect(() => { + fetchIndexedEngines(); fetchSourceEngines(); }, []); @@ -40,9 +38,15 @@ export const SourceEngines: React.FC = () => { return ( <> - + ] : []} + /> - {JSON.stringify(sourceEngines, null, 2)} + + + {isModalOpen && } + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts index df1165620adc3..49886f1257a58 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts @@ -6,129 +6,372 @@ */ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; +import { mockRecursivelyFetchEngines } from '../../__mocks__'; import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { EngineLogic } from '../engine'; import { EngineDetails } from '../engine/types'; import { SourceEnginesLogic } from './source_engines_logic'; -const DEFAULT_VALUES = { - dataLoading: true, - sourceEngines: [], -}; - describe('SourceEnginesLogic', () => { const { http } = mockHttpValues; const { mount } = new LogicMounter(SourceEnginesLogic); - const { flashAPIErrors } = mockFlashMessageHelpers; + const { flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + + const DEFAULT_VALUES = { + dataLoading: true, + modalLoading: false, + isModalOpen: false, + indexedEngines: [], + indexedEngineNames: [], + sourceEngines: [], + sourceEngineNames: [], + selectedEngineNamesToAdd: [], + selectableEngineNames: [], + }; beforeEach(() => { jest.clearAllMocks(); - mount(); }); it('initializes with default values', () => { + mount(); expect(SourceEnginesLogic.values).toEqual(DEFAULT_VALUES); }); - describe('setSourceEngines', () => { - beforeEach(() => { - SourceEnginesLogic.actions.onSourceEnginesFetch([ - { name: 'source-engine-1' }, - { name: 'source-engine-2' }, - ] as EngineDetails[]); + describe('actions', () => { + describe('closeModal', () => { + it('sets isModalOpen and modalLoading to false', () => { + mount({ + isModalOpen: true, + modalLoading: true, + }); + + SourceEnginesLogic.actions.closeModal(); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + isModalOpen: false, + modalLoading: false, + }); + }); }); - it('sets the source engines', () => { - expect(SourceEnginesLogic.values.sourceEngines).toEqual([ - { name: 'source-engine-1' }, - { name: 'source-engine-2' }, - ]); + describe('openModal', () => { + it('sets isModalOpen to true', () => { + mount({ + isModalOpen: false, + }); + + SourceEnginesLogic.actions.openModal(); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + isModalOpen: true, + }); + }); }); - it('sets dataLoading to false', () => { - expect(SourceEnginesLogic.values.dataLoading).toEqual(false); + describe('onAddEnginesSelection', () => { + it('sets selectedEngineNamesToAdd to the specified value', () => { + mount(); + + SourceEnginesLogic.actions.onAddEnginesSelection(['source-engine-1', 'source-engine-2']); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + selectedEngineNamesToAdd: ['source-engine-1', 'source-engine-2'], + }); + }); + }); + + describe('setIndexedEngines', () => { + it('sets indexedEngines to the specified value', () => { + mount(); + + SourceEnginesLogic.actions.setIndexedEngines([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ] as EngineDetails[]); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + indexedEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], + // Selectors + indexedEngineNames: ['source-engine-1', 'source-engine-2'], + selectableEngineNames: ['source-engine-1', 'source-engine-2'], + }); + }); + }); + + describe('onSourceEnginesFetch', () => { + it('sets sourceEngines to the specified value and dataLoading to false', () => { + mount(); + + SourceEnginesLogic.actions.onSourceEnginesFetch([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ] as EngineDetails[]); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + sourceEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], + // Selectors + sourceEngineNames: ['source-engine-1', 'source-engine-2'], + }); + }); + }); + + describe('onSourceEnginesAdd', () => { + it('adds to the existing sourceEngines', () => { + mount({ + sourceEngines: [ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ] as EngineDetails[], + }); + + SourceEnginesLogic.actions.onSourceEnginesAdd([ + { name: 'source-engine-3' }, + { name: 'source-engine-4' }, + ] as EngineDetails[]); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + sourceEngines: [ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + { name: 'source-engine-3' }, + { name: 'source-engine-4' }, + ], + // Selectors + sourceEngineNames: [ + 'source-engine-1', + 'source-engine-2', + 'source-engine-3', + 'source-engine-4', + ], + }); + }); + }); + + describe('onSourceEngineRemove', () => { + it('removes an item from the existing sourceEngines', () => { + mount({ + sourceEngines: [ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + { name: 'source-engine-3' }, + ] as EngineDetails[], + }); + + SourceEnginesLogic.actions.onSourceEngineRemove('source-engine-2'); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + sourceEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-3' }], + // Selectors + sourceEngineNames: ['source-engine-1', 'source-engine-3'], + }); + }); }); }); - describe('fetchSourceEngines', () => { - it('calls addSourceEngines and displayRow when it has retrieved all pages', async () => { - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 1, - }, - }, - results: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - }) - ); - jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesFetch'); - - SourceEnginesLogic.actions.fetchSourceEngines(); - await nextTick(); - - expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/source_engines', { - query: { - 'page[current]': 1, - 'page[size]': 25, - }, - }); - expect(SourceEnginesLogic.actions.onSourceEnginesFetch).toHaveBeenCalledWith([ - { name: 'source-engine-1' }, - { name: 'source-engine-2' }, - ]); + describe('selectors', () => { + describe('indexedEngineNames', () => { + it('returns a flat array of `indexedEngine.name`s', () => { + mount({ + indexedEngines: [{ name: 'a' }, { name: 'b' }, { name: 'c' }], + }); + + expect(SourceEnginesLogic.values.indexedEngineNames).toEqual(['a', 'b', 'c']); + }); }); - it('display a flash message on error', async () => { - http.get.mockReturnValueOnce(Promise.reject()); - mount(); + describe('sourceEngineNames', () => { + it('returns a flat array of `sourceEngine.name`s', () => { + mount({ + sourceEngines: [{ name: 'd' }, { name: 'e' }], + }); + + expect(SourceEnginesLogic.values.sourceEngineNames).toEqual(['d', 'e']); + }); + }); - SourceEnginesLogic.actions.fetchSourceEngines(); - await nextTick(); + describe('selectableEngineNames', () => { + it('returns a flat list of indexedEngineNames that are not already in sourceEngineNames', () => { + mount({ + indexedEngines: [{ name: 'a' }, { name: 'b' }, { name: 'c' }], + sourceEngines: [{ name: 'a' }, { name: 'b' }], + }); - expect(flashAPIErrors).toHaveBeenCalledTimes(1); + expect(SourceEnginesLogic.values.selectableEngineNames).toEqual(['c']); + }); }); + }); + + describe('listeners', () => { + describe('fetchSourceEngines', () => { + it('calls onSourceEnginesFetch with all recursively fetched engines', () => { + jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesFetch'); - it('recursively fetches a number of pages', async () => { - mount(); - jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesFetch'); - - // First page - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 2, - }, - }, - results: [{ name: 'source-engine-1' }], - }) - ); - - // Second and final page - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 2, - }, - }, - results: [{ name: 'source-engine-2' }], - }) - ); - - SourceEnginesLogic.actions.fetchSourceEngines(); - await nextTick(); - - expect(SourceEnginesLogic.actions.onSourceEnginesFetch).toHaveBeenCalledWith([ - // First page - { name: 'source-engine-1' }, - // Second and final page - { name: 'source-engine-2' }, - ]); + SourceEnginesLogic.actions.fetchSourceEngines(); + + expect(mockRecursivelyFetchEngines).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: '/api/app_search/engines/some-engine/source_engines', + }) + ); + expect(SourceEnginesLogic.actions.onSourceEnginesFetch).toHaveBeenCalledWith([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ]); + }); + }); + + describe('fetchIndexedEngines', () => { + it('calls setIndexedEngines with all recursively fetched engines', () => { + jest.spyOn(SourceEnginesLogic.actions, 'setIndexedEngines'); + + SourceEnginesLogic.actions.fetchIndexedEngines(); + + expect(mockRecursivelyFetchEngines).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: '/api/app_search/engines', + query: { type: 'indexed' }, + }) + ); + expect(SourceEnginesLogic.actions.setIndexedEngines).toHaveBeenCalledWith([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ]); + }); + }); + + describe('addSourceEngines', () => { + it('sets modalLoading to true', () => { + mount({ modalLoading: false }); + + SourceEnginesLogic.actions.addSourceEngines([]); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + modalLoading: true, + }); + }); + + describe('on success', () => { + beforeEach(() => { + http.post.mockReturnValue(Promise.resolve()); + mount({ + indexedEngines: [{ name: 'source-engine-3' }, { name: 'source-engine-4' }], + }); + }); + + it('calls the bulk endpoint, adds source engines to state, and shows a success message', async () => { + jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesAdd'); + + SourceEnginesLogic.actions.addSourceEngines(['source-engine-3', 'source-engine-4']); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/source_engines/bulk_create', + { + body: JSON.stringify({ source_engine_slugs: ['source-engine-3', 'source-engine-4'] }), + } + ); + expect(SourceEnginesLogic.actions.onSourceEnginesAdd).toHaveBeenCalledWith([ + { name: 'source-engine-3' }, + { name: 'source-engine-4' }, + ]); + expect(setSuccessMessage).toHaveBeenCalledWith( + '2 engines have been added to this meta engine.' + ); + }); + + it('re-initializes the engine and closes the modal', async () => { + jest.spyOn(EngineLogic.actions, 'initializeEngine'); + jest.spyOn(SourceEnginesLogic.actions, 'closeModal'); + + SourceEnginesLogic.actions.addSourceEngines([]); + await nextTick(); + + expect(EngineLogic.actions.initializeEngine).toHaveBeenCalled(); + expect(SourceEnginesLogic.actions.closeModal).toHaveBeenCalled(); + }); + }); + + describe('on error', () => { + beforeEach(() => { + http.post.mockReturnValue(Promise.reject()); + mount(); + }); + + it('flashes errors and closes the modal', async () => { + jest.spyOn(SourceEnginesLogic.actions, 'closeModal'); + + SourceEnginesLogic.actions.addSourceEngines([]); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledTimes(1); + expect(SourceEnginesLogic.actions.closeModal).toHaveBeenCalled(); + }); + }); + }); + + describe('removeSourceEngine', () => { + describe('on success', () => { + beforeEach(() => { + http.delete.mockReturnValue(Promise.resolve()); + mount(); + }); + + it('calls the delete endpoint and removes source engines from state', async () => { + jest.spyOn(SourceEnginesLogic.actions, 'onSourceEngineRemove'); + + SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); + await nextTick(); + + expect(http.delete).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/source_engines/source-engine-2' + ); + expect(SourceEnginesLogic.actions.onSourceEngineRemove).toHaveBeenCalledWith( + 'source-engine-2' + ); + }); + + it('shows a success message', async () => { + SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); + await nextTick(); + + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Engine source-engine-2 has been removed from this meta engine.' + ); + }); + + it('re-initializes the engine', async () => { + jest.spyOn(EngineLogic.actions, 'initializeEngine'); + + SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); + await nextTick(); + + expect(EngineLogic.actions.initializeEngine).toHaveBeenCalledWith(); + }); + }); + + it('displays a flash message on error', async () => { + http.delete.mockReturnValueOnce(Promise.reject()); + mount(); + + SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledTimes(1); + }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts index b8a5c7c359518..c10f11a7de327 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts @@ -4,24 +4,47 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { kea, MakeLogicType } from 'kea'; -import { flashAPIErrors } from '../../../shared/flash_messages'; +import { flashAPIErrors, setSuccessMessage } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; +import { recursivelyFetchEngines } from '../../utils/recursively_fetch_engines'; import { EngineLogic } from '../engine'; import { EngineDetails } from '../engine/types'; -import { EnginesAPIResponse } from '../engines/types'; -interface SourceEnginesLogicValues { +import { ADD_SOURCE_ENGINES_SUCCESS_MESSAGE, REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE } from './i18n'; + +export interface SourceEnginesLogicValues { dataLoading: boolean; + modalLoading: boolean; + isModalOpen: boolean; + indexedEngines: EngineDetails[]; + indexedEngineNames: string[]; sourceEngines: EngineDetails[]; + sourceEngineNames: string[]; + selectableEngineNames: string[]; + selectedEngineNamesToAdd: string[]; } interface SourceEnginesLogicActions { + addSourceEngines: (sourceEngineNames: string[]) => { sourceEngineNames: string[] }; + fetchIndexedEngines: () => void; fetchSourceEngines: () => void; + onSourceEngineRemove: (sourceEngineNameToRemove: string) => { sourceEngineNameToRemove: string }; + onSourceEnginesAdd: ( + sourceEnginesToAdd: EngineDetails[] + ) => { sourceEnginesToAdd: EngineDetails[] }; onSourceEnginesFetch: ( sourceEngines: SourceEnginesLogicValues['sourceEngines'] ) => { sourceEngines: SourceEnginesLogicValues['sourceEngines'] }; + removeSourceEngine: (sourceEngineName: string) => { sourceEngineName: string }; + setIndexedEngines: (indexedEngines: EngineDetails[]) => { indexedEngines: EngineDetails[] }; + openModal: () => void; + closeModal: () => void; + onAddEnginesSelection: ( + selectedEngineNamesToAdd: string[] + ) => { selectedEngineNamesToAdd: string[] }; } export const SourceEnginesLogic = kea< @@ -29,8 +52,17 @@ export const SourceEnginesLogic = kea< >({ path: ['enterprise_search', 'app_search', 'source_engines_logic'], actions: () => ({ + addSourceEngines: (sourceEngineNames) => ({ sourceEngineNames }), + fetchIndexedEngines: true, fetchSourceEngines: true, + onSourceEngineRemove: (sourceEngineNameToRemove) => ({ sourceEngineNameToRemove }), + onSourceEnginesAdd: (sourceEnginesToAdd) => ({ sourceEnginesToAdd }), onSourceEnginesFetch: (sourceEngines) => ({ sourceEngines }), + removeSourceEngine: (sourceEngineName) => ({ sourceEngineName }), + setIndexedEngines: (indexedEngines) => ({ indexedEngines }), + openModal: true, + closeModal: true, + onAddEnginesSelection: (selectedEngineNamesToAdd) => ({ selectedEngineNamesToAdd }), }), reducers: () => ({ dataLoading: [ @@ -39,47 +71,119 @@ export const SourceEnginesLogic = kea< onSourceEnginesFetch: () => false, }, ], + modalLoading: [ + false, + { + addSourceEngines: () => true, + closeModal: () => false, + }, + ], + isModalOpen: [ + false, + { + openModal: () => true, + closeModal: () => false, + }, + ], + indexedEngines: [ + [], + { + setIndexedEngines: (_, { indexedEngines }) => indexedEngines, + }, + ], + selectedEngineNamesToAdd: [ + [], + { + closeModal: () => [], + onAddEnginesSelection: (_, { selectedEngineNamesToAdd }) => selectedEngineNamesToAdd, + }, + ], sourceEngines: [ [], { + onSourceEnginesAdd: (sourceEngines, { sourceEnginesToAdd }) => [ + ...sourceEngines, + ...sourceEnginesToAdd, + ], onSourceEnginesFetch: (_, { sourceEngines }) => sourceEngines, + onSourceEngineRemove: (sourceEngines, { sourceEngineNameToRemove }) => + sourceEngines.filter((sourceEngine) => sourceEngine.name !== sourceEngineNameToRemove), }, ], }), - listeners: ({ actions }) => ({ - fetchSourceEngines: () => { + selectors: { + indexedEngineNames: [ + (selectors) => [selectors.indexedEngines], + (indexedEngines) => indexedEngines.map((engine: EngineDetails) => engine.name), + ], + sourceEngineNames: [ + (selectors) => [selectors.sourceEngines], + (sourceEngines) => sourceEngines.map((engine: EngineDetails) => engine.name), + ], + selectableEngineNames: [ + (selectors) => [selectors.indexedEngineNames, selectors.sourceEngineNames], + (indexedEngineNames, sourceEngineNames) => + indexedEngineNames.filter((engineName: string) => !sourceEngineNames.includes(engineName)), + ], + }, + listeners: ({ actions, values }) => ({ + addSourceEngines: async ({ sourceEngineNames }) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; - let enginesAccumulator: EngineDetails[] = []; + try { + await http.post(`/api/app_search/engines/${engineName}/source_engines/bulk_create`, { + body: JSON.stringify({ + source_engine_slugs: sourceEngineNames, + }), + }); + + const sourceEnginesToAdd = values.indexedEngines.filter(({ name }) => + sourceEngineNames.includes(name) + ); + + actions.onSourceEnginesAdd(sourceEnginesToAdd); + setSuccessMessage(ADD_SOURCE_ENGINES_SUCCESS_MESSAGE(sourceEngineNames)); + EngineLogic.actions.initializeEngine(); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.closeModal(); + } + }, + fetchSourceEngines: () => { + const { engineName } = EngineLogic.values; - // We need to recursively fetch all source engines because we put the data - // into an EuiInMemoryTable to enable searching - const recursiveFetchSourceEngines = async (page = 1) => { - try { - const { meta, results }: EnginesAPIResponse = await http.get( - `/api/app_search/engines/${engineName}/source_engines`, - { - query: { - 'page[current]': page, - 'page[size]': 25, - }, - } - ); + recursivelyFetchEngines({ + endpoint: `/api/app_search/engines/${engineName}/source_engines`, + onComplete: (engines) => actions.onSourceEnginesFetch(engines), + }); + }, + fetchIndexedEngines: () => { + recursivelyFetchEngines({ + endpoint: '/api/app_search/engines', + onComplete: (engines) => actions.setIndexedEngines(engines), + query: { type: 'indexed' }, + }); + }, + removeSourceEngine: async ({ sourceEngineName }) => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; - enginesAccumulator = [...enginesAccumulator, ...results]; + try { + await http.delete( + `/api/app_search/engines/${engineName}/source_engines/${sourceEngineName}` + ); - if (page >= meta.page.total_pages) { - actions.onSourceEnginesFetch(enginesAccumulator); - } else { - recursiveFetchSourceEngines(page + 1); - } - } catch (e) { - flashAPIErrors(e); - } - }; + actions.onSourceEngineRemove(sourceEngineName); + setSuccessMessage(REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE(sourceEngineName)); - recursiveFetchSourceEngines(); + // Changing source engines can change schema conflicts and invalid boosts, + // so we re-initialize the engine to re-fetch that data + EngineLogic.actions.initializeEngine(); // + } catch (e) { + flashAPIErrors(e); + } }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts new file mode 100644 index 0000000000000..104f98e45a5f5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; + +import { nextTick } from '@kbn/test/jest'; + +import { recursivelyFetchEngines } from './'; + +describe('recursivelyFetchEngines', () => { + const { http } = mockHttpValues; + const { flashAPIErrors } = mockFlashMessageHelpers; + + const MOCK_PAGE_1 = { + meta: { + page: { current: 1, total_pages: 3 }, + }, + results: [{ name: 'source-engine-1' }], + }; + const MOCK_PAGE_2 = { + meta: { + page: { current: 2, total_pages: 3 }, + }, + results: [{ name: 'source-engine-2' }], + }; + const MOCK_PAGE_3 = { + meta: { + page: { current: 3, total_pages: 3 }, + }, + results: [{ name: 'source-engine-3' }], + }; + const MOCK_CALLBACK = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('recursively calls the passed API endpoint and returns all engines to the onComplete callback', async () => { + http.get + .mockReturnValueOnce(Promise.resolve(MOCK_PAGE_1)) + .mockReturnValueOnce(Promise.resolve(MOCK_PAGE_2)) + .mockReturnValueOnce(Promise.resolve(MOCK_PAGE_3)); + + recursivelyFetchEngines({ + endpoint: '/api/app_search/engines/some-engine/source_engines', + onComplete: MOCK_CALLBACK, + }); + await nextTick(); + + expect(http.get).toHaveBeenCalledTimes(3); // Called once for each page + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/source_engines', { + query: { + 'page[current]': 1, + 'page[size]': 25, + }, + }); + + expect(MOCK_CALLBACK).toHaveBeenCalledWith([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + { name: 'source-engine-3' }, + ]); + }); + + it('passes optional query params', () => { + recursivelyFetchEngines({ + endpoint: '/api/app_search/engines/some-engine/engines', + onComplete: MOCK_CALLBACK, + query: { type: 'indexed' }, + }); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/engines', { + query: { + 'page[current]': 1, + 'page[size]': 25, + type: 'indexed', + }, + }); + }); + + it('passes optional custom page sizes', () => { + recursivelyFetchEngines({ + endpoint: '/over_9000', + onComplete: MOCK_CALLBACK, + pageSize: 9001, + }); + + expect(http.get).toHaveBeenCalledWith('/over_9000', { + query: { + 'page[current]': 1, + 'page[size]': 9001, + }, + }); + }); + + it('handles errors', async () => { + http.get.mockReturnValueOnce(Promise.reject('error')); + + recursivelyFetchEngines({ endpoint: '/error', onComplete: MOCK_CALLBACK }); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts new file mode 100644 index 0000000000000..797e89bd68b69 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; + +import { EngineDetails } from '../../components/engine/types'; +import { EnginesAPIResponse } from '../../components/engines/types'; + +interface Params { + endpoint: string; + onComplete: (engines: EngineDetails[]) => void; + query?: object; + pageSize?: number; +} + +export const recursivelyFetchEngines = ({ + endpoint, + onComplete, + query = {}, + pageSize = 25, +}: Params) => { + const { http } = HttpLogic.values; + + let enginesAccumulator: EngineDetails[] = []; + + const fetchEngines = async (page = 1) => { + try { + const { meta, results }: EnginesAPIResponse = await http.get(endpoint, { + query: { + 'page[current]': page, + 'page[size]': pageSize, + ...query, + }, + }); + + enginesAccumulator = [...enginesAccumulator, ...results]; + + if (page >= meta.page.total_pages) { + onComplete(enginesAccumulator); + } else { + fetchEngines(page + 1); + } + } catch (e) { + flashAPIErrors(e); + } + }; + + fetchEngines(); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts index bc4259fa37889..c653cad5c1c0d 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -259,47 +259,4 @@ describe('engine routes', () => { }); }); }); - - describe('GET /api/app_search/engines/{name}/source_engines', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/api/app_search/engines/{name}/source_engines', - }); - - registerEnginesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('validates correctly with name', () => { - const request = { params: { name: 'test-engine' } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with a non-string name', () => { - const request = { params: { name: 1 } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with missing query params', () => { - const request = { query: {} }; - mockRouter.shouldThrow(request); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:name/source_engines', - }); - }); - }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts index f6e9d30dd0ade..77b055add7d79 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -95,21 +95,4 @@ export function registerEnginesRoutes({ path: '/as/engines/:name/overview_metrics', }) ); - router.get( - { - path: '/api/app_search/engines/{name}/source_engines', - validate: { - params: schema.object({ - name: schema.string(), - }), - query: schema.object({ - 'page[current]': schema.number(), - 'page[size]': schema.number(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:name/source_engines', - }) - ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 99aaaeeec38b3..18de4580318a2 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -20,6 +20,7 @@ import { registerSchemaRoutes } from './schema'; import { registerSearchSettingsRoutes } from './search_settings'; import { registerSearchUIRoutes } from './search_ui'; import { registerSettingsRoutes } from './settings'; +import { registerSourceEnginesRoutes } from './source_engines'; import { registerSynonymsRoutes } from './synonyms'; export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { @@ -30,6 +31,7 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerDocumentsRoutes(dependencies); registerDocumentRoutes(dependencies); registerSchemaRoutes(dependencies); + registerSourceEnginesRoutes(dependencies); registerCurationsRoutes(dependencies); registerSynonymsRoutes(dependencies); registerSearchSettingsRoutes(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts new file mode 100644 index 0000000000000..5b51048067c00 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerSourceEnginesRoutes } from './source_engines'; + +describe('source engine routes', () => { + describe('GET /api/app_search/engines/{name}/source_engines', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{name}/source_engines', + }); + + registerSourceEnginesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('validates correctly with name', () => { + const request = { params: { name: 'test-engine' } }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without name', () => { + const request = { params: {} }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with a non-string name', () => { + const request = { params: { name: 1 } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with missing query params', () => { + const request = { query: {} }; + mockRouter.shouldThrow(request); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:name/source_engines', + }); + }); + }); + + describe('POST /api/app_search/engines/{name}/source_engines/bulk_create', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{name}/source_engines/bulk_create', + }); + + registerSourceEnginesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('validates correctly with name', () => { + const request = { params: { name: 'test-engine' }, body: { source_engine_slugs: [] } }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without name', () => { + const request = { params: {}, body: { source_engine_slugs: [] } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with a non-string name', () => { + const request = { params: { name: 1 }, body: { source_engine_slugs: [] } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with missing query params', () => { + const request = { params: { name: 'test-engine' }, body: {} }; + mockRouter.shouldThrow(request); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:name/source_engines/bulk_create', + }); + }); + }); + + describe('DELETE /api/app_search/engines/{name}/source_engines/{source_engine_name}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/app_search/engines/{name}/source_engines/{source_engine_name}', + }); + + registerSourceEnginesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('validates correctly with name and source_engine_name', () => { + const request = { params: { name: 'test-engine', source_engine_name: 'source-engine' } }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without name', () => { + const request = { params: { source_engine_name: 'source-engine' } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with a non-string name', () => { + const request = { params: { name: 1, source_engine_name: 'source-engine' } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation without source_engine_name', () => { + const request = { params: { name: 'test-engine' } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with a non-string source_engine_name', () => { + const request = { params: { name: 'test-engine', source_engine_name: 1 } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with missing query params', () => { + const request = { query: {} }; + mockRouter.shouldThrow(request); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:name/source_engines/:source_engine_name', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts new file mode 100644 index 0000000000000..8e55b0e6f1ac6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerSourceEnginesRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/engines/{name}/source_engines', + validate: { + params: schema.object({ + name: schema.string(), + }), + query: schema.object({ + 'page[current]': schema.number(), + 'page[size]': schema.number(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:name/source_engines', + }) + ); + + router.post( + { + path: '/api/app_search/engines/{name}/source_engines/bulk_create', + validate: { + params: schema.object({ + name: schema.string(), + }), + body: schema.object({ + source_engine_slugs: schema.arrayOf(schema.string()), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:name/source_engines/bulk_create', + }) + ); + + router.delete( + { + path: '/api/app_search/engines/{name}/source_engines/{source_engine_name}', + validate: { + params: schema.object({ + name: schema.string(), + source_engine_name: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:name/source_engines/:source_engine_name', + }) + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx index fc9504f003198..5efb4dbb44767 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import React from 'react'; import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '../column_types'; import { IndexPatternLayer } from '../../../types'; import { @@ -18,6 +19,7 @@ import { import { DEFAULT_TIME_SCALE } from '../../time_scale_utils'; import { OperationDefinition } from '..'; import { getFormatFromPreviousColumn, getFilter } from '../helpers'; +import { Markdown } from '../../../../../../../../src/plugins/kibana_react/public'; const ofName = buildLabelFunction((name?: string) => { return i18n.translate('xpack.lens.indexPattern.CounterRateOf', { @@ -118,4 +120,26 @@ export const counterRateOperation: OperationDefinition< }, timeScalingMode: 'mandatory', filterable: true, + documentation: { + section: 'calculation', + description: ( + + ), + }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx index 2adb9a1376f60..5c26f61bd6390 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import React from 'react'; import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '../column_types'; import { IndexPatternLayer } from '../../../types'; import { @@ -16,6 +17,7 @@ import { } from './utils'; import { OperationDefinition } from '..'; import { getFormatFromPreviousColumn, getFilter } from '../helpers'; +import { Markdown } from '../../../../../../../../src/plugins/kibana_react/public'; const ofName = (name?: string) => { return i18n.translate('xpack.lens.indexPattern.cumulativeSumOf', { @@ -111,4 +113,25 @@ export const cumulativeSumOperation: OperationDefinition< )?.join(', '); }, filterable: true, + documentation: { + section: 'calculation', + description: ( + + ), + }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx index 06555a9b41c2f..63e1b4bff648e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import React from 'react'; import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '../column_types'; import { IndexPatternLayer } from '../../../types'; import { @@ -18,6 +19,7 @@ import { import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; import { OperationDefinition } from '..'; import { getFormatFromPreviousColumn, getFilter } from '../helpers'; +import { Markdown } from '../../../../../../../../src/plugins/kibana_react/public'; const OPERATION_NAME = 'differences'; @@ -108,4 +110,25 @@ export const derivativeOperation: OperationDefinition< }, timeScalingMode: 'optional', filterable: true, + documentation: { + section: 'calculation', + description: ( + + ), + }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index 8d18a2752fd7e..afef871ee733a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -28,6 +28,7 @@ import { import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; import { HelpPopover, HelpPopoverButton } from '../../../help_popover'; import type { OperationDefinition, ParamEditorProps } from '..'; +import { Markdown } from '../../../../../../../../src/plugins/kibana_react/public'; const ofName = buildLabelFunction((name?: string) => { return i18n.translate('xpack.lens.indexPattern.movingAverageOf', { @@ -133,6 +134,29 @@ export const movingAverageOperation: OperationDefinition< }, timeScalingMode: 'optional', filterable: true, + documentation: { + section: 'calculation', + description: ( + + ), + }, }; function MovingAverageParamEditor({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index e77357a6f441a..8d990c7740ec5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -6,10 +6,12 @@ */ import { i18n } from '@kbn/i18n'; +import React from 'react'; import { AggFunctionsMapping } from '../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; +import { Markdown } from '../../../../../../../src/plugins/kibana_react/public'; import { getFormatFromPreviousColumn, @@ -109,4 +111,28 @@ export const cardinalityOperation: OperationDefinition + ), + }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index fd474ea04a165..ca1feed4c3af2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import React from 'react'; import { AggFunctionsMapping } from '../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; import { OperationDefinition } from './index'; @@ -16,6 +17,7 @@ import { adjustTimeScaleLabelSuffix, adjustTimeScaleOnOtherColumnChange, } from '../time_scale_utils'; +import { Markdown } from '../../../../../../../src/plugins/kibana_react/public'; const countLabel = i18n.translate('xpack.lens.indexPattern.countOf', { defaultMessage: 'Count of records', @@ -89,4 +91,28 @@ export const countOperation: OperationDefinition + ), + }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss index 40cdbd58c3acf..5e97f592a0474 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss @@ -30,6 +30,12 @@ padding: $euiSizeS; } +.lnsFormula__editorFooter { + // make sure docs are rendered in front of monaco + z-index: 1; + background-color: $euiColorLightestShade; +} + .lnsFormula__editorHeaderGroup, .lnsFormula__editorFooterGroup { display: block; // Overrides EUI's styling of `display: flex` on `EuiFlexItem` components @@ -65,6 +71,8 @@ .lnsFormula__docs--inline { display: flex; flex-direction: column; + // make sure docs are rendered in front of monaco + z-index: 1; } .lnsFormula__docsContent { @@ -84,6 +92,7 @@ } .lnsFormula__docsNav { + @include euiYScroll; background: $euiColorLightestShade; padding: $euiSizeS; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx index 615ca6623d57e..0366aeab2043e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx @@ -60,7 +60,7 @@ export function FormulaEditor({ }: ParamEditorProps) { const [text, setText] = useState(currentColumn.params.formula); const [warnings, setWarnings] = useState>([]); - const [isHelpOpen, setIsHelpOpen] = useState(false); + const [isHelpOpen, setIsHelpOpen] = useState(isFullscreen); const editorModel = React.useRef( monaco.editor.createModel(text ?? '', LANGUAGE_ID) ); @@ -566,11 +566,16 @@ export function FormulaEditor({ {isFullscreen ? ( - // TODO: Hook up the below `EuiLink` button so that it toggles the presence of the `.lnsFormula__docs--inline` element in fullscreen mode. Note that when docs are hidden, the `arrowDown` button should change to `arrowUp` and the label should change to `Show function reference`. @@ -580,9 +585,10 @@ export function FormulaEditor({ })} className="lnsFormula__editorHelp lnsFormula__editorHelp--inline" color="text" + onClick={() => setIsHelpOpen(!isHelpOpen)} > - + ) : ( @@ -615,6 +621,7 @@ export function FormulaEditor({ } > @@ -649,9 +656,10 @@ export function FormulaEditor({ - {isFullscreen ? ( + {isFullscreen && isHelpOpen ? (
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx index ef08b6d03a7b7..0ff120ea110db 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx @@ -5,56 +5,127 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiPopoverTitle, EuiText, - EuiSelectable, + EuiListGroupItem, EuiSelectableOption, + EuiListGroup, + EuiHorizontalRule, + EuiSpacer, } from '@elastic/eui'; import { Markdown } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { GenericOperationDefinition, ParamEditorProps } from '../../index'; +import { GenericOperationDefinition } from '../../index'; import { IndexPattern } from '../../../../types'; import { tinymathFunctions } from '../util'; import { getPossibleFunctions } from './math_completion'; -import { FormulaIndexPatternColumn } from '../formula'; - function FormulaHelp({ indexPattern, operationDefinitionMap, + isFullscreen, }: { indexPattern: IndexPattern; operationDefinitionMap: Record; + isFullscreen: boolean; }) { const [selectedFunction, setSelectedFunction] = useState(); + const scrollTargets = useRef>({}); + + useEffect(() => { + if (selectedFunction && scrollTargets.current[selectedFunction]) { + scrollTargets.current[selectedFunction].scrollIntoView(); + } + }, [selectedFunction]); const helpItems: Array = []; - helpItems.push({ label: 'Math', isGroupLabel: true }); + helpItems.push({ + label: i18n.translate('xpack.lens.formulaDocumentation.mathSection', { + defaultMessage: 'Math', + }), + isGroupLabel: true, + description: ( + + {i18n.translate('xpack.lens.formulaDocumentation.mathSectionDescription', { + defaultMessage: + 'These functions will be executed for reach row of the resulting table using single values from the same row calculated using other functions.', + })} + + ), + }); helpItems.push( ...getPossibleFunctions(indexPattern) .filter((key) => key in tinymathFunctions) + .sort() .map((key) => ({ label: `${key}`, description: , - checked: selectedFunction === key ? ('on' as const) : undefined, })) ); - helpItems.push({ label: 'Elasticsearch', isGroupLabel: true }); + helpItems.push({ + label: i18n.translate('xpack.lens.formulaDocumentation.elasticsearchSection', { + defaultMessage: 'Elasticsearch', + }), + isGroupLabel: true, + description: ( + + {i18n.translate('xpack.lens.formulaDocumentation.elasticsearchSectionDescription', { + defaultMessage: + 'These functions will be executed on the raw documents for each row of the resulting table, aggregating all documents matching the break down dimensions into a single value.', + })} + + ), + }); // Es aggs helpItems.push( ...getPossibleFunctions(indexPattern) - .filter((key) => key in operationDefinitionMap) + .filter( + (key) => + key in operationDefinitionMap && + operationDefinitionMap[key].documentation?.section === 'elasticsearch' + ) + .sort() .map((key) => ({ - label: `${key}: ${operationDefinitionMap[key].displayName}`, - description: getHelpText(key, operationDefinitionMap), + label: key, + description: operationDefinitionMap[key].documentation?.description, + })) + ); + + helpItems.push({ + label: i18n.translate('xpack.lens.formulaDocumentation.columnCalculationSection', { + defaultMessage: 'Column-wise calculation', + }), + isGroupLabel: true, + description: ( + + {i18n.translate('xpack.lens.formulaDocumentation.columnCalculationSectionDescription', { + defaultMessage: + 'These functions will be executed for reach row of the resulting table, using data from cells from other rows as well as the current value.', + })} + + ), + }); + + // Calculations aggs + helpItems.push( + ...getPossibleFunctions(indexPattern) + .filter( + (key) => + key in operationDefinitionMap && + operationDefinitionMap[key].documentation?.section === 'calculation' + ) + .sort() + .map((key) => ({ + label: key, + description: operationDefinitionMap[key].documentation?.description, checked: selectedFunction === `${key}: ${operationDefinitionMap[key].displayName}` ? ('on' as const) @@ -65,42 +136,49 @@ function FormulaHelp({ return ( <> - Formula reference + {i18n.translate('xpack.lens.formulaDocumentation.header', { + defaultMessage: 'Formula reference', + })} - { - const chosenType = newOptions.find(({ checked }) => checked === 'on')!; - if (!chosenType) { - setSelectedFunction(undefined); + + {helpItems.map((helpItem) => { + if (helpItem.isGroupLabel) { + return ( + { + setSelectedFunction(helpItem.label); + }} + /> + ); } else { - setSelectedFunction(chosenType.label); + return ( + { + setSelectedFunction(helpItem.label); + }} + /> + ); } - }} - > - {(list, search) => ( - <> - {search} - {list} - - )} - + })} + - {selectedFunction ? ( - helpItems.find(({ label }) => label === selectedFunction)?.description - ) : ( - - )} + description: + 'Text is in markdown. Do not translate function names or field names like sum(bytes)', + })} + /> + + {helpItems.map((item, index) => { + return ( +
{ + if (el) { + scrollTargets.current[item.label] = el; + } + }} + > + {item.isGroupLabel ? ( + +

{item.label}

+ {item.description} + +
+ ) : ( + + {item.description} + {helpItems.length - 1 !== index && } + + )} +
+ ); + })}
@@ -148,37 +249,3 @@ Use the symbols +, -, /, and * to perform basic math. } export const MemoizedFormulaHelp = React.memo(FormulaHelp); - -// TODO: i18n this whole thing, or move examples into the operation definitions with i18n -function getHelpText( - type: string, - operationDefinitionMap: ParamEditorProps['operationDefinitionMap'] -) { - const definition = operationDefinitionMap[type]; - - if (type === 'count') { - return ( - -

Example: count()

-
- ); - } - - return ( - - {definition.input === 'field' ?

Example: {type}(bytes)

: null} - {definition.input === 'fullReference' && !('operationParams' in definition) ? ( -

Example: {type}(sum(bytes))

- ) : null} - - {'operationParams' in definition && definition.operationParams ? ( -

-

- Example: {type}(sum(bytes),{' '} - {definition.operationParams.map((p) => `${p.name}=5`).join(', ')}) -

-

- ) : null} -
- ); -} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts index 5d9a8647eb7ab..2f68522b1ef51 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts @@ -85,9 +85,20 @@ export const tinymathFunctions: Record< { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, ], help: ` +### add(summand1: number, summand2: number) \`+\` +Adds up two numbers. Also works with + symbol -Example: ${'`count() + sum(bytes)`'} -Example: ${'`add(count(), 5)`'} + +Example: Calculate the sum of two fields +\`\`\` +sum(price) + sum(tax) +\`\`\` + +Example: Offset count by a static value + +\`\`\` +add(count(), 5) +\`\`\` `, }, subtract: { @@ -96,8 +107,14 @@ Example: ${'`add(count(), 5)`'} { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, ], help: ` +### subtract(minuend: number, subtrahend: number) \`-\` +Subtracts the first number from the second number. Also works with ${'`-`'} symbol -Example: ${'`subtract(sum(bytes), avg(bytes))`'} + +Example: Calculate the range of a field +\`\`\` +subtract(max(bytes), min(bytes)) +\`\`\` `, }, multiply: { @@ -106,8 +123,19 @@ Example: ${'`subtract(sum(bytes), avg(bytes))`'} { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, ], help: ` -Also works with ${'`*`'} symbol -Example: ${'`multiply(sum(bytes), 2)`'} +### multiply(factor1: number, factor2: number) \`*\` +Multiplies two numbers. +Also works with ${'`*`'} symbol. + +Example: Calculate price after current tax rate +\`\`\` +sum(bytes) * last_value(tax_rate) +\`\`\` + +Example: Calculate price after constant tax rate +\`\`\` +multiply(sum(price), 1.2) +\`\`\` `, }, divide: { @@ -116,8 +144,14 @@ Example: ${'`multiply(sum(bytes), 2)`'} { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, ], help: ` +### divide(dividend: number, divisor: number) \`/\` +Divides the first number by the second number. Also works with ${'`/`'} symbol -Example: ${'`ceil(sum(bytes))`'} + +Example: Calculate profit margin +\`\`\` +sum(profit) / sum(revenue) +\`\`\` `, }, abs: { @@ -125,8 +159,10 @@ Example: ${'`ceil(sum(bytes))`'} { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, ], help: ` -Absolute value -Example: ${'`abs(sum(bytes))`'} +### abs(value: number) +Calculates absolute value. A negative value is multiplied by -1, a positive value stays the same. + +Example: Calculate average distance to sea level ${'`abs(average(altitude))`'} `, }, cbrt: { @@ -134,8 +170,13 @@ Example: ${'`abs(sum(bytes))`'} { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, ], help: ` -Cube root of value -Example: ${'`cbrt(sum(bytes))`'} +### cbrt(value: number) +Cube root of value. + +Example: Calculate side length from volume +\`\`\` +cbrt(last_value(volume)) +\`\`\` `, }, ceil: { @@ -143,8 +184,13 @@ Example: ${'`cbrt(sum(bytes))`'} { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, ], help: ` -Ceiling of value, rounds up -Example: ${'`ceil(sum(bytes))`'} +### ceil(value: number) +Ceiling of value, rounds up. + +Example: Round up price to the next dollar +\`\`\` +ceil(sum(price)) +\`\`\` `, }, clamp: { @@ -154,17 +200,31 @@ Example: ${'`ceil(sum(bytes))`'} { name: i18n.translate('xpack.lens.formula.max', { defaultMessage: 'max' }) }, ], help: ` -Limits the value from a minimum to maximum -Example: ${'`ceil(sum(bytes))`'} - `, +### clamp(value: number, minimum: number, maximum: number) +Limits the value from a minimum to maximum. + +Example: Make sure to catch outliers +\`\`\` +clamp( + average(bytes), + percentile(bytes, percentile=5), + percentile(bytes, percentile=95) +) +\`\`\` +`, }, cube: { positionalArguments: [ { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, ], help: ` -Limits the value from a minimum to maximum -Example: ${'`ceil(sum(bytes))`'} +### cube(value: number) +Calculates the cube of a number. + +Example: Calculate volume from side length +\`\`\` +cube(last_value(length)) +\`\`\` `, }, exp: { @@ -172,8 +232,13 @@ Example: ${'`ceil(sum(bytes))`'} { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, ], help: ` +### exp(value: number) Raises e to the nth power. -Example: ${'`exp(sum(bytes))`'} + +Example: Calculate the natural expontential function +\`\`\` +exp(last_value(duration)) +\`\`\` `, }, fix: { @@ -181,8 +246,13 @@ Example: ${'`exp(sum(bytes))`'} { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, ], help: ` +### fix(value: number) For positive values, takes the floor. For negative values, takes the ceiling. -Example: ${'`fix(sum(bytes))`'} + +Example: Rounding towards zero +\`\`\` +fix(sum(profit)) +\`\`\` `, }, floor: { @@ -190,8 +260,13 @@ Example: ${'`fix(sum(bytes))`'} { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, ], help: ` +### floor(value: number) Round down to nearest integer value -Example: ${'`floor(sum(bytes))`'} + +Example: Round down a price +\`\`\` +floor(sum(price)) +\`\`\` `, }, log: { @@ -203,9 +278,13 @@ Example: ${'`floor(sum(bytes))`'} }, ], help: ` +### log(value: number, base?: number) Logarithm with optional base. The natural base e is used as default. -Example: ${'`log(sum(bytes))`'} -Example: ${'`log(sum(bytes), 2)`'} + +Example: Calculate number of bits required to store values +\`\`\` +log(max(price), 2) +\`\`\` `, }, // TODO: check if this is valid for Tinymath @@ -227,20 +306,30 @@ Example: ${'`log(sum(bytes), 2)`'} }, ], help: ` +### mod(value: number) Remainder after dividing the function by a number -Example: ${'`mod(sum(bytes), 2)`'} + +Example: Calculate last three digits of a value +\`\`\` +mod(sum(price), 1000) +\`\`\` `, }, pow: { positionalArguments: [ { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, { - name: i18n.translate('xpack.lens.formula.base', { defaultMessage: 'base' }), + name: i18n.translate('xpack.lens.\\formula.base', { defaultMessage: 'base' }), }, ], help: ` +### pow(value: number, power: number) Raises the value to a certain power. The second argument is required -Example: ${'`pow(sum(bytes), 3)`'} + +Example: Calculate volume based on side length +\`\`\` +pow(last_value(length), 3) +\`\`\` `, }, round: { @@ -252,9 +341,13 @@ Example: ${'`pow(sum(bytes), 3)`'} }, ], help: ` +### round(value: number, digits: number = 0) Rounds to a specific number of decimal places, default of 0 -Example: ${'`round(sum(bytes))`'} -Example: ${'`round(sum(bytes), 2)`'} + +Example: Round to the cent +\`\`\` +round(sum(price), 2) +\`\`\` `, }, sqrt: { @@ -262,8 +355,13 @@ Example: ${'`round(sum(bytes), 2)`'} { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, ], help: ` +### sqrt(value: number) Square root of a positive value only -Example: ${'`sqrt(sum(bytes))`'} + +Example: Calculate side length based on area +\`\`\` +sqrt(last_value(area)) +\`\`\` `, }, square: { @@ -271,8 +369,13 @@ Example: ${'`sqrt(sum(bytes))`'} { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, ], help: ` +### square(value: number) Raise the value to the 2nd power -Example: ${'`square(sum(bytes))`'} + +Example: Calculate area based on side length +\`\`\` +square(last_value(length)) +\`\`\` `, }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 27982243f8c2b..510a59b109d10 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -262,6 +262,10 @@ interface BaseOperationDefinitionProps { * Operations can be used as middleware for other operations, hence not shown in the panel UI */ hidden?: boolean; + documentation?: { + description: JSX.Element; + section: 'elasticsearch' | 'calculation'; + }; } interface BaseBuildColumnArgs { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx index 4632d262c441d..5ec17b6ac3e6f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -21,6 +21,7 @@ import { getSafeName, getFilter, } from './helpers'; +import { Markdown } from '../../../../../../../src/plugins/kibana_react/public'; function ofName(name: string) { return i18n.translate('xpack.lens.indexPattern.lastValueOf', { @@ -265,4 +266,25 @@ export const lastValueOperation: OperationDefinition ); }, + documentation: { + section: 'elasticsearch', + description: ( + + ), + }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 725ef93203a43..2c5bda1d2870d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import React from 'react'; import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; import { OperationDefinition } from './index'; import { @@ -23,6 +24,7 @@ import { adjustTimeScaleLabelSuffix, adjustTimeScaleOnOtherColumnChange, } from '../time_scale_utils'; +import { Markdown } from '../../../../../../../src/plugins/kibana_react/public'; type MetricColumn = FormattedIndexPatternColumn & FieldBasedIndexPatternColumn & { @@ -125,6 +127,33 @@ function buildMetricOperation>({ getErrorMessage: (layer, columnId, indexPattern) => getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), filterable: true, + documentation: { + section: 'elasticsearch', + description: ( + + ), + }, } as OperationDefinition; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index 705a1f7172fff..c98832c8516cb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -20,6 +20,7 @@ import { getFilter, } from './helpers'; import { FieldBasedIndexPatternColumn } from './column_types'; +import { Markdown } from '../../../../../../../src/plugins/kibana_react/public'; export interface PercentileIndexPatternColumn extends FieldBasedIndexPatternColumn { operationType: 'percentile'; @@ -193,4 +194,23 @@ export const percentileOperation: OperationDefinition ); }, + documentation: { + section: 'elasticsearch', + description: ( + + ), + }, }; diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index bc9d0ca8d7b94..f00afb7ac810d 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -5,45 +5,6 @@ * 2.0. */ -// TODO: We should remove these and instead directly import them in the security_solution project. This is to get my PR across the line without too many conflicts. -export { - CommentsArray, - Comment, - CreateComment, - CreateCommentsArray, - Entry, - EntryExists, - EntryMatch, - EntryMatchAny, - EntryMatchWildcard, - EntryNested, - EntryList, - EntriesArray, - NamespaceType, - NestedEntriesArray, - ListOperator as Operator, - ListOperatorEnum as OperatorEnum, - ListOperatorTypeEnum as OperatorTypeEnum, - listOperator as operator, - ExceptionListTypeEnum, - ExceptionListType, - comment, - exceptionListType, - entry, - entriesNested, - nestedEntryItem, - entriesMatch, - entriesMatchAny, - entriesMatchWildcard, - entriesExists, - entriesList, - namespaceType, - osType, - osTypeArray, - OsTypeArray, - Type, -} from '@kbn/securitysolution-io-ts-list-types'; - export { ListSchema, ExceptionListSchema, diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx index a0994871808d1..c1776280842c6 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx @@ -14,8 +14,8 @@ import { EuiSuperSelect, } from '@elastic/eui'; import { uniq } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { OperatorTypeEnum } from '../../../../common'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx index 08958f6d99aab..82347f6212442 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx @@ -8,8 +8,8 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { uniq } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { OperatorTypeEnum } from '../../../../common'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts index 4f25bec3b38dc..b982193d1d349 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts @@ -7,8 +7,9 @@ import dateMath from '@elastic/datemath'; import { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { Type } from '@kbn/securitysolution-io-ts-list-types'; -import { ListSchema, Type } from '../../../../common'; +import type { ListSchema } from '../../../../common'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; import { diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts index 4e3fb2179d786..0335ffa55d2a2 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts @@ -6,10 +6,10 @@ */ import { act, renderHook } from '@testing-library/react-hooks'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub'; import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { OperatorTypeEnum } from '../../../../../common'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; import { diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts index 6c6198ac55a0f..674bb5e5537d9 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts @@ -7,10 +7,10 @@ import { useEffect, useRef, useState } from 'react'; import { debounce } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { AutocompleteStart } from '../../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { OperatorTypeEnum } from '../../../../../common'; interface FuncArgs { fieldSelected: IFieldType | undefined; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts index 551dfcb61e3ad..83a424d72ec5f 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts @@ -6,8 +6,10 @@ */ import { i18n } from '@kbn/i18n'; - -import { OperatorEnum, OperatorTypeEnum } from '../../../../common'; +import { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; import { OperatorOption } from './types'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts index 8ea3e8d927d68..76d5b7758007b 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts @@ -6,8 +6,10 @@ */ import { EuiComboBoxOptionOption } from '@elastic/eui'; - -import { OperatorEnum, OperatorTypeEnum } from '../../../../common'; +import type { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; export interface GetGenericComboBoxPropsReturn { comboOptions: EuiComboBoxOptionOption[]; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx index 5b3730a6deb93..dd67381c30934 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx @@ -9,8 +9,11 @@ import { Story } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { HttpStart } from 'kibana/public'; +import { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; -import { OperatorEnum, OperatorTypeEnum } from '../../../../common'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index 0ece28d409bd5..09863660e98af 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -8,7 +8,11 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import styled from 'styled-components'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; +import { + ExceptionListType, + ListOperatorTypeEnum as OperatorTypeEnum, + OsTypeArray, +} from '@kbn/securitysolution-io-ts-list-types'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; @@ -21,7 +25,7 @@ import { AutocompleteFieldExistsComponent } from '../autocomplete/field_value_ex import { AutocompleteFieldMatchComponent } from '../autocomplete/field_value_match'; import { AutocompleteFieldMatchAnyComponent } from '../autocomplete/field_value_match_any'; import { AutocompleteFieldListsComponent } from '../autocomplete/field_value_lists'; -import { ExceptionListType, ListSchema, OperatorTypeEnum } from '../../../../common'; +import { ListSchema } from '../../../../common'; import { getEmptyValue } from '../../../common/empty_value'; import { diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx index 94c3bff8f4cf9..e10cd2934328f 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx @@ -10,9 +10,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { AutocompleteStart } from 'src/plugins/data/public'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; +import { ExceptionListType, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; -import { ExceptionListType } from '../../../../common'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index 4ec152e155e39..f771969a92025 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -10,19 +10,21 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { addIdToItem } from '@kbn/securitysolution-utils'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; - -import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { - CreateExceptionListItemSchema, - ExceptionListItemSchema, ExceptionListType, NamespaceType, - OperatorEnum, - OperatorTypeEnum, + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, + OsTypeArray, entriesNested, +} from '@kbn/securitysolution-io-ts-list-types'; + +import { + CreateExceptionListItemSchema, + ExceptionListItemSchema, exceptionListItemSchema, } from '../../../../common'; +import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { AndOrBadge } from '../and_or_badge'; import { CreateExceptionListItemBuilderSchema, ExceptionsBuilderExceptionItem } from './types'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts index 1e74193299e56..dbfeaa4a258ca 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts @@ -5,6 +5,18 @@ * 2.0. */ +import { + EntryExists, + EntryList, + EntryMatch, + EntryMatchAny, + EntryNested, + ExceptionListType, + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../../../../common'; import { ENTRIES_WITH_IDS } from '../../../../common/constants.mock'; import { getEntryExistsMock } from '../../../../common/schemas/types/entry_exists.mock'; import { getExceptionListItemSchemaMock } from '../../../../common/schemas/response/exception_list_item_schema.mock'; @@ -23,25 +35,23 @@ import { doesNotExistOperator, existsOperator, isInListOperator, + isNotInListOperator, isNotOneOfOperator, isNotOperator, isOneOfOperator, isOperator, } from '../autocomplete/operators'; -import { - EntryExists, - EntryList, - EntryMatch, - EntryMatchAny, - EntryNested, - ExceptionListType, - OperatorEnum, - OperatorTypeEnum, -} from '../../../../common'; import { OperatorOption } from '../autocomplete/types'; +import { getEntryListMock } from '../../../../common/schemas/types/entry_list.mock'; -import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types'; import { + BuilderEntry, + EmptyEntry, + ExceptionsBuilderExceptionItem, + FormattedBuilderEntry, +} from './types'; +import { + filterExceptionItems, getCorrespondingKeywordField, getEntryFromOperator, getEntryOnFieldChange, @@ -49,10 +59,14 @@ import { getEntryOnMatchAnyChange, getEntryOnMatchChange, getEntryOnOperatorChange, + getEntryValue, + getExceptionOperatorSelect, getFilteredIndexPatterns, getFormattedBuilderEntries, getFormattedBuilderEntry, + getNewExceptionItem, getOperatorOptions, + getOperatorType, getUpdatedEntriesOnDelete, isEntryNested, } from './helpers'; @@ -1426,4 +1440,298 @@ describe('Exception builder helpers', () => { expect(output).toEqual(undefined); }); }); + + describe('#getOperatorType', () => { + test('returns operator type "match" if entry.type is "match"', () => { + const payload = getEntryMatchMock(); + const operatorType = getOperatorType(payload); + + expect(operatorType).toEqual(OperatorTypeEnum.MATCH); + }); + + test('returns operator type "match_any" if entry.type is "match_any"', () => { + const payload = getEntryMatchAnyMock(); + const operatorType = getOperatorType(payload); + + expect(operatorType).toEqual(OperatorTypeEnum.MATCH_ANY); + }); + + test('returns operator type "list" if entry.type is "list"', () => { + const payload = getEntryListMock(); + const operatorType = getOperatorType(payload); + + expect(operatorType).toEqual(OperatorTypeEnum.LIST); + }); + + test('returns operator type "exists" if entry.type is "exists"', () => { + const payload = getEntryExistsMock(); + const operatorType = getOperatorType(payload); + + expect(operatorType).toEqual(OperatorTypeEnum.EXISTS); + }); + }); + + describe('#getExceptionOperatorSelect', () => { + test('it returns "isOperator" when "operator" is "included" and operator type is "match"', () => { + const payload = getEntryMatchMock(); + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isOperator); + }); + + test('it returns "isNotOperator" when "operator" is "excluded" and operator type is "match"', () => { + const payload = getEntryMatchMock(); + payload.operator = 'excluded'; + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isNotOperator); + }); + + test('it returns "isOneOfOperator" when "operator" is "included" and operator type is "match_any"', () => { + const payload = getEntryMatchAnyMock(); + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isOneOfOperator); + }); + + test('it returns "isNotOneOfOperator" when "operator" is "excluded" and operator type is "match_any"', () => { + const payload = getEntryMatchAnyMock(); + payload.operator = 'excluded'; + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isNotOneOfOperator); + }); + + test('it returns "existsOperator" when "operator" is "included" and no operator type is provided', () => { + const payload = getEntryExistsMock(); + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(existsOperator); + }); + + test('it returns "doesNotExistsOperator" when "operator" is "excluded" and no operator type is provided', () => { + const payload = getEntryExistsMock(); + payload.operator = 'excluded'; + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(doesNotExistOperator); + }); + + test('it returns "isInList" when "operator" is "included" and operator type is "list"', () => { + const payload = getEntryListMock(); + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isInListOperator); + }); + + test('it returns "isNotInList" when "operator" is "excluded" and operator type is "list"', () => { + const payload = getEntryListMock(); + payload.operator = 'excluded'; + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isNotInListOperator); + }); + }); + + describe('#filterExceptionItems', () => { + // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes + // for context around the temporary `id` + test('it correctly validates entries that include a temporary `id`', () => { + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }]); + }); + + test('it removes entry items with "value" of "undefined"', () => { + const { entries, ...rest } = getExceptionListItemSchemaMock(); + const mockEmptyException: EmptyEntry = { + field: 'host.name', + id: '123', + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH, + value: undefined, + }; + const exceptions = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(exceptions).toEqual([getExceptionListItemSchemaMock()]); + }); + + test('it removes "match" entry items with "value" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EmptyEntry = { + field: 'host.name', + id: '123', + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH, + value: '', + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes "match" entry items with "field" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EmptyEntry = { + field: '', + id: '123', + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH, + value: 'some value', + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes "match_any" entry items with "field" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EmptyEntry = { + field: '', + id: '123', + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH_ANY, + value: ['some value'], + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes "nested" entry items with "field" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + entries: [getEntryMatchMock()], + field: '', + type: OperatorTypeEnum.NESTED, + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes the "nested" entry entries with "value" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + entries: [getEntryMatchMock(), { ...getEntryMatchMock(), value: '' }], + field: 'host.name', + type: OperatorTypeEnum.NESTED, + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([ + { + ...getExceptionListItemSchemaMock(), + entries: [ + ...getExceptionListItemSchemaMock().entries, + { ...mockEmptyException, entries: [getEntryMatchMock()] }, + ], + }, + ]); + }); + + test('it removes the "nested" entry item if all its entries are invalid', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + entries: [{ ...getEntryMatchMock(), value: '' }], + field: 'host.name', + type: OperatorTypeEnum.NESTED, + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes `temporaryId` from items', () => { + const { meta, ...rest } = getNewExceptionItem({ + listId: '123', + namespaceType: 'single', + ruleName: 'rule name', + }); + const exceptions = filterExceptionItems([{ ...rest, meta }]); + + expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]); + }); + }); + + describe('#getEntryValue', () => { + it('returns "match" entry value', () => { + const payload = getEntryMatchMock(); + const result = getEntryValue(payload); + const expected = 'some host name'; + expect(result).toEqual(expected); + }); + + it('returns "match any" entry values', () => { + const payload = getEntryMatchAnyMock(); + const result = getEntryValue(payload); + const expected = ['some host name']; + expect(result).toEqual(expected); + }); + + it('returns "exists" entry value', () => { + const payload = getEntryExistsMock(); + const result = getEntryValue(payload); + const expected = undefined; + expect(result).toEqual(expected); + }); + + it('returns "list" entry value', () => { + const payload = getEntryListMock(); + const result = getEntryValue(payload); + const expected = 'some-list-id'; + expect(result).toEqual(expected); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts index 18d607d6807fc..6cd9dec0dc7a1 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts @@ -8,27 +8,29 @@ import uuid from 'uuid'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; import { validate } from '@kbn/securitysolution-io-ts-utils'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; - -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { - CreateExceptionListItemSchema, EntriesArray, Entry, EntryNested, - ExceptionListItemSchema, ExceptionListType, - ListSchema, NamespaceType, - OperatorEnum, - OperatorTypeEnum, - createExceptionListItemSchema, + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, + OsTypeArray, entriesList, entriesNested, entry, - exceptionListItemSchema, nestedEntryItem, +} from '@kbn/securitysolution-io-ts-list-types'; + +import { + CreateExceptionListItemSchema, + ExceptionListItemSchema, + ListSchema, + createExceptionListItemSchema, + exceptionListItemSchema, } from '../../../../common'; +import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { EXCEPTION_OPERATORS, EXCEPTION_OPERATORS_SANS_LISTS, @@ -96,7 +98,7 @@ export const filterExceptionItems = ( return [...acc, item]; } else if (createExceptionListItemSchema.is(item)) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { meta: _, ...rest } = item; + const { meta, ...rest } = item; const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined }; return [...acc, itemSansMetaId]; } else { diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts b/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts index 92df2fd3793de..0e8a5fadd3b1a 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { ExceptionListItemSchema, OperatorTypeEnum } from '../../../../common'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +import { ExceptionListItemSchema } from '../../../../common'; import { ExceptionsBuilderExceptionItem } from './types'; import { getDefaultEmptyEntry } from './helpers'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/types.ts b/x-pack/plugins/lists/public/exceptions/components/builder/types.ts index 800f1445217b9..5cf4238ab5e0c 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/types.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/types.ts @@ -5,20 +5,20 @@ * 2.0. */ -import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { OperatorOption } from '../autocomplete/types'; -import { - CreateExceptionListItemSchema, +import type { Entry, EntryExists, EntryMatch, EntryMatchAny, EntryMatchWildcard, EntryNested, - ExceptionListItemSchema, - OperatorEnum, - OperatorTypeEnum, -} from '../../../../common'; + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import type { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../../../../common'; +import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { OperatorOption } from '../autocomplete/types'; export interface FormattedBuilderEntry { id: string; diff --git a/x-pack/plugins/lists/public/exceptions/transforms.ts b/x-pack/plugins/lists/public/exceptions/transforms.ts index 50ce1b6e33a4b..564ba1a699f98 100644 --- a/x-pack/plugins/lists/public/exceptions/transforms.ts +++ b/x-pack/plugins/lists/public/exceptions/transforms.ts @@ -7,11 +7,10 @@ import { flow } from 'fp-ts/lib/function'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; +import type { EntriesArray, Entry } from '@kbn/securitysolution-io-ts-list-types'; import type { CreateExceptionListItemSchema, - EntriesArray, - Entry, ExceptionListItemSchema, UpdateExceptionListItemSchema, } from '../../common'; diff --git a/x-pack/plugins/lists/public/shared_exports.ts b/x-pack/plugins/lists/public/shared_exports.ts index 2032a44a8fd33..6d14c6b541904 100644 --- a/x-pack/plugins/lists/public/shared_exports.ts +++ b/x-pack/plugins/lists/public/shared_exports.ts @@ -7,11 +7,8 @@ // Exports to be shared with plugins export { withOptionalSignal } from './common/with_optional_signal'; -export { useIsMounted } from './common/hooks/use_is_mounted'; export { useAsync } from './common/hooks/use_async'; export { useApi } from './exceptions/hooks/use_api'; -export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item'; -export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list'; export { useExceptionListItems } from './exceptions/hooks/use_exception_list_items'; export { useExceptionLists } from './exceptions/hooks/use_exception_lists'; export { useFindLists } from './lists/hooks/use_find_lists'; @@ -24,13 +21,18 @@ export { useReadListIndex } from './lists/hooks/use_read_list_index'; export { useCreateListIndex } from './lists/hooks/use_create_list_index'; export { useReadListPrivileges } from './lists/hooks/use_read_list_privileges'; export { - addExceptionListItem, - updateExceptionListItem, + getEntryValue, + getExceptionOperatorSelect, + getOperatorType, + getNewExceptionItem, + addIdToEntries, +} from './exceptions/components/builder/helpers'; +export { fetchExceptionListById, addExceptionList, addEndpointExceptionList, } from './exceptions/api'; -export { +export type { ExceptionList, ExceptionListFilter, ExceptionListIdentifiers, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts index 79fd264808138..e2c3ee88f6a65 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; -import { exceptionListType, namespaceType } from '../../../shared_imports'; +import { exceptionListType, namespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { NonEmptyString } from './non_empty_string'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts index c477036a07d85..1e0f7e087a5b3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts @@ -13,7 +13,8 @@ import { normalizeMachineLearningJobIds, normalizeThresholdField, } from './utils'; -import { EntriesArray } from '../shared_imports'; + +import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; describe('#hasLargeValueList', () => { test('it returns false if empty array', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index a8e0ffcccef82..611d23fd1ce22 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -7,11 +7,10 @@ import { isEmpty } from 'lodash'; -import { - CreateExceptionListItemSchema, - EntriesArray, - ExceptionListItemSchema, -} from '../shared_imports'; +import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; + +import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../shared_imports'; + import { Type, JobStatus, Threshold, ThresholdNormalized } from './schemas/common/schemas'; export const hasLargeValueItem = ( diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index e987775a8e768..a6bad0347e641 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -7,44 +7,14 @@ export { ListSchema, - CommentsArray, - CreateCommentsArray, - Comment, - CreateComment, ExceptionListSchema, ExceptionListItemSchema, CreateExceptionListSchema, CreateExceptionListItemSchema, UpdateExceptionListItemSchema, - Entry, - EntryExists, - EntryMatch, - EntryMatchAny, - EntryMatchWildcard, - EntryNested, - EntryList, - EntriesArray, - NamespaceType, - Operator, - OperatorEnum, - OperatorTypeEnum, - ExceptionListTypeEnum, exceptionListItemSchema, - exceptionListType, - comment, createExceptionListItemSchema, listSchema, - entry, - entriesNested, - nestedEntryItem, - entriesMatch, - entriesMatchAny, - entriesMatchWildcard, - entriesExists, - entriesList, - namespaceType, - ExceptionListType, - Type, ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, EXCEPTION_LIST_URL, @@ -52,8 +22,5 @@ export { ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_EVENT_FILTERS_LIST_NAME, ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, - osType, - osTypeArray, - OsTypeArray, buildExceptionFilter, } from '../../lists/common'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index c1efb4d7c4565..9cb219e7a8d45 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -15,10 +15,11 @@ import { } from '@elastic/eui'; import { uniq } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; import { paramIsValid, getGenericComboBoxProps } from './helpers'; -import { OperatorTypeEnum } from '../../../lists_plugin_deps'; + import { GetGenericComboBoxPropsReturn } from './types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx index e77bf570adc63..dbfdaf9749b6d 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx @@ -9,11 +9,12 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiFormRow, EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; import { uniq } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; import { getGenericComboBoxProps, paramIsValid } from './helpers'; -import { OperatorTypeEnum } from '../../../lists_plugin_deps'; import { GetGenericComboBoxPropsReturn } from './types'; + import * as i18n from './translations'; interface AutocompleteFieldMatchAnyProps { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts index b89f9525024c7..bd79bb0fcc8e8 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts @@ -8,6 +8,8 @@ import dateMath from '@elastic/datemath'; import { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { Type } from '@kbn/securitysolution-io-ts-list-types'; +import type { ListSchema } from '../../../lists_plugin_deps'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; import { @@ -19,7 +21,6 @@ import { } from './operators'; import { GetGenericComboBoxPropsReturn, OperatorOption } from './types'; import * as i18n from './translations'; -import { ListSchema, Type } from '../../../lists_plugin_deps'; /** * Returns the appropriate operators given a field type diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts index 36e050c84f0b3..e0bdbf2603dc3 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts @@ -15,7 +15,7 @@ import { import { useKibana } from '../../../../common/lib/kibana'; import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub'; import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { OperatorTypeEnum } from '../../../../lists_plugin_deps'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; jest.mock('../../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts index b8440205e7d32..0f369fa01d01e 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts @@ -8,9 +8,9 @@ import { useEffect, useState, useRef } from 'react'; import { debounce } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; import { useKibana } from '../../../../common/lib/kibana'; -import { OperatorTypeEnum } from '../../../../lists_plugin_deps'; interface FuncArgs { fieldSelected: IFieldType | undefined; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts index 93eab41264bf7..53e2ddf84b3d3 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts @@ -6,8 +6,11 @@ */ import { i18n } from '@kbn/i18n'; +import { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; import { OperatorOption } from './types'; -import { OperatorEnum, OperatorTypeEnum } from '../../../lists_plugin_deps'; export const isOperator: OperatorOption = { message: i18n.translate('xpack.securitySolution.exceptions.isOperatorLabel', { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts index 903edc403ea25..1d8e3e9aee28e 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts @@ -7,7 +7,10 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { OperatorEnum, OperatorTypeEnum } from '../../../lists_plugin_deps'; +import type { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; export interface GetGenericComboBoxPropsReturn { comboOptions: EuiComboBoxOptionOption[]; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx index c627363fc29ef..c13a1b011ccbd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx @@ -17,7 +17,7 @@ import { EuiCommentProps, EuiText, } from '@elastic/eui'; -import { Comment } from '../../../shared_imports'; +import type { Comment } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from './translations'; import { useCurrentUser } from '../../lib/kibana'; import { getFormattedComments } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index 5ec8999d20518..5fb527a821bac 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -49,7 +49,10 @@ jest.mock('../../../containers/source'); jest.mock('../../../../detections/containers/detection_engine/rules'); jest.mock('../use_add_exception'); jest.mock('../use_fetch_or_create_rule_exception_list'); -jest.mock('../../../../shared_imports'); +jest.mock('../../../../shared_imports', () => ({ + ...jest.requireActual('../../../../shared_imports'), + useAsync: jest.fn(), +})); jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); describe('When the add exception modal is opened', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 120c4ad8efc1b..6efbbcf64406b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -25,6 +25,7 @@ import { EuiComboBox, EuiComboBoxOptionOption, } from '@elastic/eui'; +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { hasEqlSequenceQuery, isEqlRule, @@ -34,9 +35,9 @@ import { Status } from '../../../../../common/detection_engine/schemas/common/sc import { ExceptionListItemSchema, CreateExceptionListItemSchema, - ExceptionListType, ExceptionBuilder, } from '../../../../../public/shared_imports'; + import * as i18nCommon from '../../../translations'; import * as i18n from './translations'; import * as sharedI18n from '../translations'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 5fb52994fb0f5..6c68dcf934b71 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -22,6 +22,7 @@ import { EuiCallOut, } from '@elastic/eui'; +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { hasEqlSequenceQuery, isEqlRule, @@ -33,9 +34,9 @@ import { useRuleAsync } from '../../../../detections/containers/detection_engine import { ExceptionListItemSchema, CreateExceptionListItemSchema, - ExceptionListType, ExceptionBuilder, } from '../../../../../public/shared_imports'; + import * as i18n from './translations'; import * as sharedI18n from '../translations'; import { useKibana } from '../../../lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 907b30fcaa879..98c2b4db5676e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -10,13 +10,8 @@ import { mount } from 'enzyme'; import moment from 'moment-timezone'; import { - getOperatorType, - getExceptionOperatorSelect, getFormattedComments, - filterExceptionItems, - getNewExceptionItem, formatOperatingSystems, - getEntryValue, formatExceptionItemForUpdate, enrichNewExceptionItemsWithComments, enrichExistingExceptionItemWithComments, @@ -32,35 +27,19 @@ import { retrieveAlertOsTypes, filterIndexPatterns, } from './helpers'; -import { AlertData, EmptyEntry } from './types'; +import { AlertData } from './types'; import { - isOperator, - isNotOperator, - isOneOfOperator, - isNotOneOfOperator, - isInListOperator, - isNotInListOperator, - existsOperator, - doesNotExistOperator, -} from '../autocomplete/operators'; -import { OperatorTypeEnum, OperatorEnum, EntryNested } from '../../../shared_imports'; + ListOperatorTypeEnum as OperatorTypeEnum, + EntriesArray, + OsTypeArray, +} from '@kbn/securitysolution-io-ts-list-types'; + import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getEntryMatchMock } from '../../../../../lists/common/schemas/types/entry_match.mock'; -import { getEntryMatchAnyMock } from '../../../../../lists/common/schemas/types/entry_match_any.mock'; -import { getEntryExistsMock } from '../../../../../lists/common/schemas/types/entry_exists.mock'; -import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock'; import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comment.mock'; import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { - ENTRIES, - ENTRIES_WITH_IDS, - OLD_DATE_RELATIVE_TO_DATE_NOW, -} from '../../../../../lists/common/constants.mock'; -import { EntriesArray, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; -import { - CreateExceptionListItemSchema, - ExceptionListItemSchema, -} from '../../../../../lists/common/schemas'; +import { ENTRIES, OLD_DATE_RELATIVE_TO_DATE_NOW } from '../../../../../lists/common/constants.mock'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { IFieldType, IIndexPattern } from 'src/plugins/data/common'; jest.mock('uuid', () => ({ @@ -162,128 +141,6 @@ describe('Exception helpers', () => { }); }); - describe('#getOperatorType', () => { - test('returns operator type "match" if entry.type is "match"', () => { - const payload = getEntryMatchMock(); - const operatorType = getOperatorType(payload); - - expect(operatorType).toEqual(OperatorTypeEnum.MATCH); - }); - - test('returns operator type "match_any" if entry.type is "match_any"', () => { - const payload = getEntryMatchAnyMock(); - const operatorType = getOperatorType(payload); - - expect(operatorType).toEqual(OperatorTypeEnum.MATCH_ANY); - }); - - test('returns operator type "list" if entry.type is "list"', () => { - const payload = getEntryListMock(); - const operatorType = getOperatorType(payload); - - expect(operatorType).toEqual(OperatorTypeEnum.LIST); - }); - - test('returns operator type "exists" if entry.type is "exists"', () => { - const payload = getEntryExistsMock(); - const operatorType = getOperatorType(payload); - - expect(operatorType).toEqual(OperatorTypeEnum.EXISTS); - }); - }); - - describe('#getExceptionOperatorSelect', () => { - test('it returns "isOperator" when "operator" is "included" and operator type is "match"', () => { - const payload = getEntryMatchMock(); - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isOperator); - }); - - test('it returns "isNotOperator" when "operator" is "excluded" and operator type is "match"', () => { - const payload = getEntryMatchMock(); - payload.operator = 'excluded'; - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isNotOperator); - }); - - test('it returns "isOneOfOperator" when "operator" is "included" and operator type is "match_any"', () => { - const payload = getEntryMatchAnyMock(); - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isOneOfOperator); - }); - - test('it returns "isNotOneOfOperator" when "operator" is "excluded" and operator type is "match_any"', () => { - const payload = getEntryMatchAnyMock(); - payload.operator = 'excluded'; - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isNotOneOfOperator); - }); - - test('it returns "existsOperator" when "operator" is "included" and no operator type is provided', () => { - const payload = getEntryExistsMock(); - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(existsOperator); - }); - - test('it returns "doesNotExistsOperator" when "operator" is "excluded" and no operator type is provided', () => { - const payload = getEntryExistsMock(); - payload.operator = 'excluded'; - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(doesNotExistOperator); - }); - - test('it returns "isInList" when "operator" is "included" and operator type is "list"', () => { - const payload = getEntryListMock(); - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isInListOperator); - }); - - test('it returns "isNotInList" when "operator" is "excluded" and operator type is "list"', () => { - const payload = getEntryListMock(); - payload.operator = 'excluded'; - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isNotInListOperator); - }); - }); - - describe('#getEntryValue', () => { - it('returns "match" entry value', () => { - const payload = getEntryMatchMock(); - const result = getEntryValue(payload); - const expected = 'some host name'; - expect(result).toEqual(expected); - }); - - it('returns "match any" entry values', () => { - const payload = getEntryMatchAnyMock(); - const result = getEntryValue(payload); - const expected = ['some host name']; - expect(result).toEqual(expected); - }); - - it('returns "exists" entry value', () => { - const payload = getEntryExistsMock(); - const result = getEntryValue(payload); - const expected = undefined; - expect(result).toEqual(expected); - }); - - it('returns "list" entry value', () => { - const payload = getEntryListMock(); - const result = getEntryValue(payload); - const expected = 'some-list-id'; - expect(result).toEqual(expected); - }); - }); - describe('#formatOperatingSystems', () => { test('it returns null if no operating system tag specified', () => { const result = formatOperatingSystems(['some os', 'some other os']); @@ -324,178 +181,6 @@ describe('Exception helpers', () => { }); }); - describe('#filterExceptionItems', () => { - // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes - // for context around the temporary `id` - test('it correctly validates entries that include a temporary `id`', () => { - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }]); - }); - - test('it removes entry items with "value" of "undefined"', () => { - const { entries, ...rest } = getExceptionListItemSchemaMock(); - const mockEmptyException: EmptyEntry = { - id: '123', - field: 'host.name', - type: OperatorTypeEnum.MATCH, - operator: OperatorEnum.INCLUDED, - value: undefined, - }; - const exceptions = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(exceptions).toEqual([getExceptionListItemSchemaMock()]); - }); - - test('it removes "match" entry items with "value" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EmptyEntry = { - id: '123', - field: 'host.name', - type: OperatorTypeEnum.MATCH, - operator: OperatorEnum.INCLUDED, - value: '', - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes "match" entry items with "field" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EmptyEntry = { - id: '123', - field: '', - type: OperatorTypeEnum.MATCH, - operator: OperatorEnum.INCLUDED, - value: 'some value', - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes "match_any" entry items with "field" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EmptyEntry = { - id: '123', - field: '', - type: OperatorTypeEnum.MATCH_ANY, - operator: OperatorEnum.INCLUDED, - value: ['some value'], - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes "nested" entry items with "field" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EntryNested = { - field: '', - type: OperatorTypeEnum.NESTED, - entries: [getEntryMatchMock()], - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes the "nested" entry entries with "value" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EntryNested = { - field: 'host.name', - type: OperatorTypeEnum.NESTED, - entries: [getEntryMatchMock(), { ...getEntryMatchMock(), value: '' }], - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([ - { - ...getExceptionListItemSchemaMock(), - entries: [ - ...getExceptionListItemSchemaMock().entries, - { ...mockEmptyException, entries: [getEntryMatchMock()] }, - ], - }, - ]); - }); - - test('it removes the "nested" entry item if all its entries are invalid', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EntryNested = { - field: 'host.name', - type: OperatorTypeEnum.NESTED, - entries: [{ ...getEntryMatchMock(), value: '' }], - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes `temporaryId` from items', () => { - const { meta, ...rest } = getNewExceptionItem({ - listId: '123', - namespaceType: 'single', - ruleName: 'rule name', - }); - const exceptions = filterExceptionItems([{ ...rest, meta }]); - - expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]); - }); - }); - describe('#formatExceptionItemForUpdate', () => { test('it should return correct update fields', () => { const payload = getExceptionListItemSchemaMock(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index ce76114309e2e..437e93bb26fef 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -9,46 +9,36 @@ import React from 'react'; import { EuiText, EuiCommentProps, EuiAvatar } from '@elastic/eui'; import { capitalize } from 'lodash'; import moment from 'moment'; -import uuid from 'uuid'; -import * as i18n from './translations'; -import { - AlertData, - BuilderEntry, - CreateExceptionListItemBuilderSchema, - ExceptionsBuilderExceptionItem, - Flattened, -} from './types'; -import { EXCEPTION_OPERATORS, isOperator } from '../autocomplete/operators'; -import { OperatorOption } from '../autocomplete/types'; import { + comment, + osType, CommentsArray, Comment, CreateComment, Entry, - ExceptionListItemSchema, NamespaceType, - OperatorTypeEnum, - CreateExceptionListItemSchema, - comment, - entry, - entriesNested, - nestedEntryItem, - createExceptionListItemSchema, - exceptionListItemSchema, - UpdateExceptionListItemSchema, EntryNested, OsTypeArray, - EntriesArray, - osType, ExceptionListType, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import * as i18n from './translations'; +import { AlertData, ExceptionsBuilderExceptionItem, Flattened } from './types'; +import { + ExceptionListItemSchema, + CreateExceptionListItemSchema, + UpdateExceptionListItemSchema, + getOperatorType, + getNewExceptionItem, + addIdToEntries, } from '../../../shared_imports'; + import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { validate } from '../../../../common/validate'; import { Ecs } from '../../../../common/ecs'; import { CodeSignature } from '../../../../common/ecs/file'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; -import { addIdToItem, removeIdFromItem } from '../../../../common'; import exceptionableLinuxFields from './exceptionable_linux_fields.json'; import exceptionableWindowsMacFields from './exceptionable_windows_mac_fields.json'; import exceptionableEndpointFields from './exceptionable_endpoint_fields.json'; @@ -84,75 +74,6 @@ export const filterIndexPatterns = ( } }; -export const addIdToEntries = (entries: EntriesArray): EntriesArray => { - return entries.map((singleEntry) => { - if (singleEntry.type === 'nested') { - return addIdToItem({ - ...singleEntry, - entries: singleEntry.entries.map((nestedEntry) => addIdToItem(nestedEntry)), - }); - } else { - return addIdToItem(singleEntry); - } - }); -}; - -/** - * Returns the operator type, may not need this if using io-ts types - * - * @param item a single ExceptionItem entry - */ -export const getOperatorType = (item: BuilderEntry): OperatorTypeEnum => { - switch (item.type) { - case 'match': - return OperatorTypeEnum.MATCH; - case 'match_any': - return OperatorTypeEnum.MATCH_ANY; - case 'list': - return OperatorTypeEnum.LIST; - default: - return OperatorTypeEnum.EXISTS; - } -}; - -/** - * Determines operator selection (is/is not/is one of, etc.) - * Default operator is "is" - * - * @param item a single ExceptionItem entry - */ -export const getExceptionOperatorSelect = (item: BuilderEntry): OperatorOption => { - if (item.type === 'nested') { - return isOperator; - } else { - const operatorType = getOperatorType(item); - const foundOperator = EXCEPTION_OPERATORS.find((operatorOption) => { - return item.operator === operatorOption.operator && operatorType === operatorOption.type; - }); - - return foundOperator ?? isOperator; - } -}; - -/** - * Returns the fields corresponding value for an entry - * - * @param item a single ExceptionItem entry - */ -export const getEntryValue = (item: BuilderEntry): string | string[] | undefined => { - switch (item.type) { - case OperatorTypeEnum.MATCH: - case OperatorTypeEnum.MATCH_ANY: - return item.value; - case OperatorTypeEnum.EXISTS: - return undefined; - case OperatorTypeEnum.LIST: - return item.list.id; - default: - return undefined; - } -}; - /** * Formats os value array to a displayable string */ @@ -189,91 +110,6 @@ export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[] ), })); -export const getNewExceptionItem = ({ - listId, - namespaceType, - ruleName, -}: { - listId: string; - namespaceType: NamespaceType; - ruleName: string; -}): CreateExceptionListItemBuilderSchema => { - return { - comments: [], - description: `${ruleName} - exception list item`, - entries: addIdToEntries([ - { - field: '', - operator: 'included', - type: 'match', - value: '', - }, - ]), - item_id: undefined, - list_id: listId, - meta: { - temporaryUuid: uuid.v4(), - }, - name: `${ruleName} - exception list item`, - namespace_type: namespaceType, - tags: [], - type: 'simple', - }; -}; - -export const filterExceptionItems = ( - exceptions: ExceptionsBuilderExceptionItem[] -): Array => { - return exceptions.reduce>( - (acc, exception) => { - const entries = exception.entries.reduce((nestedAcc, singleEntry) => { - const strippedSingleEntry = removeIdFromItem(singleEntry); - - if (entriesNested.is(strippedSingleEntry)) { - const nestedEntriesArray = strippedSingleEntry.entries.filter((singleNestedEntry) => { - const noIdSingleNestedEntry = removeIdFromItem(singleNestedEntry); - const [validatedNestedEntry] = validate(noIdSingleNestedEntry, nestedEntryItem); - return validatedNestedEntry != null; - }); - const noIdNestedEntries = nestedEntriesArray.map((singleNestedEntry) => - removeIdFromItem(singleNestedEntry) - ); - - const [validatedNestedEntry] = validate( - { ...strippedSingleEntry, entries: noIdNestedEntries }, - entriesNested - ); - - if (validatedNestedEntry != null) { - return [...nestedAcc, { ...singleEntry, entries: nestedEntriesArray }]; - } - return nestedAcc; - } else { - const [validatedEntry] = validate(strippedSingleEntry, entry); - - if (validatedEntry != null) { - return [...nestedAcc, singleEntry]; - } - return nestedAcc; - } - }, []); - - const item = { ...exception, entries }; - - if (exceptionListItemSchema.is(item)) { - return [...acc, item]; - } else if (createExceptionListItemSchema.is(item)) { - const { meta, ...rest } = item; - const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined }; - return [...acc, itemSansMetaId]; - } else { - return acc; - } - }, - [] - ); -}; - export const formatExceptionItemForUpdate = ( exceptionItem: ExceptionListItemSchema ): UpdateExceptionListItemSchema => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index 92a3cb2cfac93..49cdd7103c48b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -6,23 +6,22 @@ */ import { ReactNode } from 'react'; -import { Ecs } from '../../../../common/ecs'; -import { CodeSignature } from '../../../../common/ecs/file'; -import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { OperatorOption } from '../autocomplete/types'; -import { +import type { EntryNested, Entry, EntryMatch, EntryMatchAny, EntryMatchWildcard, EntryExists, - ExceptionListItemSchema, - CreateExceptionListItemSchema, NamespaceType, - OperatorTypeEnum, - OperatorEnum, -} from '../../../lists_plugin_deps'; + ListOperatorTypeEnum as OperatorTypeEnum, + ListOperatorEnum as OperatorEnum, +} from '@kbn/securitysolution-io-ts-list-types'; +import { Ecs } from '../../../../common/ecs'; +import { CodeSignature } from '../../../../common/ecs/file'; +import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { OperatorOption } from '../autocomplete/types'; +import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../lists_plugin_deps'; export interface FormattedEntry { fieldName: string; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 0f6dd19ea9b66..f609acf9c6c63 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -27,6 +27,7 @@ import { ReturnUseAddOrUpdateException, AddOrUpdateExceptionItemsFunc, } from './use_add_exception'; +import { UpdateDocumentByQueryResponse } from 'elasticsearch'; const mockKibanaHttpService = coreMock.createStart().http; const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -36,11 +37,9 @@ const fetchMock = jest.fn(); mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); describe('useAddOrUpdateException', () => { - let updateAlertStatus: jest.SpyInstance>; - let addExceptionListItem: jest.SpyInstance>; - let updateExceptionListItem: jest.SpyInstance< - ReturnType - >; + let updateAlertStatus: jest.SpyInstance>; + let addExceptionListItem: jest.SpyInstance>; + let updateExceptionListItem: jest.SpyInstance>; let getQueryFilter: jest.SpyInstance>; let buildAlertStatusFilter: jest.SpyInstance< ReturnType diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index 877f545b69d65..17237f4f94c61 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -12,7 +12,7 @@ import * as rulesApi from '../../../detections/containers/detection_engine/rules import * as listsApi from '../../../../../lists/public/exceptions/api'; import { getExceptionListSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock'; -import { ExceptionListType } from '../../../lists_plugin_deps'; +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { ListArray } from '../../../../common/detection_engine/schemas/types'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { @@ -20,6 +20,7 @@ import { UseFetchOrCreateRuleExceptionListProps, ReturnUseFetchOrCreateRuleExceptionList, } from './use_fetch_or_create_rule_exception_list'; +import { ExceptionListSchema } from '../../../shared_imports'; const mockKibanaHttpService = coreMock.createStart().http; jest.mock('../../../detections/containers/detection_engine/rules/api'); @@ -31,7 +32,7 @@ describe('useFetchOrCreateRuleExceptionList', () => { let addEndpointExceptionList: jest.SpyInstance< ReturnType >; - let fetchExceptionListById: jest.SpyInstance>; + let fetchExceptionListById: jest.SpyInstance>; let render: ( listType?: UseFetchOrCreateRuleExceptionListProps['exceptionListType'] ) => RenderHookResult< diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx index 8ded1b902f302..4f78b49ea266c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx @@ -11,8 +11,8 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionsViewerHeader } from './exceptions_viewer_header'; -import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps'; addDecorator((storyFn) => ( ({ eui: euiLightVars, darkMode: false })}>{storyFn()} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx index b82a472befdcf..7dcd59069b53c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx @@ -9,7 +9,8 @@ import React from 'react'; import { mount } from 'enzyme'; import { ExceptionsViewerHeader } from './exceptions_viewer_header'; -import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; describe('ExceptionsViewerHeader', () => { it('it renders all disabled if "isInitLoading" is true', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx index eff4368ef6809..8fc28ad89156d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx @@ -18,9 +18,9 @@ import { } from '@elastic/eui'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from '../translations'; import { Filter } from '../types'; -import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps'; interface ExceptionsViewerHeaderProps { isInitLoading: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx index 29764625075d6..abd45cf2945cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx @@ -7,8 +7,14 @@ import moment from 'moment'; -import { entriesNested, ExceptionListItemSchema } from '../../../../lists_plugin_deps'; -import { getEntryValue, getExceptionOperatorSelect, formatOperatingSystems } from '../helpers'; +import { entriesNested } from '@kbn/securitysolution-io-ts-list-types'; +import { + ExceptionListItemSchema, + getEntryValue, + getExceptionOperatorSelect, +} from '../../../../lists_plugin_deps'; + +import { formatOperatingSystems } from '../helpers'; import { FormattedEntry, BuilderEntry, DescriptionListItem } from '../types'; import * as i18n from '../translations'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx index 3fe6497105af1..971b3fda47191 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx @@ -11,11 +11,9 @@ import { ThemeProvider } from 'styled-components'; import { ExceptionsViewer } from './'; import { useKibana } from '../../../../common/lib/kibana'; -import { - ExceptionListTypeEnum, - useExceptionListItems, - useApi, -} from '../../../../../public/lists_plugin_deps'; +import { useExceptionListItems, useApi } from '../../../../../public/lists_plugin_deps'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 8c4569ed29b33..da7607f40ab72 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiSpacer } from '@elastic/eui'; import uuid from 'uuid'; +import type { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from '../translations'; import { useStateToaster } from '../../toasters'; import { useKibana } from '../../../../common/lib/kibana'; @@ -20,11 +21,11 @@ import { allExceptionItemsReducer, State, ViewerModalName } from './reducer'; import { useExceptionListItems, ExceptionListIdentifiers, - ExceptionListTypeEnum, ExceptionListItemSchema, UseExceptionListItemsSuccess, useApi, } from '../../../../../public/lists_plugin_deps'; + import { ExceptionsViewerPagination } from './exceptions_pagination'; import { ExceptionsViewerUtility } from './exceptions_utility'; import { ExceptionsViewerItems } from './exceptions_viewer_items'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts index 46ac19f47503d..bf8e454e9971f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { FilterOptions, ExceptionsPagination, @@ -12,7 +13,6 @@ import { Filter, } from '../types'; import { - ExceptionListType, ExceptionListItemSchema, ExceptionListIdentifiers, Pagination, 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 69e41a2c3d0a2..3152c08fab323 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 @@ -19,6 +19,7 @@ import styled from 'styled-components'; import { getOr } from 'lodash/fp'; import { indexOf } from 'lodash'; +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { TimelineId } from '../../../../../common/types/timeline'; @@ -44,7 +45,6 @@ import { } from '../../../../common/components/toasters'; import { inputsModel } from '../../../../common/store'; import { useUserData } from '../../user_info'; -import { ExceptionListType } from '../../../../../common/shared_imports'; import { AlertData, EcsHit } from '../../../../common/components/exceptions/types'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx index 94cb22592f4ed..ea903882c326d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx @@ -18,7 +18,9 @@ import { EuiSelectOption, } from '@elastic/eui'; -import { useImportList, ListSchema, Type } from '../../../shared_imports'; +import type { Type } from '@kbn/securitysolution-io-ts-list-types'; +import { useImportList, ListSchema } from '../../../shared_imports'; + import * as i18n from './translations'; import { useKibana } from '../../../common/lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index d11ceee7f5978..64cb936f160f1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -11,8 +11,8 @@ import React from 'react'; import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui'; import { History } from 'history'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { Spacer } from '../../../../../../common/components/page'; -import { NamespaceType } from '../../../../../../../../lists/common'; import { FormatUrl } from '../../../../../../common/components/link_to'; import { LinkAnchor } from '../../../../../../common/components/links'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 146b7e8470718..50cf1b1830fec 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -15,9 +15,9 @@ import { } from '@elastic/eui'; import { History } from 'history'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts'; import { AutoDownload } from '../../../../../../common/components/auto_download/auto_download'; -import { NamespaceType } from '../../../../../../../../lists/common'; import { useKibana } from '../../../../../../common/lib/kibana'; import { ExceptionListFilter, useApi, useExceptionLists } from '../../../../../../shared_imports'; import { FormatUrl } from '../../../../../../common/components/link_to'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 64dfac5787f23..29b63721513d4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -9,11 +9,12 @@ import { has, isEmpty } from 'lodash/fp'; import moment from 'moment'; import deepmerge from 'deepmerge'; +import type { ExceptionListType, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/constants'; import { assertUnreachable } from '../../../../../../common/utility_types'; import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; import { List } from '../../../../../../common/detection_engine/schemas/types'; -import { ENDPOINT_LIST_ID, ExceptionListType, NamespaceType } from '../../../../../shared_imports'; +import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; import { Rule } from '../../../../containers/detection_engine/rules'; import { Threats, 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 0fab428ef6d1b..9660132147a57 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 @@ -28,6 +28,7 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { useDeepEqualSelector, useShallowEqualSelector, @@ -83,7 +84,8 @@ import { ExceptionsViewer } from '../../../../../common/components/exceptions/vi import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; -import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports'; +import type { ExceptionListIdentifiers } from '../../../../../shared_imports'; + import { focusUtilityBarAction, onTimelineTabKeyPressed, diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts index 5d600f471994b..e1fa1107fcb01 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts @@ -5,9 +5,8 @@ * 2.0. */ +import { ExceptionListType, ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { - ExceptionListType, - ExceptionListTypeEnum, EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL, ENDPOINT_EVENT_FILTERS_LIST_ID, diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index e77c4a0eec486..76ec761d41703 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -33,23 +33,23 @@ export { ERROR_CODE } from '../../../../src/plugins/es_ui_shared/static/forms/he export { exportList, - useIsMounted, useCursor, useApi, useAsync, useExceptionListItems, useExceptionLists, - usePersistExceptionItem, - usePersistExceptionList, useFindLists, useDeleteList, useImportList, useCreateListIndex, useReadListIndex, useReadListPrivileges, - addExceptionListItem, - updateExceptionListItem, fetchExceptionListById, + addIdToEntries, + getOperatorType, + getNewExceptionItem, + getEntryValue, + getExceptionOperatorSelect, addExceptionList, ExceptionListFilter, ExceptionListIdentifiers, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts index 786a74e91b51a..e4704523a16c3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts @@ -7,17 +7,19 @@ import uuid from 'uuid'; -import { OsType } from '../../../../../lists/common/schemas'; -import { +import type { EntriesArray, EntryMatch, EntryMatchWildcard, EntryNested, - ExceptionListItemSchema, NestedEntriesArray, -} from '../../../../../lists/common'; +} from '@kbn/securitysolution-io-ts-list-types'; + +import type { ExceptionListItemSchema } from '../../../../../lists/common'; + +import type { OsType } from '../../../../../lists/common/schemas'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; -import { +import type { CreateExceptionListItemOptions, UpdateExceptionListItemOptions, } from '../../../../../lists/server'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts index 3fa5d1178b3ec..578c1aba64558 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts @@ -11,7 +11,7 @@ import { mockLogger, sampleDocWithSortId } from '../__mocks__/es_results'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { listMock } from '../../../../../../lists/server/mocks'; import { getSearchListItemResponseMock } from '../../../../../../lists/common/schemas/response/search_list_item_schema.mock'; -import { EntryList } from '../../../../../../lists/common'; +import { EntryList } from '@kbn/securitysolution-io-ts-list-types'; import { buildRuleMessageMock as buildRuleMessage } from '../rule_messages.mock'; describe('filterEventsAgainstList', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts index b2002dbb5a7e2..40322029c1d98 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryList, entriesList } from '../../../../../../lists/common'; +import { EntryList, entriesList } from '@kbn/securitysolution-io-ts-list-types'; import { createSetToFilterAgainst } from './create_set_to_filter_against'; import { CreateFieldAndSetTuplesOptions, FieldSet } from './types'; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx index 5cc1dd12ef961..7ab6c81fbf162 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx @@ -17,6 +17,7 @@ import { EuiDescribedFormGroup, EuiCheckbox, EuiSpacer, + EuiFieldPassword, } from '@elastic/eui'; import { useHTTPAdvancedFieldsContext } from './contexts'; @@ -110,7 +111,7 @@ export const HTTPAdvancedFields = memo(({ validate }) => { /> } > - handleInputChange({ diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx index e01d3d59175a4..de8879ec3a819 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx @@ -13,12 +13,12 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, - EuiFieldText, EuiTextArea, EuiFormFieldset, EuiSelect, EuiScreenReaderOnly, EuiSpacer, + EuiFieldPassword, } from '@elastic/eui'; import { useTLSFieldsContext } from './contexts'; @@ -333,7 +333,7 @@ export const TLSFields: React.FunctionComponent<{ } labelAppend={} > - { const value = event.target.value; diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts index 55a54245cf832..ecef6225632e9 100644 --- a/x-pack/test/functional/apps/status_page/status_page.ts +++ b/x-pack/test/functional/apps/status_page/status_page.ts @@ -12,17 +12,16 @@ export default function statusPageFunctonalTests({ getPageObjects, }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['security', 'statusPage', 'home']); + const PageObjects = getPageObjects(['security', 'statusPage', 'common']); - // FLAKY: https://github.com/elastic/kibana/issues/50448 - describe.skip('Status Page', function () { + describe('Status Page', function () { this.tags(['skipCloud', 'includeFirefox']); before(async () => await esArchiver.load('empty_kibana')); after(async () => await esArchiver.unload('empty_kibana')); it('allows user to navigate without authentication', async () => { await PageObjects.security.forceLogout(); - await PageObjects.statusPage.navigateToPage(); + await PageObjects.common.navigateToApp('status_page', { shouldLoginIfPrompted: false }); await PageObjects.statusPage.expectStatusPage(); }); }); diff --git a/x-pack/test/functional/page_objects/status_page.ts b/x-pack/test/functional/page_objects/status_page.ts index 9edaf4dea53f8..ed90aef954770 100644 --- a/x-pack/test/functional/page_objects/status_page.ts +++ b/x-pack/test/functional/page_objects/status_page.ts @@ -5,36 +5,18 @@ * 2.0. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function StatusPagePageProvider({ getService }: FtrProviderContext) { - const retry = getService('retry'); const log = getService('log'); - const browser = getService('browser'); const find = getService('find'); - const deployment = getService('deployment'); - class StatusPage { async initTests() { log.debug('StatusPage:initTests'); } - async navigateToPage() { - return await retry.try(async () => { - const url = deployment.getHostPort() + '/status'; - log.info(`StatusPage:navigateToPage(): ${url}`); - await browser.get(url); - }); - } - async expectStatusPage(): Promise { - return await retry.try(async () => { - log.debug(`expectStatusPage()`); - await find.byCssSelector('[data-test-subj="statusPageRoot"]', 20000); - const url = await browser.getCurrentUrl(); - expect(url).to.contain(`/status`); - }); + await find.byCssSelector('[data-test-subj="statusPageRoot"]', 20000); } }