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.', + } + )} + > + + + )} +
); };